Emperor555 Claude commited on
Commit
661fd83
·
1 Parent(s): 41b4742

Redesign UI for better visual appeal

Browse files

- Cleaner centered header with improved typography
- Grouped inputs using gr.Group() for card-like sections
- Persona and Audience dropdowns in same row
- Tabbed interface for Details (Agent Tools, Sources, Trace)
- Collapsible MCP Server info accordion
- Compact centered footer
- Added copy button to explanation output
- Better visual hierarchy and spacing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +163 -153
app.py CHANGED
@@ -18,30 +18,59 @@ from src.tts import generate_speech
18
  load_dotenv()
19
 
20
 
21
- # Custom CSS for dark mode input visibility
22
  CUSTOM_CSS = """
23
- /* Fix dark mode input visibility */
24
- input, textarea, select {
25
- color: var(--body-text-color) !important;
26
- background-color: var(--input-background-fill) !important;
27
  }
28
 
29
- input:hover, textarea:hover, select:hover,
30
- input:focus, textarea:focus, select:focus {
31
- color: var(--body-text-color) !important;
32
- background-color: var(--input-background-fill) !important;
33
  }
34
 
35
- /* Ensure placeholder is visible */
36
- input::placeholder, textarea::placeholder {
37
- color: var(--body-text-color-subdued) !important;
38
- opacity: 0.7;
39
  }
40
 
41
- /* Force dark background on inputs */
42
- .dark input, .dark textarea {
43
- background-color: #374151 !important;
44
- color: #ffffff !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
  """
47
 
@@ -61,28 +90,20 @@ def format_sources(sources: list[dict]) -> str:
61
 
62
 
63
  def format_mcp_tools(tools: list[dict]) -> str:
64
- """Format tools used as markdown."""
65
  if not tools:
66
- return "*No tools used*"
67
 
68
- md = "**🔌 Agent Tool Calls:**\n\n"
69
  for tool in tools:
70
- md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\n"
71
  return md
72
 
73
 
74
  def explain_topic(topic: str, persona_name: str, audience: str = "", progress=gr.Progress()):
75
- """Main function to explain a topic in a persona's voice.
76
-
77
- Returns: (explanation_text, sources_md, steps_md, mcp_md)
78
- """
79
  if not topic.strip():
80
- return (
81
- "Please enter a topic to explain!",
82
- "",
83
- "❌ No topic provided",
84
- "",
85
- )
86
 
87
  if not persona_name:
88
  persona_name = "5-Year-Old"
