IndraneelKumar commited on
Commit
dc59d80
Β·
1 Parent(s): 56756c6

Redesigned Frontend Gradio UI

Browse files
Files changed (1) hide show
  1. frontend/app.py +316 -110
frontend/app.py CHANGED
@@ -38,6 +38,231 @@ feed_names = [f["name"] for f in feeds]
38
  feed_authors = [f["author"] for f in feeds]
39
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # -----------------------
42
  # API helpers
43
  # -----------------------
@@ -156,26 +381,27 @@ def handle_search_articles(query_text, feed_name, feed_author, title_keywords, l
156
  if not results:
157
  return "No results found."
158
 
159
- html_output = ""
160
  for item in results:
 
 
 
 
 
161
  html_output += (
162
- f"<div style='background-color:#F0F8FF; padding:20px; "
163
- f"border-radius:10px; font-size:18px; margin-bottom:15px;'>\n"
164
- f" <h2 style='font-size:22px; color:#1f4e79; margin-top:0;'>"
165
- f"{item.get('title', 'No title')}</h2>\n"
166
- f" <p style='margin:5px 0;'>"
167
- f"<b>Newsletter:</b> {item.get('feed_name', 'N/A')}"
168
- f"</p>\n"
169
- f" <p style='margin:5px 0;'>"
170
- f"<b>Author:</b> {item.get('feed_author', 'N/A')}"
171
- f"</p>\n"
172
- f" <p style='margin:5px 0;'><b>Article Authors:</b> "
173
- f"{', '.join(item.get('article_author') or ['N/A'])}</p>\n"
174
- f" <p style='margin:5px 0;'><b>URL:</b> "
175
- f"<a href='{item.get('url', '#')}' target='_blank' style='color:#0066cc;'>"
176
- f"{item.get('url', 'No URL')}</a></p>\n"
177
- f"</div>\n"
178
  )
 
179
  return html_output
180
 
181
  except Exception as e:
@@ -224,40 +450,30 @@ def handle_ai_question_streaming(
224
 
225
  try:
226
  answer_html = ""
227
- model_info = f"Provider: {provider}"
228
 
229
  for _, (event_type, content) in enumerate(call_ai(payload, streaming=True)):
230
  if event_type == "text":
231
  # Convert markdown to HTML
232
  html_content = markdown.markdown(content, extensions=["tables"])
233
- answer_html = (
234
- f"\n"
235
- f"<div style='background-color:#E8F0FE; "
236
- f"padding:15px; border-radius:10px; font-size:16px;'>\n"
237
- f" {html_content}\n"
238
- f"</div>\n"
239
- )
240
  yield answer_html, model_info
241
 
242
  elif event_type == "model":
243
- model_info = f"Provider: {provider} | Model: {content}"
244
  yield answer_html, model_info
245
 
246
  elif event_type == "truncated":
247
- answer_html += (
248
- f"<div style='color:#ff6600; padding:10px; font-weight:bold;'>⚠️ {content}</div>"
249
- )
250
  yield answer_html, model_info
251
 
252
  elif event_type == "error":
253
- error_html = (
254
- f"<div style='color:red; padding:10px; font-weight:bold;'>❌ {content}</div>"
255
- )
256
  yield error_html, model_info
257
  break
258
 
259
  except Exception as e:
260
- error_html = f"<div style='color:red; padding:10px;'>Error: {str(e)}</div>"
261
  yield error_html, model_info
262
 
263
 
@@ -295,26 +511,19 @@ def handle_ai_question_non_streaming(query_text, feed_name, feed_author, limit,
295
 
296
  try:
297
  answer_html = ""
298
- model_info = f"Provider: {provider}"
299
 
300
  for event_type, content in call_ai(payload, streaming=False):
301
  if event_type == "text":
302
  html_content = markdown.markdown(content, extensions=["tables"])
303
- answer_html = (
304
- "<div style='background-color:#E8F0FE; "
305
- "padding:15px; border-radius:10px; font-size:16px;'>\n"
306
- f"{html_content}\n"
307
- "</div>\n"
308
- )
309
  elif event_type == "model":
310
- model_info = f"Provider: {provider} | Model: {content}"
311
  elif event_type == "truncated":
312
- answer_html += (
313
- f"<div style='color:#ff6600; padding:10px; font-weight:bold;'>⚠️ {content}</div>"
314
- )
315
  elif event_type == "error":
316
  return (
317
- f"<div style='color:red; padding:10px; font-weight:bold;'>❌ {content}</div>",
318
  model_info,
319
  )
320
 
@@ -322,8 +531,8 @@ def handle_ai_question_non_streaming(query_text, feed_name, feed_author, limit,
322
 
323
  except Exception as e:
324
  return (
325
- f"<div style='color:red; padding:10px;'>Error: {str(e)}</div>",
326
- f"Provider: {provider}",
327
  )
328
 
329
 
@@ -342,76 +551,73 @@ def update_model_choices(provider):
342
  # -----------------------
343
  # Gradio UI
344
  # -----------------------
345
- with gr.Blocks(title="Substack Articles LLM Engine", theme=gr.themes.Soft()) as demo:
346
  # Header
347
  gr.HTML(
348
- "<div style='background-color:#ff6719; padding:20px; border-radius:12px; "
349
- "text-align:center; margin-bottom:20px;'>\n"
350
- " <h1 style='color:white; font-size:42px; font-family:serif; margin:0;'>\n"
351
- " πŸ“° Substack Articles LLM Engine\n"
352
- " </h1>\n"
353
- "</div>\n"
354
  )
355
 
356
  with gr.Row():
357
- with gr.Column(scale=1):
358
- # Search Mode Selection
359
- gr.Markdown("## πŸ” Select Search Mode")
360
- search_type = gr.Radio(
361
- choices=["Search Articles", "Ask the AI"],
362
- value="Search Articles",
363
- label="Search Mode",
364
- info="Choose between searching for articles or asking AI questions",
365
- )
366
-
367
- # Common filters
368
- gr.Markdown("### Filters")
369
- query_text = gr.Textbox(label="Query", placeholder="Type your query here...", lines=3)
370
- feed_author = gr.Dropdown(
371
- choices=[""] + feed_authors, label="Author (optional)", value=""
372
- )
373
- feed_name = gr.Dropdown(
374
- choices=[""] + feed_names, label="Newsletter (optional)", value=""
375
- )
376
-
377
- # Conditional fields based on search type
378
- title_keywords = gr.Textbox(
379
- label="Title Keywords (optional)",
380
- placeholder="Filter by words in the title",
381
- visible=True,
382
- )
383
-
384
- limit = gr.Slider(
385
- minimum=1, maximum=20, step=1, label="Number of results", value=5, visible=True
386
- )
387
-
388
- # LLM Options (only visible for AI mode)
389
- with gr.Group(visible=False) as llm_options:
390
- gr.Markdown("### βš™οΈ LLM Options")
391
- provider = gr.Dropdown(
392
- choices=["OpenRouter", "HuggingFace", "OpenAI"],
393
- label="Select LLM Provider",
394
- value="OpenRouter",
395
- )
396
- model = gr.Dropdown(
397
- choices=get_models_for_provider("OpenRouter"),
398
- label="Select Model",
399
- value="Automatic Model Selection (Model Routing)",
400
  )
401
- streaming_mode = gr.Radio(
402
- choices=["Streaming", "Non-Streaming"],
403
- value="Streaming",
404
- label="Answer Mode",
405
- info="Streaming shows results as they're generated",
406
- )
407
-
408
- # Submit button
409
- submit_btn = gr.Button("πŸ”Ž Search / Ask AI", variant="primary", size="lg")
410
 
411
- with gr.Column(scale=2):
412
- # Output area
413
- output_html = gr.HTML(label="Results")
414
- model_info = gr.HTML(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  # Event handlers
417
  def toggle_visibility(search_type):
 
38
  feed_authors = [f["author"] for f in feeds]
39
 
40
 
41
+ # -----------------------
42
+ # Custom CSS for modern UI
43
+ # -----------------------
44
+ CUSTOM_CSS = """
45
+ /* Modern, clean UI with subtle glass and gradients */
46
+ :root {
47
+ --radius-xl: 16px;
48
+ --radius-lg: 14px;
49
+ --radius-md: 12px;
50
+ --shadow-lg: 0 18px 35px rgba(2, 6, 23, 0.10);
51
+ --shadow-md: 0 10px 22px rgba(2, 6, 23, 0.08);
52
+ --border: 1px solid rgba(2, 6, 23, 0.08);
53
+ --primary: #6366f1; /* indigo-500 */
54
+ --primary-600: #4f46e5;
55
+ --primary-700: #4338ca;
56
+ --slate-900: #0f172a;
57
+ --slate-800: #1e293b;
58
+ --slate-700: #334155;
59
+ --slate-600: #475569;
60
+ --slate-500: #64748b;
61
+ --slate-200: #e2e8f0;
62
+ --slate-100: #f1f5f9;
63
+ --bg: radial-gradient(1200px 800px at 0% 0%, #f6f8ff 0%, #ffffff 40%);
64
+ }
65
+
66
+ .dark:root {
67
+ --border: 1px solid rgba(255, 255, 255, 0.08);
68
+ --bg: radial-gradient(1200px 800px at 0% 0%, #0b1220 0%, #0a0f1c 40%);
69
+ }
70
+
71
+ .gradio-container, body {
72
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
73
+ background: var(--bg);
74
+ color: var(--slate-900);
75
+ }
76
+ .dark .gradio-container, .dark body { color: #e5e7eb; }
77
+
78
+ /* Header */
79
+ #app-header {
80
+ background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 50%, #7c3aed 100%);
81
+ color: white;
82
+ padding: 28px 28px;
83
+ border-radius: var(--radius-xl);
84
+ box-shadow: var(--shadow-lg);
85
+ margin-bottom: 18px;
86
+ }
87
+ #app-header h1 {
88
+ font-size: 34px;
89
+ line-height: 1.1;
90
+ margin: 0 0 8px 0;
91
+ letter-spacing: -0.02em;
92
+ }
93
+ #app-header p {
94
+ margin: 0;
95
+ opacity: 0.95;
96
+ }
97
+
98
+ /* Panels */
99
+ .panel {
100
+ backdrop-filter: saturate(160%) blur(8px);
101
+ background: rgba(255, 255, 255, 0.75);
102
+ border: var(--border);
103
+ border-radius: var(--radius-xl);
104
+ padding: 18px;
105
+ box-shadow: var(--shadow-md);
106
+ }
107
+ .dark .panel {
108
+ background: rgba(2, 6, 23, 0.55);
109
+ }
110
+
111
+ /* Segmented control (radio) */
112
+ .segmented .wrap {
113
+ display: grid !important;
114
+ grid-auto-flow: column;
115
+ grid-auto-columns: 1fr;
116
+ gap: 8px;
117
+ background: var(--slate-100);
118
+ border: var(--border);
119
+ border-radius: 999px;
120
+ padding: 6px;
121
+ }
122
+ .dark .segmented .wrap { background: rgba(255, 255, 255, 0.06); }
123
+ .segmented input[type="radio"] { display: none; }
124
+ .segmented label {
125
+ border-radius: 999px !important;
126
+ padding: 10px 14px !important;
127
+ text-align: center;
128
+ border: none !important;
129
+ transition: all .18s ease;
130
+ color: var(--slate-700);
131
+ background: transparent;
132
+ }
133
+ .dark .segmented label { color: #cbd5e1; }
134
+ .segmented input[type="radio"]:checked + label {
135
+ background: white !important;
136
+ color: var(--slate-900) !important;
137
+ box-shadow: 0 8px 18px rgba(2, 6, 23, 0.08);
138
+ }
139
+ .dark .segmented input[type="radio"]:checked + label {
140
+ background: var(--slate-800) !important;
141
+ color: #e5e7eb !important;
142
+ }
143
+
144
+ /* Form controls polish */
145
+ .panel .gr-form .gr-block, .panel .gr-form { gap: 10px; }
146
+ .panel .gr-textbox textarea, .panel .gr-textbox input,
147
+ .panel .gr-dropdown input, .panel .gr-dropdown .wrap,
148
+ .panel .gr-slider input {
149
+ border-radius: 12px !important;
150
+ }
151
+
152
+ /* Submit button */
153
+ .submit-button .gr-button {
154
+ background: linear-gradient(135deg, var(--primary), var(--primary-600));
155
+ border: none;
156
+ color: white;
157
+ border-radius: 12px;
158
+ box-shadow: 0 10px 24px rgba(79, 70, 229, 0.25);
159
+ padding: 12px 16px;
160
+ }
161
+ .submit-button .gr-button:hover {
162
+ transform: translateY(-1px);
163
+ box-shadow: 0 14px 28px rgba(79, 70, 229, 0.32);
164
+ }
165
+
166
+ /* Output area */
167
+ .output-panel {
168
+ padding: 0;
169
+ }
170
+ .model-info {
171
+ margin-top: 8px;
172
+ }
173
+ .model-info .content {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 10px;
177
+ font-weight: 600;
178
+ background: linear-gradient(135deg, #dcfce7, #dbeafe);
179
+ color: #065f46;
180
+ padding: 8px 12px;
181
+ border-radius: 999px;
182
+ border: var(--border);
183
+ }
184
+ .dark .model-info .content {
185
+ background: linear-gradient(135deg, rgba(22, 101, 52, 0.35), rgba(30, 58, 138, 0.35));
186
+ color: #d1fae5;
187
+ }
188
+
189
+ /* Results grid and cards */
190
+ .results-grid {
191
+ display: grid;
192
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
193
+ gap: 14px;
194
+ padding: 14px;
195
+ }
196
+ .article-card {
197
+ border: var(--border);
198
+ border-radius: var(--radius-lg);
199
+ background: rgba(255, 255, 255, 0.9);
200
+ padding: 16px;
201
+ box-shadow: var(--shadow-md);
202
+ }
203
+ .dark .article-card {
204
+ background: rgba(2, 6, 23, 0.6);
205
+ }
206
+ .article-card__title {
207
+ font-size: 18px;
208
+ margin: 0 0 8px 0;
209
+ color: var(--slate-900);
210
+ }
211
+ .dark .article-card__title { color: #e5e7eb; }
212
+ .article-card__meta {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: 8px;
216
+ margin-bottom: 8px;
217
+ }
218
+ .chip {
219
+ font-size: 12px;
220
+ padding: 6px 10px;
221
+ border-radius: 999px;
222
+ background: var(--slate-100);
223
+ border: var(--border);
224
+ color: var(--slate-700);
225
+ }
226
+ .dark .chip { background: rgba(255, 255, 255, 0.06); color: #cbd5e1; }
227
+ .article-card__authors {
228
+ color: var(--slate-600);
229
+ font-size: 14px;
230
+ margin-bottom: 10px;
231
+ }
232
+ .dark .article-card__authors { color: #94a3b8; }
233
+ .article-card__link {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ gap: 8px;
237
+ color: var(--primary-600);
238
+ text-decoration: none;
239
+ font-weight: 600;
240
+ }
241
+ .article-card__link:hover { color: var(--primary-700); }
242
+
243
+ /* AI answer card */
244
+ .answer-card {
245
+ margin: 14px;
246
+ border: var(--border);
247
+ border-radius: var(--radius-xl);
248
+ padding: 18px;
249
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.06), rgba(124, 58, 237, 0.06));
250
+ box-shadow: var(--shadow-md);
251
+ }
252
+ .dark .answer-card {
253
+ background: linear-gradient(180deg, rgba(79, 70, 229, 0.18), rgba(124, 58, 237, 0.18));
254
+ }
255
+ .answer-card .markdown-body table {
256
+ width: 100%;
257
+ border-collapse: collapse;
258
+ }
259
+ .answer-card .markdown-body th, .answer-card .markdown-body td {
260
+ border: 1px solid rgba(0,0,0,0.05);
261
+ padding: 6px 10px;
262
+ }
263
+ """
264
+
265
+
266
  # -----------------------
267
  # API helpers
268
  # -----------------------
 
381
  if not results:
382
  return "No results found."
383
 
384
+ html_output = "<div class='results-grid'>"
385
  for item in results:
386
+ title = item.get("title", "No title")
387
+ feed_n = item.get("feed_name", "N/A")
388
+ feed_a = item.get("feed_author", "N/A")
389
+ authors = ", ".join(item.get("article_author") or ["N/A"])
390
+ url = item.get("url", "#")
391
  html_output += (
392
+ "<div class='article-card'>"
393
+ f" <h3 class='article-card__title'>{title}</h3>"
394
+ f" <div class='article-card__meta'>"
395
+ f" <span class='chip'>Newsletter: {feed_n}</span>"
396
+ f" <span class='chip'>Author: {feed_a}</span>"
397
+ f" </div>"
398
+ f" <div class='article-card__authors'><b>Article Authors:</b> {authors}</div>"
399
+ f" <a class='article-card__link' href='{url}' target='_blank' rel='noopener noreferrer'>"
400
+ f" Open Article β†’"
401
+ f" </a>"
402
+ "</div>"
 
 
 
 
 
403
  )
404
+ html_output += "</div>"
405
  return html_output
406
 
407
  except Exception as e:
 
450
 
451
  try:
452
  answer_html = ""
453
+ model_info = f"<div class='content'>Provider: {provider}</div>"
454
 
455
  for _, (event_type, content) in enumerate(call_ai(payload, streaming=True)):
456
  if event_type == "text":
457
  # Convert markdown to HTML
458
  html_content = markdown.markdown(content, extensions=["tables"])
459
+ answer_html = f"<div class='answer-card'><div class='markdown-body'>{html_content}</div></div>"
 
 
 
 
 
 
460
  yield answer_html, model_info
461
 
462
  elif event_type == "model":
463
+ model_info = f"<div class='content'>Provider: {provider} | Model: {content}</div>"
464
  yield answer_html, model_info
465
 
466
  elif event_type == "truncated":
467
+ answer_html += f"<div class='answer-card'><div style='color:#ff8800; font-weight:700;'>⚠️ {content}</div></div>"
 
 
468
  yield answer_html, model_info
469
 
470
  elif event_type == "error":
471
+ error_html = f"<div class='answer-card'><div style='color:#ef4444; font-weight:700;'>❌ {content}</div></div>"
 
 
472
  yield error_html, model_info
473
  break
474
 
475
  except Exception as e:
476
+ error_html = "<div class='answer-card'><div style='color:#ef4444;'>Error: {}</div></div>".format(str(e))
477
  yield error_html, model_info
478
 
479
 
 
511
 
512
  try:
513
  answer_html = ""
514
+ model_info = f"<div class='content'>Provider: {provider}</div>"
515
 
516
  for event_type, content in call_ai(payload, streaming=False):
517
  if event_type == "text":
518
  html_content = markdown.markdown(content, extensions=["tables"])
519
+ answer_html = f"<div class='answer-card'><div class='markdown-body'>{html_content}</div></div>"
 
 
 
 
 
520
  elif event_type == "model":
521
+ model_info = f"<div class='content'>Provider: {provider} | Model: {content}</div>"
522
  elif event_type == "truncated":
523
+ answer_html += f"<div class='answer-card'><div style='color:#ff8800; font-weight:700;'>⚠️ {content}</div></div>"
 
 
524
  elif event_type == "error":
525
  return (
526
+ f"<div class='answer-card'><div style='color:#ef4444; font-weight:700;'>❌ {content}</div></div>",
527
  model_info,
528
  )
529
 
 
531
 
532
  except Exception as e:
533
  return (
534
+ f"<div class='answer-card'><div style='color:#ef4444;'>Error: {str(e)}</div></div>",
535
+ f"<div class='content'>Provider: {provider}</div>",
536
  )
537
 
538
 
 
551
  # -----------------------
552
  # Gradio UI
553
  # -----------------------
554
+ with gr.Blocks(title="Substack Articles LLM Engine", theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo:
555
  # Header
556
  gr.HTML(
557
+ "<div id='app-header'>"
558
+ " <h1>πŸ“° Substack Articles LLM Engine</h1>"
559
+ " <p>Search Substack content or ask an AI across your feeds β€” fast and delightful.</p>"
560
+ "</div>"
 
 
561
  )
562
 
563
  with gr.Row():
564
+ with gr.Column(scale=5):
565
+ with gr.Group(elem_classes="panel"):
566
+ gr.Markdown("#### Mode")
567
+ search_type = gr.Radio(
568
+ choices=["Search Articles", "Ask the AI"],
569
+ value="Search Articles",
570
+ label="",
571
+ info="Choose between searching for articles or asking AI questions",
572
+ elem_classes="segmented",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  )
 
 
 
 
 
 
 
 
 
574
 
575
+ with gr.Accordion("Filters", open=True):
576
+ query_text = gr.Textbox(
577
+ label="Query",
578
+ placeholder="Type your query here...",
579
+ lines=4,
580
+ )
581
+ feed_author = gr.Dropdown(
582
+ choices=[""] + feed_authors, label="Author (optional)", value=""
583
+ )
584
+ feed_name = gr.Dropdown(
585
+ choices=[""] + feed_names, label="Newsletter (optional)", value=""
586
+ )
587
+ title_keywords = gr.Textbox(
588
+ label="Title Keywords (optional)",
589
+ placeholder="Filter by words in the title",
590
+ visible=True,
591
+ )
592
+ limit = gr.Slider(
593
+ minimum=1, maximum=20, step=1, label="Number of results", value=5, visible=True
594
+ )
595
+
596
+ with gr.Accordion("βš™οΈ LLM Settings", open=True):
597
+ with gr.Group(visible=False) as llm_options:
598
+ provider = gr.Dropdown(
599
+ choices=["OpenRouter", "HuggingFace", "OpenAI"],
600
+ label="Select LLM Provider",
601
+ value="OpenRouter",
602
+ )
603
+ model = gr.Dropdown(
604
+ choices=get_models_for_provider("OpenRouter"),
605
+ label="Select Model",
606
+ value="Automatic Model Selection (Model Routing)",
607
+ )
608
+ streaming_mode = gr.Radio(
609
+ choices=["Streaming", "Non-Streaming"],
610
+ value="Streaming",
611
+ label="Answer Mode",
612
+ info="Streaming shows results as they're generated",
613
+ )
614
+
615
+ submit_btn = gr.Button("πŸ”Ž Search / Ask AI", variant="primary", size="lg", elem_classes="submit-button")
616
+
617
+ with gr.Column(scale=7):
618
+ with gr.Group(elem_classes="panel output-panel"):
619
+ output_html = gr.HTML(label="Results")
620
+ model_info = gr.HTML(visible=False, elem_classes="model-info")
621
 
622
  # Event handlers
623
  def toggle_visibility(search_type):