1-
21"""
32Setup Wizard UI for first-time users.
43"""
54import gradio as gr
6- from typing import Tuple , Optional
5+ from typing import Tuple , Optional , Dict , Any
6+ import logging
77
88from src .ui .helpers import StatusMessages , UIComponents
99from src .ui .api_key_manager import save_api_keys
10+ from src .ui .config_manager import ConfigManager
1011from 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
1218class 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