Skip to content

Commit d2576cf

Browse files
feat(ui): implement quick setup wizard phase 2 (#138)
- Replaced basic setup wizard with multi-step flow: 1. AI Backend Selection (OpenAI, Groq, Ollama) 2. Model Configuration (Whisper, LLM, Diarization) 3. Campaign Creation & Connectivity Test - Added visual stepper component via `src/ui/theme.py` CSS. - Integrated with `ConfigManager` and `save_api_keys` for persistence. - Maintained compatibility with `app.py` wiring. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent dea5519 commit d2576cf

1 file changed

Lines changed: 280 additions & 34 deletions

File tree

src/ui/setup_wizard.py

Lines changed: 280 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
21
"""
32
Setup Wizard UI for first-time users.
43
"""
54
import gradio as gr
6-
from typing import Tuple, Optional
5+
from typing import Tuple, Optional, Dict, Any
6+
import logging
77

88
from src.ui.helpers import StatusMessages, UIComponents
99
from src.ui.api_key_manager import save_api_keys
10+
from src.ui.config_manager import ConfigManager
1011
from src.party_config import CampaignManager
12+
from src.config import Config
13+
14+
logger = logging.getLogger(__name__)
15+
16+
# Stepper CSS styles are in theme.py, utilizing .stepper, .step, etc.
1117

1218
class SetupWizard:
1319
"""
14-
Manages the First-Time Setup Wizard UI.
20+
Manages the First-Time Setup Wizard UI (Phase 2).
1521
"""
1622

1723
def __init__(self, campaign_manager: CampaignManager, on_complete_callback):
@@ -25,65 +31,305 @@ def create_ui(self) -> Tuple[gr.Column, gr.Button, gr.Textbox]:
2531
Returns:
2632
Tuple of (wizard_container, complete_button, campaign_name_input)
2733
"""
34+
# Load initial config to pre-populate
35+
current_config = ConfigManager.load_env_config()
36+
2837
with gr.Column(visible=False, elem_id="setup-wizard") as wizard_container:
38+
# Header
2939
gr.Markdown("# \u2728 Welcome to D&D Session Processor")
30-
gr.Markdown("It looks like this is your first time here. Let's get you set up in a few steps.")
40+
gr.Markdown("Let's get your environment set up in 3 easy steps.")
3141

32-
with gr.Group():
33-
gr.Markdown("### 1. API Configuration (Optional)")
34-
gr.Markdown("Configure API keys for cloud services. You can skip this if you plan to use local models only.")
42+
# Stepper UI
43+
step_tracker = gr.State(value=1)
3544

36-
with gr.Row():
37-
openai_key = gr.Textbox(label="OpenAI API Key", type="password", placeholder="sk-...")
38-
groq_key = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_...")
39-
hf_key = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_...")
45+
# We use HTML to render the stepper. The CSS classes used here
46+
# (.stepper, .step, .step.active, .step.completed) must be present in theme.py
47+
stepper_html = gr.HTML(self._generate_stepper_html(1))
4048

41-
save_keys_btn = gr.Button("Save API Keys", size="sm")
42-
keys_status = gr.Markdown("")
49+
# --- STEP 1: AI Backend ---
50+
with gr.Column(visible=True) as step1_container:
51+
gr.Markdown("### 1. Choose Your AI Backend")
52+
gr.Markdown("Select the primary service you want to use. You can change this later.")
4353

44-
save_keys_btn.click(
45-
fn=self._save_keys,
46-
inputs=[openai_key, groq_key, hf_key],
47-
outputs=keys_status
54+
backend_choice = gr.Radio(
55+
choices=["OpenAI (Recommended)", "Groq (Fast)", "Ollama (Local)", "Custom / Mixed"],
56+
value="OpenAI (Recommended)",
57+
label="Primary Backend"
4858
)
4959

50-
gr.Markdown("---")
60+
# API Key Inputs
61+
with gr.Group():
62+
openai_key = gr.Textbox(
63+
label="OpenAI API Key",
64+
type="password",
65+
placeholder="sk-...",
66+
value=current_config.get("OPENAI_API_KEY", ""),
67+
visible=True
68+
)
69+
groq_key = gr.Textbox(
70+
label="Groq API Key",
71+
type="password",
72+
placeholder="gsk_...",
73+
value=current_config.get("GROQ_API_KEY", ""),
74+
visible=False
75+
)
76+
hf_key = gr.Textbox(
77+
label="Hugging Face Token (Optional, for PyAnnote)",
78+
type="password",
79+
placeholder="hf_...",
80+
value=current_config.get("HUGGING_FACE_API_KEY", ""),
81+
info="Required only if you use HuggingFace-based diarization."
82+
)
83+
ollama_url = gr.Textbox(
84+
label="Ollama URL",
85+
value=current_config.get("OLLAMA_BASE_URL", "http://localhost:11434"),
86+
visible=False
87+
)
88+
89+
step1_status = gr.Markdown("")
90+
91+
with gr.Row():
92+
# Empty column to push button to right
93+
gr.Column(scale=1)
94+
step1_next_btn = gr.Button("Next: Configure Models \u2192", variant="primary", scale=0)
95+
96+
# --- STEP 2: Model Configuration ---
97+
with gr.Column(visible=False) as step2_container:
98+
gr.Markdown("### 2. Configure Models")
99+
gr.Markdown("We've selected defaults based on your backend choice. Review and adjust if needed.")
100+
101+
with gr.Group():
102+
with gr.Row():
103+
whisper_backend = gr.Dropdown(
104+
choices=ConfigManager.VALID_WHISPER_BACKENDS,
105+
value=current_config.get("WHISPER_BACKEND", "openai"),
106+
label="Whisper Backend (Transcription)"
107+
)
108+
whisper_model = gr.Dropdown(
109+
choices=ConfigManager.VALID_WHISPER_MODELS,
110+
value=current_config.get("WHISPER_MODEL", "large-v3"),
111+
label="Whisper Model"
112+
)
113+
114+
with gr.Row():
115+
llm_backend = gr.Dropdown(
116+
choices=ConfigManager.VALID_LLM_BACKENDS,
117+
value=current_config.get("LLM_BACKEND", "openai"),
118+
label="LLM Backend (Analysis)"
119+
)
120+
diarization_backend = gr.Dropdown(
121+
choices=ConfigManager.VALID_DIARIZATION_BACKENDS,
122+
value=current_config.get("DIARIZATION_BACKEND", "local"),
123+
label="Diarization Backend"
124+
)
125+
126+
step2_status = gr.Markdown("")
127+
128+
with gr.Row():
129+
step2_back_btn = gr.Button("\u2190 Back", variant="secondary", scale=0)
130+
gr.Column(scale=1)
131+
step2_next_btn = gr.Button("Next: Finalize \u2192", variant="primary", scale=0)
132+
133+
# --- STEP 3: Finalize ---
134+
with gr.Column(visible=False) as step3_container:
135+
gr.Markdown("### 3. Create Campaign & Finish")
51136

52-
with gr.Group():
53-
gr.Markdown("### 2. Create Your First Campaign (Required)")
54-
gr.Markdown("A campaign holds all your sessions, characters, and lore.")
137+
# Connectivity Test
138+
with gr.Group():
139+
gr.Markdown("**Pre-flight Check**")
140+
test_status = gr.Markdown("Click 'Test Connection' to verify your settings.")
141+
test_btn = gr.Button("Test Connection", size="sm")
55142

143+
gr.Markdown("---")
144+
145+
# Campaign Creation
146+
gr.Markdown("**First Campaign**")
56147
campaign_name = gr.Textbox(
57148
label="Campaign Name",
58149
placeholder="e.g., The Curse of Strahd",
59-
info="You can change this later."
150+
info="A campaign organizes your sessions and characters."
60151
)
61152

62-
create_btn = UIComponents.create_action_button("Create Campaign & Finish Setup", variant="primary")
63153
creation_status = gr.Markdown("")
64154

65-
# Event wiring
66-
# We trigger the creation method, then hide the wizard.
67-
# The external caller will wire the "on complete" logic to show the main dashboard
155+
with gr.Row():
156+
step3_back_btn = gr.Button("\u2190 Back", variant="secondary", scale=0)
157+
gr.Column(scale=1)
158+
# This is the "Finish" button app.py expects
159+
create_btn = UIComponents.create_action_button("Create Campaign & Start", variant="primary")
160+
161+
# --- Event Wiring ---
162+
163+
# Step 1: Backend Choice Logic
164+
def _update_visibility(choice):
165+
return (
166+
gr.update(visible=(choice == "OpenAI (Recommended)" or choice == "Custom / Mixed")),
167+
gr.update(visible=(choice == "Groq (Fast)" or choice == "Custom / Mixed")),
168+
gr.update(visible=(choice == "Ollama (Local)" or choice == "Custom / Mixed"))
169+
)
170+
171+
backend_choice.change(
172+
fn=_update_visibility,
173+
inputs=[backend_choice],
174+
outputs=[openai_key, groq_key, ollama_url]
175+
)
176+
177+
# Step 1 -> Step 2
178+
step1_next_btn.click(
179+
fn=self._save_step1_and_advance,
180+
inputs=[backend_choice, openai_key, groq_key, hf_key, ollama_url],
181+
outputs=[step1_status, step_tracker, stepper_html, step1_container, step2_container, whisper_backend, llm_backend, diarization_backend]
182+
)
183+
184+
# Step 2 -> Step 3
185+
step2_next_btn.click(
186+
fn=self._save_step2_and_advance,
187+
inputs=[whisper_backend, whisper_model, llm_backend, diarization_backend],
188+
outputs=[step2_status, step_tracker, stepper_html, step2_container, step3_container]
189+
)
190+
191+
# Back Buttons
192+
step2_back_btn.click(
193+
fn=lambda: (1, self._generate_stepper_html(1), gr.update(visible=True), gr.update(visible=False)),
194+
outputs=[step_tracker, stepper_html, step1_container, step2_container]
195+
)
196+
197+
step3_back_btn.click(
198+
fn=lambda: (2, self._generate_stepper_html(2), gr.update(visible=True), gr.update(visible=False)),
199+
outputs=[step_tracker, stepper_html, step2_container, step3_container]
200+
)
201+
202+
# Test Connection
203+
test_btn.click(
204+
fn=self._test_connection,
205+
outputs=[test_status]
206+
)
207+
208+
# Finish
68209
create_btn.click(
69210
fn=self._complete_setup,
70211
inputs=[campaign_name],
71212
outputs=[creation_status, wizard_container]
72213
)
73214

74-
# Return the components needed for external wiring
75215
return wizard_container, create_btn, campaign_name
76216

77-
def _save_keys(self, openai, groq, hf):
217+
def _generate_stepper_html(self, current_step: int) -> str:
218+
"""Generates HTML for the progress stepper."""
219+
steps = ["Backend", "Models", "Finalize"]
220+
html = '<div class="stepper">'
221+
222+
for i, label in enumerate(steps, 1):
223+
active_class = "active" if i == current_step else ""
224+
completed_class = "completed" if i < current_step else ""
225+
226+
# Connector
227+
if i > 1:
228+
# We add a visual connector, maybe CSS handles it or we add a div
229+
pass
230+
231+
html += f"""
232+
<div class="step {active_class} {completed_class}">
233+
<div class="step-connector"></div>
234+
<div class="step-number">{i if i >= current_step else "✓"}</div>
235+
<div class="step-label">{label}</div>
236+
</div>
237+
"""
238+
html += '</div>'
239+
return html
240+
241+
def _save_step1_and_advance(self, backend_choice, openai, groq, hf, ollama_url):
242+
# 1. Save API Keys
78243
try:
79-
save_api_keys({
80-
"OPENAI_API_KEY": openai,
81-
"GROQ_API_KEY": groq,
82-
"HUGGING_FACE_API_KEY": hf
244+
keys_to_save = {}
245+
if openai: keys_to_save["OPENAI_API_KEY"] = openai
246+
if groq: keys_to_save["GROQ_API_KEY"] = groq
247+
if hf: keys_to_save["HUGGING_FACE_API_KEY"] = hf
248+
249+
if keys_to_save:
250+
save_api_keys(**{k.lower(): v for k, v in keys_to_save.items()})
251+
252+
# 2. Save Ollama URL if changed
253+
if ollama_url:
254+
ConfigManager.save_config({"OLLAMA_BASE_URL": ollama_url})
255+
256+
# 3. Determine defaults for next step
257+
whisper_def = "openai"
258+
llm_def = "openai"
259+
diarization_def = "local" # Default unless HF key present?
260+
261+
if backend_choice == "Groq (Fast)":
262+
whisper_def = "groq"
263+
llm_def = "groq"
264+
elif backend_choice == "Ollama (Local)":
265+
whisper_def = "local"
266+
llm_def = "ollama"
267+
268+
# If HF key is provided, suggest huggingface diarization?
269+
# Or stick to local as safer default.
270+
271+
return (
272+
gr.update(visible=False), # Clear status
273+
2, # Step 2
274+
self._generate_stepper_html(2),
275+
gr.update(visible=False), # Hide Step 1
276+
gr.update(visible=True), # Show Step 2
277+
gr.update(value=whisper_def),
278+
gr.update(value=llm_def),
279+
gr.update(value=diarization_def)
280+
)
281+
282+
except Exception as e:
283+
return (
284+
StatusMessages.error("Error", str(e)),
285+
1,
286+
self._generate_stepper_html(1),
287+
gr.update(visible=True),
288+
gr.update(visible=False),
289+
gr.update(), gr.update(), gr.update()
290+
)
291+
292+
def _save_step2_and_advance(self, whisper, whisper_model, llm, diarization):
293+
try:
294+
# Save Config
295+
ConfigManager.save_config({
296+
"WHISPER_BACKEND": whisper,
297+
"WHISPER_MODEL": whisper_model,
298+
"LLM_BACKEND": llm,
299+
"DIARIZATION_BACKEND": diarization
83300
})
84-
return StatusMessages.success("Saved", "API keys stored successfully.")
301+
302+
return (
303+
gr.update(visible=False),
304+
3,
305+
self._generate_stepper_html(3),
306+
gr.update(visible=False),
307+
gr.update(visible=True)
308+
)
85309
except Exception as e:
86-
return StatusMessages.error("Error", str(e))
310+
return (
311+
StatusMessages.error("Error", str(e)),
312+
2,
313+
self._generate_stepper_html(2),
314+
gr.update(visible=True),
315+
gr.update(visible=False)
316+
)
317+
318+
def _test_connection(self):
319+
# Basic check: Do we have keys for the selected backends?
320+
config = ConfigManager.load_env_config()
321+
llm = config.get("LLM_BACKEND", "openai")
322+
323+
missing = []
324+
if llm == "openai" and not config.get("OPENAI_API_KEY"):
325+
missing.append("OpenAI API Key")
326+
if llm == "groq" and not config.get("GROQ_API_KEY"):
327+
missing.append("Groq API Key")
328+
329+
if missing:
330+
return StatusMessages.error("Missing Keys", f"Please provide: {', '.join(missing)}")
331+
332+
return StatusMessages.success("Ready", "Configuration looks good! Ready to create campaign.")
87333

88334
def _complete_setup(self, name):
89335
if not name.strip():

0 commit comments

Comments
 (0)