@@ -92,7 +113,6 @@ def explain_topic(topic: str, persona_name: str, audience: str = "", progress=gr
92
  sources = []
93
  mcp_tools = []
94
 
95
- # Run the agent pipeline
96
  progress(0, desc="Starting...")
97
 
98
  for update in run_agent(topic, persona_name, audience):
@@ -101,229 +121,220 @@ def explain_topic(topic: str, persona_name: str, audience: str = "", progress=gr
101
  steps_log.append(step_text)
102
 
103
  if update["step"] == "research":
104
- progress(0.2, desc="Researching...")
105
  elif update["step"] == "research_done":
106
- progress(0.4, desc="Research complete")
107
  if "sources" in update:
108
  sources = update["sources"]
109
  elif update["step"] == "generating":
110
- progress(0.6, desc="Generating explanation...")
111
 
112
  elif update["type"] == "result":
113
  explanation = update["explanation"]
114
  sources = update.get("sources", sources)
115
  mcp_tools = update.get("mcp_tools", [])
116
- progress(1.0, desc="Done!")
117
 
118
- # Format the steps log
119
  steps_md = "\n\n---\n\n".join(steps_log)
120
-
121
- # Format sources
122
  sources_md = format_sources(sources)
123
-
124
- # Format MCP tools
125
  mcp_md = format_mcp_tools(mcp_tools)
126
 
127
  return explanation, sources_md, steps_md, mcp_md
128
 
129
 
130
  def generate_audio(explanation: str, persona_name: str, progress=gr.Progress()):
131
- """Generate audio from the explanation text.
132
-
133
- Returns: audio_path
134
- """
135
  if not explanation or not explanation.strip():
136
  return None
137
 
138
  if not persona_name:
139
  persona_name = "5-Year-Old"
140
 
141
- # Get persona voice settings
142
  persona = get_persona(persona_name)
143
  voice_id = persona["voice_id"]
144
  voice_settings = persona.get("voice_settings")
145
 
146
- progress(0.3, desc="Generating audio...")
147
 
148
  try:
149
  audio_bytes = generate_speech(explanation, voice_id, voice_settings)
150
- # Save to temp file for Gradio
151
  with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
152
  f.write(audio_bytes)
153
  audio_path = f.name
154
- progress(1.0, desc="Audio ready!")
155
  return audio_path
156
  except Exception as e:
157
- progress(1.0, desc="Audio failed")
158
  raise gr.Error(f"Audio generation failed: {str(e)}")
159
 
160
 
161
- # Build the Gradio interface
162
  def create_app():
163
  """Create and configure the Gradio app."""
164
 
165
- with gr.Blocks(title="Explainor - AI Persona Explanations") as app:
166
- # Header
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  gr.Markdown(
168
  """
169
- # 🎭 Explainor
170
- ### *AI Agent with Tool Orchestration*
171
-
172
- **Learn anything through the voice of your favorite characters!**
173
-
174
- This agent orchestrates multiple tools to: research your topic,
175
- extract key facts, transform explanations into character voices, and generate audio.
176
- """
177
  )
178
 
179
- with gr.Row():
180
- with gr.Column(scale=2):
181
- topic_input = gr.Textbox(
182
- label="📝 What do you want to learn about?",
183
- placeholder="e.g., Blockchain, Photosynthesis, Black Holes...",
184
- lines=1,
185
- max_lines=1,
186
- )
 
187
 
188
- with gr.Column(scale=1):
189
- # Build persona choices with emojis
190
- persona_choices = [
191
- f"{PERSONAS[name]['emoji']} {name}"
192
- for name in get_persona_names()
193
- ]
194
  persona_dropdown = gr.Dropdown(
195
  choices=persona_choices,
196
  value=persona_choices[0],
197
- label="🎭 Choose your explainer",
 
 
 
 
 
 
 
198
  )
199
 
200
- with gr.Row():
201
- # Funny listener options (don't overlap with personas)
202
- listener_choices = [
203
- "👤 Just me",
204
- "👵 My confused grandmother",
205
- "🤖 A skeptical robot",
206
- "👽 An alien visiting Earth",
207
- "🧟 A zombie (short attention span)",
208
- "🦊 A very smart fox",
209
- "👔 A stressed CEO",
210
- "🎮 A distracted gamer",
211
- ]
212
- audience_dropdown = gr.Dropdown(
213
- choices=listener_choices,
214
- value=listener_choices[0],
215
- label="👤 Who's listening?",
216
- )
217
-
218
  explain_btn = gr.Button(
219
  "✨ Explain it to me!",
220
  variant="primary",
221
  size="lg",
 
222
  )
223
 
224
- # Output section
225
- with gr.Row():
226
- with gr.Column():
227
- explanation_output = gr.Textbox(
228
- label="📖 Explanation",
229
- lines=8,
230
- max_lines=15,
231
- )
232
 
 
 
233
  read_aloud_btn = gr.Button(
234
  "🔊 Read Aloud",
235
  variant="secondary",
236
- size="sm",
237
  )
238
  audio_output = gr.Audio(
239
- label="🔊 Listen to the explanation",
240
  type="filepath",
241
  autoplay=True,
 
242
  )
243
 
244
- with gr.Row():
245
- with gr.Column():
246
- with gr.Accordion("🔌 Agent Tool Calls", open=True):
247
- mcp_output = gr.Markdown("")
 
248
 
249
- with gr.Row():
250
- with gr.Column():
251
- with gr.Accordion("🔍 Sources", open=False):
252
- sources_output = gr.Markdown("")
253
 
254
- with gr.Column():
255
- with gr.Accordion("🧠 Execution Trace", open=False):
256
- steps_output = gr.Markdown("")
257
 
258
- # Example topics
 
259
  gr.Examples(
260
  examples=[
261
- ["Quantum Computing", f"{PERSONAS['5-Year-Old']['emoji']} 5-Year-Old"],
262
- ["Blockchain", f"{PERSONAS['Gordon Ramsay']['emoji']} Gordon Ramsay"],
263
- ["Black Holes", f"{PERSONAS['Pirate']['emoji']} Pirate"],
264
- ["Machine Learning", f"{PERSONAS['Shakespeare']['emoji']} Shakespeare"],
265
- ["Climate Change", f"{PERSONAS['Surfer Dude']['emoji']} Surfer Dude"],
266
- ["The Force", f"{PERSONAS['Yoda']['emoji']} Yoda"],
267
  ],
268
  inputs=[topic_input, persona_dropdown],
269
- label="Try these examples:",
270
  )
271
 
272
- # MCP Info
273
- gr.Markdown(
274
- """
275
- ---
276
- ### 🔌 MCP Server Enabled!
277
 
278
- This app is an **MCP Server**! Connect it to Claude Desktop or any MCP client:
 
 
279
 
280
- ```
281
- https://kaiser-data-mcp-1st-birthday-explainor.hf.space/gradio_api/mcp/sse
282
- ```
283
- """
284
- )
285
 
286
- # Footer
287
  gr.Markdown(
288
  """
289
- ---
290
- **Built for MCP's 1st Birthday Hackathon** | Track: MCP in Action (Creative)
291
-
292
- Powered by: [Nebius AI](https://nebius.com) (LLM) + [ElevenLabs](https://elevenlabs.io) (TTS)
293
-
294
- Made with ❤️ by **kaiser-data**
295
- """
296
  )
297
 
298
- # Event handler for explanation
299
  def process_and_explain(topic, persona_with_emoji, audience_with_emoji):
300
- # Extract persona name (remove emoji prefix)
301
  persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
302
- # Extract audience (remove emoji prefix), skip if "Just me"
303
  audience = ""
304
  if audience_with_emoji and "Just me" not in audience_with_emoji:
305
  audience = audience_with_emoji.split(" ", 1)[1] if " " in audience_with_emoji else audience_with_emoji
306
  return explain_topic(topic, persona_name, audience)
307
 
 
 
 
 
 
308
  explain_btn.click(
309
  fn=process_and_explain,
310
  inputs=[topic_input, persona_dropdown, audience_dropdown],
311
  outputs=[explanation_output, sources_output, steps_output, mcp_output],
312
  )
313
 
314
- # Also trigger on Enter key in topic input
315
  topic_input.submit(
316
  fn=process_and_explain,
317
  inputs=[topic_input, persona_dropdown, audience_dropdown],
318
  outputs=[explanation_output, sources_output, steps_output, mcp_output],
319
  )
320
 
321
- # Event handler for audio generation
322
- def process_audio(explanation, persona_with_emoji):
323
- # Extract persona name (remove emoji prefix)
324
- persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
325
- return generate_audio(explanation, persona_name)
326
-
327
  read_aloud_btn.click(
328
  fn=process_audio,
329
  inputs=[explanation_output, persona_dropdown],
@@ -337,7 +348,6 @@ def create_app():
337
  app = create_app()
338
 
339
  if __name__ == "__main__":
340
- # MCP server enabled for Gradio 5+
341
  enable_mcp = os.getenv("ENABLE_MCP_SERVER", "true").lower() == "true"
342
 
343
  app.launch(
 
18
  load_dotenv()
19
 
20
 
21
+ # Custom CSS for better styling
22
  CUSTOM_CSS = """
23
+ /* Dark mode input fix */
24
+ .dark input, .dark textarea {
25
+ background-color: #374151 !important;
26
+ color: #ffffff !important;
27
  }
28
 
29
+ /* Header styling */
30
+ .header-container {
31
+ text-align: center;
32
+ padding: 1rem 0;
33
  }
34
 
35
+ /* Card-like sections */
36
+ .input-section, .output-section {
37
+ border-radius: 12px;
38
+ padding: 1rem;
39
  }
40
 
41
+ /* Primary button enhancement */
42
+ .primary-btn {
43
+ font-size: 1.1rem !important;
44
+ padding: 0.75rem 2rem !important;
45
+ }
46
+
47
+ /* Audio section layout */
48
+ .audio-row {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 1rem;
52
+ }
53
+
54
+ /* Persona cards in examples */
55
+ .example-row {
56
+ margin-top: 0.5rem;
57
+ }
58
+
59
+ /* Footer styling */
60
+ .footer {
61
+ text-align: center;
62
+ opacity: 0.8;
63
+ font-size: 0.9rem;
64
+ }
65
+
66
+ /* MCP badge */
67
+ .mcp-badge {
68
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
69
+ color: white;
70
+ padding: 0.5rem 1rem;
71
+ border-radius: 8px;
72
+ display: inline-block;
73
+ font-weight: bold;
74
  }
75
  """
76
 
 
90
 
91
 
92
  def format_mcp_tools(tools: list[dict]) -> str:
93
+ """Format tools used as markdown table."""
94
  if not tools:
95
+ return "*Waiting for explanation...*"
96
 
97
+ md = "| Tool | Description |\n|------|-------------|\n"
98
  for tool in tools:
99
+ md += f"| {tool['icon']} `{tool['name']}` | {tool['desc']} |\n"
100
  return md
101
 
102
 
103
  def explain_topic(topic: str, persona_name: str, audience: str = "", progress=gr.Progress()):
104
+ """Main function to explain a topic in a persona's voice."""
 
 
 
105
  if not topic.strip():
106
+ return "Please enter a topic to explain!", "", "", ""
 
 
 
 
 
107
 
108
  if not persona_name:
109
  persona_name = "5-Year-Old"
 
113
  sources = []
114
  mcp_tools = []
115
 
 
116
  progress(0, desc="Starting...")
117
 
118
  for update in run_agent(topic, persona_name, audience):
 
121
  steps_log.append(step_text)
122
 
123
  if update["step"] == "research":
124
+ progress(0.2, desc="🔍 Researching...")
125
  elif update["step"] == "research_done":
126
+ progress(0.4, desc="📚 Research complete")
127
  if "sources" in update:
128
  sources = update["sources"]
129
  elif update["step"] == "generating":
130
+ progress(0.6, desc="🎭 Generating explanation...")
131
 
132
  elif update["type"] == "result":
133
  explanation = update["explanation"]
134
  sources = update.get("sources", sources)
135
  mcp_tools = update.get("mcp_tools", [])
136
+ progress(1.0, desc="Done!")
137
 
 
138
  steps_md = "\n\n---\n\n".join(steps_log)
 
 
139
  sources_md = format_sources(sources)
 
 
140
  mcp_md = format_mcp_tools(mcp_tools)
141
 
142
  return explanation, sources_md, steps_md, mcp_md
143
 
144
 
145
  def generate_audio(explanation: str, persona_name: str, progress=gr.Progress()):
146
+ """Generate audio from the explanation text."""
 
 
 
147
  if not explanation or not explanation.strip():
148
  return None
149
 
150
  if not persona_name:
151
  persona_name = "5-Year-Old"
152
 
 
153
  persona = get_persona(persona_name)
154
  voice_id = persona["voice_id"]
155
  voice_settings = persona.get("voice_settings")
156
 
157
+ progress(0.3, desc="🔊 Generating audio...")
158
 
159
  try:
160
  audio_bytes = generate_speech(explanation, voice_id, voice_settings)
 
161
  with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
162
  f.write(audio_bytes)
163
  audio_path = f.name
164
+ progress(1.0, desc="Audio ready!")
165
  return audio_path
166
  except Exception as e:
167
+ progress(1.0, desc="Audio failed")
168
  raise gr.Error(f"Audio generation failed: {str(e)}")
169
 
170
 
 
171
  def create_app():
172
  """Create and configure the Gradio app."""
173
 
174
+ # Build persona choices
175
+ persona_choices = [
176
+ f"{PERSONAS[name]['emoji']} {name}"
177
+ for name in get_persona_names()
178
+ ]
179
+
180
+ # Audience choices
181
+ audience_choices = [
182
+ "👤 Just me",
183
+ "👵 Confused grandmother",
184
+ "🤖 Skeptical robot",
185
+ "👽 Alien visitor",
186
+ "🧟 Zombie",
187
+ "👔 Stressed CEO",
188
+ ]
189
+
190
+ with gr.Blocks(title="Explainor", fill_width=True) as app:
191
+
192
+ # ===== HEADER =====
193
  gr.Markdown(
194
  """
195
+ <div style="text-align: center; padding: 1rem 0;">
196
+ <h1>🎭 Explainor</h1>
197
+ <p style="font-size: 1.2rem; opacity: 0.9;">Learn anything through the voice of your favorite characters!</p>
198
+ </div>
199
+ """,
200
+ elem_classes=["header-container"]
 
 
201
  )
202
 
203
+ # ===== INPUT SECTION =====
204
+ with gr.Group():
205
+ # Topic input - full width, prominent
206
+ topic_input = gr.Textbox(
207
+ label="What do you want to learn about?",
208
+ placeholder="Try: Quantum Computing, Blockchain, Black Holes, Climate Change...",
209
+ lines=1,
210
+ scale=2,
211
+ )
212
 
213
+ # Persona and Audience in one row
214
+ with gr.Row():
 
 
 
 
215
  persona_dropdown = gr.Dropdown(
216
  choices=persona_choices,
217
  value=persona_choices[0],
218
+ label="🎭 Explainer",
219
+ scale=1,
220
+ )
221
+ audience_dropdown = gr.Dropdown(
222
+ choices=audience_choices,
223
+ value=audience_choices[0],
224
+ label="👤 Audience",
225
+ scale=1,
226
  )
227
 
228
+ # ===== ACTION BUTTON =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  explain_btn = gr.Button(
230
  "✨ Explain it to me!",
231
  variant="primary",
232
  size="lg",
233
+ elem_classes=["primary-btn"],
234
  )
235
 
236
+ # ===== OUTPUT SECTION =====
237
+ with gr.Group():
238
+ explanation_output = gr.Textbox(
239
+ label="📖 Explanation",
240
+ lines=6,
241
+ show_copy_button=True,
242
+ )
 
243
 
244
+ # Audio controls in a row
245
+ with gr.Row():
246
  read_aloud_btn = gr.Button(
247
  "🔊 Read Aloud",
248
  variant="secondary",
249
+ scale=1,
250
  )
251
  audio_output = gr.Audio(
252
+ label="Listen",
253
  type="filepath",
254
  autoplay=True,
255
+ scale=3,
256
  )
257
 
258
+ # ===== DETAILS SECTION (Tabs) =====
259
+ with gr.Accordion("📊 Details", open=False):
260
+ with gr.Tabs():
261
+ with gr.TabItem("🔧 Agent Tools"):
262
+ mcp_output = gr.Markdown("*Run an explanation to see tool calls*")
263
 
264
+ with gr.TabItem("📚 Sources"):
265
+ sources_output = gr.Markdown("*Sources will appear here*")
 
 
266
 
267
+ with gr.TabItem("🔍 Trace"):
268
+ steps_output = gr.Markdown("*Execution trace will appear here*")
 
269
 
270
+ # ===== EXAMPLES =====
271
+ gr.Markdown("### 💡 Try these examples")
272
  gr.Examples(
273
  examples=[
274
+ ["Quantum Computing", "👶 5-Year-Old"],
275
+ ["Blockchain", "👨‍🍳 Gordon Ramsay"],
276
+ ["Black Holes", "🏴‍☠️ Pirate"],
277
+ ["Machine Learning", "🎭 Shakespeare"],
278
+ ["Climate Change", "🏄 Surfer Dude"],
279
+ ["The Force", "🧙 Yoda"],
280
  ],
281
  inputs=[topic_input, persona_dropdown],
282
+ label="",
283
  )
284
 
285
+ # ===== MCP INFO =====
286
+ with gr.Accordion("🔌 MCP Server", open=False):
287
+ gr.Markdown(
288
+ """
289
+ This app is an **MCP Server**! Connect it to Claude Desktop or any MCP client:
290
 
291
+ ```
292
+ https://kaiser-data-mcp-1st-birthday-explainor.hf.space/gradio_api/mcp/sse
293
+ ```
294
 
295
+ **Available Tools:** `explain_topic`, `generate_audio`
296
+ """
297
+ )
 
 
298
 
299
+ # ===== FOOTER =====
300
  gr.Markdown(
301
  """
302
+ <div style="text-align: center; padding: 1rem 0; opacity: 0.7; font-size: 0.85rem;">
303
+ <strong>MCP's 1st Birthday Hackathon</strong> · Track: MCP in Action (Creative)<br/>
304
+ Powered by <a href="https://nebius.com">Nebius AI</a> + <a href="https://elevenlabs.io">ElevenLabs</a> ·
305
+ Made with ❤️ by <strong>kaiser-data</strong>
306
+ </div>
307
+ """,
308
+ elem_classes=["footer"]
309
  )
310
 
311
+ # ===== EVENT HANDLERS =====
312
  def process_and_explain(topic, persona_with_emoji, audience_with_emoji):
 
313
  persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
 
314
  audience = ""
315
  if audience_with_emoji and "Just me" not in audience_with_emoji:
316
  audience = audience_with_emoji.split(" ", 1)[1] if " " in audience_with_emoji else audience_with_emoji
317
  return explain_topic(topic, persona_name, audience)
318
 
319
+ def process_audio(explanation, persona_with_emoji):
320
+ persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
321
+ return generate_audio(explanation, persona_name)
322
+
323
+ # Explain button click
324
  explain_btn.click(
325
  fn=process_and_explain,
326
  inputs=[topic_input, persona_dropdown, audience_dropdown],
327
  outputs=[explanation_output, sources_output, steps_output, mcp_output],
328
  )
329
 
330
+ # Enter key in topic input
331
  topic_input.submit(
332
  fn=process_and_explain,
333
  inputs=[topic_input, persona_dropdown, audience_dropdown],
334
  outputs=[explanation_output, sources_output, steps_output, mcp_output],
335
  )
336
 
337
+ # Read aloud button
 
 
 
 
 
338
  read_aloud_btn.click(
339
  fn=process_audio,
340
  inputs=[explanation_output, persona_dropdown],
 
348
  app = create_app()
349
 
350
  if __name__ == "__main__":
 
351
  enable_mcp = os.getenv("ENABLE_MCP_SERVER", "true").lower() == "true"
352
 
353
  app.launch(