Pushkar02-n commited on
Commit
dbb9b6d
·
1 Parent(s): 21dca42

Complete MVP with awesome UI

Browse files
Files changed (3) hide show
  1. src/api/main.py +3 -0
  2. src/llm/tool_use_schema.json +35 -0
  3. ui/gradio_app.py +352 -458
src/api/main.py CHANGED
@@ -75,6 +75,9 @@ async def get_recommendations(request: RecommendationRequest):
75
  filters=filters if filters else None
76
  )
77
  end_time = time.time()
 
 
 
78
  return RecommendationResponse(
79
  query=result["query"],
80
  recommendations=result["recommendations"],
 
75
  filters=filters if filters else None
76
  )
77
  end_time = time.time()
78
+
79
+ print(f"Retrieved anime : \n{result["retrieved_count"]}")
80
+ print(f"Result Recommendations: \n{result["recommendations"][:20]}")
81
  return RecommendationResponse(
82
  query=result["query"],
83
  recommendations=result["recommendations"],
src/llm/tool_use_schema.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tools": [
3
+ {
4
+ "type": "function",
5
+ "function": {
6
+ "name": "get_weather",
7
+ "description": "Get current weather for a location",
8
+ "parameters": {
9
+ "type": "object",
10
+ "properties": {
11
+ "location": {
12
+ "type": "string",
13
+ "description": "City and state, e.g. San Francisco, CA"
14
+ },
15
+ "unit": {
16
+ "type": "string",
17
+ "enum": ["celsius", "fahrenheit"]
18
+ }
19
+ },
20
+ "required": ["location"]
21
+ }
22
+ }
23
+ }
24
+ ],
25
+ "messages": [
26
+ {
27
+ "role": "system",
28
+ "content": "You are a weather assistant. Respond to the user question and use tools if needed to answer the query."
29
+ },
30
+ {
31
+ "role": "user",
32
+ "content": "What's the weather in San Francisco?"
33
+ }
34
+ ],
35
+ }
ui/gradio_app.py CHANGED
@@ -1,118 +1,5 @@
1
- # import gradio as gr
2
- # import requests
3
- # import os
4
-
5
- # API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
6
-
7
-
8
- # def get_recommendations(query, min_score, genre_filter, n_results):
9
- # """Call the fastapi backend"""
10
- # try:
11
- # payload = {
12
- # "query": query,
13
- # "n_results": n_results
14
- # }
15
-
16
- # if min_score > 0:
17
- # payload["min_score"] = min_score
18
-
19
- # if genre_filter and genre_filter != None:
20
- # payload["genre_filter"] = genre_filter
21
-
22
- # response = requests.post(
23
- # f"{API_URL}/recommend",
24
- # json=payload,
25
- # timeout=30
26
- # )
27
- # response.raise_for_status()
28
-
29
- # result = response.json()
30
- # print(result["metadata"])
31
- # return result["recommendations"]
32
- # except requests.exceptions.RequestException as e:
33
- # return f"Error connecting to the API. Make sure FastAPI server is running. \nDetails: {str(e)}"
34
- # except Exception as e:
35
- # return f"Error: {str(e)}"
36
-
37
-
38
- # with gr.Blocks(title="Anime Recommender") as demo:
39
- # gr.Markdown("""
40
- # # Anime Recommendation System
41
-
42
- # Powered by RAG(Retrieval-Augmented Generation)
43
-
44
- # Ask for anime recommendations and get AI-powered suggestions!
45
- # """)
46
- # with gr.Row():
47
- # with gr.Column(scale=2):
48
- # query_input = gr.Textbox(
49
- # label="What are you looking for?",
50
- # placeholder="e.g., 'Anime similar to Death Note but lighter' or 'Romantic comedy set in high school'",
51
- # lines=3
52
- # )
53
-
54
- # with gr.Row():
55
- # min_score_slider = gr.Slider(
56
- # minimum=0,
57
- # maximum=10,
58
- # value=0,
59
- # step=0.5,
60
- # label="Minimum Rating this animes should have (0 = no filter)"
61
- # )
62
-
63
- # genre_dropdown = gr.Dropdown(
64
- # choices=["None", "Action", "Comedy", "Drama",
65
- # "Romance", "Sci-Fi", "Fantasy", "Thriller"],
66
- # value="None",
67
- # label="Genre Filter (optional)"
68
- # )
69
-
70
- # n_results_dropdown = gr.Slider(
71
- # minimum=1,
72
- # maximum=8,
73
- # value=3,
74
- # step=1,
75
- # label="Number of recommendation you want to get"
76
- # )
77
-
78
- # submit_btn = gr.Button("Get Recommendations",
79
- # variant="primary", size="lg")
80
-
81
- # with gr.Column(scale=3):
82
- # output = gr.Markdown(label="Recommendations")
83
-
84
- # # Examples
85
- # gr.Examples(
86
- # examples=[
87
- # ["Anime similar to Death Note but lighter", 0, "None", 3],
88
- # ["Romantic comedy set in high school", 7.5, "Comedy", 4],
89
- # ["Dark psychological thriller", 8.0, "None", 2],
90
- # ["Action anime with epic fights", 7.0, "Action", 1],
91
- # ],
92
- # inputs=[query_input, min_score_slider,
93
- # genre_dropdown, n_results_dropdown],
94
- # )
95
-
96
- # # Connect button to function
97
- # submit_btn.click(
98
- # fn=get_recommendations,
99
- # inputs=[query_input, min_score_slider,
100
- # genre_dropdown, n_results_dropdown],
101
- # outputs=output
102
- # )
103
-
104
- # # Launch
105
- # if __name__ == "__main__":
106
- # print("Starting Gradio UI...")
107
- # print("Make sure FastAPI server is running at http://localhost:8000")
108
- # demo.launch(
109
- # server_name="0.0.0.0",
110
- # server_port=7860,
111
- # share=True # Set to True to get public URL
112
- # )
113
-
114
-
115
  import gradio as gr
 
116
  import requests
117
  import os
118
 
@@ -120,404 +7,411 @@ API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
120
 
121
 
122
  def get_recommendations(query, min_score, genre_filter, n_results):
 
 
 
 
 
 
123
  try:
124
- payload = {"query": query, "n_results": int(n_results)}
 
 
 
125
  if min_score > 0:
126
- payload["min_score"] = min_score
127
- if genre_filter and genre_filter != "None":
128
  payload["genre_filter"] = genre_filter
129
 
130
  response = requests.post(
131
  f"{API_URL}/recommend", json=payload, timeout=30)
132
  response.raise_for_status()
133
  result = response.json()
134
- print(result["metadata"])
135
- return result["recommendations"]
136
- except requests.exceptions.RequestException as e:
137
- return f"**Connection Error** — Make sure FastAPI is running.\n\n`{str(e)}`"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  except Exception as e:
139
- return f"**Error:** {str(e)}"
140
 
141
 
142
- css = """
143
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Serif+Display:ital@0;1&display=swap');
144
 
145
  :root {
146
- --bg: #1c1714;
147
- --surface: #241e1a;
148
- --card: #2d2520;
149
- --border: #3d3028;
150
- --accent: #d4845a;
151
- --gold: #c9a97a;
152
- --text: #f5ede3;
153
- --sub: #d0bfae;
154
- --muted: #9a8878;
155
- --faint: #6a5a4a;
156
- --radius: 16px;
157
- --shadow: 0 4px 28px rgba(0,0,0,0.4);
158
  }
159
 
160
- /* ── GLOBAL ── */
161
  body, .gradio-container {
162
- background: var(--bg) !important;
163
- font-family: 'DM Sans', sans-serif !important;
164
- color: var(--text) !important;
165
  }
166
- footer { display: none !important; }
167
  .gradio-container {
168
- max-width: 980px !important;
169
  margin: 0 auto !important;
170
- padding: 0 36px !important;
171
  }
172
 
173
- /* ── HEADER ── */
174
- .app-header { text-align: center; padding: 56px 0 40px; }
175
- .app-header h1 {
176
- font-family: 'DM Serif Display', serif !important;
177
- font-size: 46px !important;
178
- font-weight: 400 !important;
179
- color: var(--text) !important;
180
- margin: 0 0 8px !important;
181
- letter-spacing: -0.5px;
182
- }
183
- .app-header h1 em { font-style: italic; color: var(--accent); }
184
- .app-header p { font-size: 15px; color: var(--sub); margin: 0; }
185
- .header-line { width: 44px; height: 2px; background: var(--accent); border-radius: 2px; margin: 16px auto 0; opacity: 0.75; }
186
-
187
- /* ── STEP CARDS ── */
188
- .step-card {
189
- background: var(--card);
190
- border-radius: var(--radius);
191
- padding: 28px 32px 26px;
192
- margin-bottom: 14px;
193
- box-shadow: var(--shadow);
194
- border: 1px solid var(--border);
195
  }
196
- .step-label {
197
- font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
198
- text-transform: uppercase; color: var(--muted);
199
- margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
 
 
 
 
200
  }
201
- .step-label .num {
202
- width: 21px; height: 21px; background: var(--accent); color: #1c1714;
203
- border-radius: 50%; display: inline-flex; align-items: center;
204
- justify-content: center; font-size: 10px; font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
 
207
- /* ── INPUTS ── */
208
- textarea, input[type="text"] {
209
- background: var(--surface) !important;
210
- border: 1.5px solid var(--border) !important;
211
- border-radius: 10px !important;
212
- color: var(--text) !important;
213
- font-family: 'DM Sans', sans-serif !important;
214
- font-size: 15px !important;
215
- transition: border-color 0.2s, box-shadow 0.2s !important;
216
  }
217
- textarea::placeholder, input::placeholder { color: var(--faint) !important; }
218
- textarea:focus, input:focus {
219
- border-color: var(--accent) !important;
220
- box-shadow: 0 0 0 3px rgba(212,132,90,0.15) !important;
221
- outline: none !important;
 
 
222
  }
223
- label > span {
224
- font-size: 13px !important; font-weight: 500 !important;
225
- color: var(--sub) !important; letter-spacing: 0 !important;
226
- text-transform: none !important;
 
 
 
 
 
 
227
  }
228
 
229
- /* ── SLIDER ── */
230
- input[type="range"] { accent-color: var(--accent) !important; }
 
 
231
 
232
- /* ── SUBMIT BUTTON ── */
233
- #submit-btn button {
234
- width: 100% !important;
235
  background: var(--accent) !important;
236
- color: #1c1714 !important;
237
  border: none !important;
238
- border-radius: 12px !important;
239
- font-family: 'DM Sans', sans-serif !important;
240
- font-size: 15px !important;
241
- font-weight: 700 !important;
242
- padding: 17px !important;
243
- cursor: pointer !important;
244
- letter-spacing: 0.03em !important;
245
- transition: background 0.2s, transform 0.15s, box-shadow 0.2s !important;
246
- box-shadow: 0 4px 20px rgba(212,132,90,0.30) !important;
247
- }
248
- #submit-btn button:hover {
249
- background: var(--gold) !important;
250
- transform: translateY(-2px) !important;
251
- box-shadow: 0 8px 28px rgba(212,132,90,0.42) !important;
252
- }
253
- #submit-btn button:active { transform: translateY(0) !important; }
254
-
255
- /* ── LOADER ── */
256
- #ao-loader { display: none; text-align: center; padding: 44px 0 36px; }
257
- .ao-loader-phrase {
258
- font-family: 'DM Serif Display', serif;
259
- font-size: 20px; font-style: italic;
260
- color: var(--text); margin-bottom: 24px;
261
- min-height: 30px; transition: opacity 0.35s;
262
- }
263
- .ao-dots { display: flex; justify-content: center; gap: 10px; }
264
- .ao-dots span {
265
- width: 9px; height: 9px; background: var(--accent);
266
- border-radius: 50%; animation: aobounce 1.2s infinite ease-in-out;
267
- }
268
- .ao-dots span:nth-child(2) { animation-delay: 0.18s; background: var(--gold); }
269
- .ao-dots span:nth-child(3) { animation-delay: 0.36s; background: var(--faint); }
270
- @keyframes aobounce {
271
- 0%, 80%, 100% { transform: translateY(0) scale(0.75); opacity: 0.35; }
272
- 40% { transform: translateY(-11px) scale(1.15); opacity: 1; }
273
  }
274
 
275
- /* ── OUTPUT BOX ── */
276
- /* Target the Gradio Markdown block container via elem_id */
277
- #ao-output-wrap,
278
- #ao-output-wrap > .wrap,
279
- div#ao-output-wrap {
280
- background: var(--card) !important;
281
- border-radius: var(--radius) !important;
282
- border: 1px solid var(--border) !important;
283
- box-shadow: var(--shadow) !important;
284
- padding: 32px 36px !important;
285
- margin-top: 4px !important;
286
- transition: opacity 0.45s !important;
287
  }
288
- /* The actual prose inside Markdown */
289
- #ao-output-wrap .prose,
290
- #ao-output-wrap .md,
291
- #ao-output-wrap > div {
292
- color: var(--sub) !important;
293
  }
294
- #ao-output-wrap p,
295
- #ao-output-wrap .prose p {
296
- font-size: 14.5px !important;
297
- line-height: 1.78 !important;
298
- color: var(--sub) !important;
299
- margin: 0 0 10px !important;
300
  }
301
- #ao-output-wrap h1, #ao-output-wrap h2,
302
- #ao-output-wrap .prose h1, #ao-output-wrap .prose h2 {
303
- font-family: 'DM Serif Display', serif !important;
304
- font-size: 22px !important;
305
- font-weight: 400 !important;
306
- color: var(--text) !important;
307
- padding-bottom: 10px !important;
308
- border-bottom: 1px solid var(--border) !important;
309
- margin: 28px 0 12px !important;
310
  }
311
- #ao-output-wrap h2:first-child,
312
- #ao-output-wrap .prose h2:first-child { margin-top: 0 !important; }
313
- #ao-output-wrap h3,
314
- #ao-output-wrap .prose h3 {
315
- font-size: 15px !important;
316
- color: var(--accent) !important;
317
- font-weight: 600 !important;
318
- margin: 0 0 5px !important;
319
  }
320
- #ao-output-wrap strong,
321
- #ao-output-wrap .prose strong { color: var(--text) !important; font-weight: 600 !important; }
322
- #ao-output-wrap em,
323
- #ao-output-wrap .prose em { color: var(--gold) !important; }
324
- #ao-output-wrap code,
325
- #ao-output-wrap .prose code {
326
- background: var(--surface) !important;
327
- color: var(--accent) !important;
328
- padding: 2px 7px !important;
329
- border-radius: 5px !important;
330
- font-size: 13px !important;
331
- border: 1px solid var(--border) !important;
332
  }
333
- #ao-output-wrap ul,
334
- #ao-output-wrap .prose ul { padding-left: 18px !important; }
335
- #ao-output-wrap li,
336
- #ao-output-wrap .prose li {
337
- font-size: 14.5px !important;
338
- line-height: 1.72 !important;
339
- color: var(--sub) !important;
340
- margin-bottom: 4px !important;
 
 
 
 
341
  }
342
 
343
- /* ── EXAMPLES ── */
344
- .examples-label {
345
- font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
346
- text-transform: uppercase; color: var(--faint); margin: 26px 0 12px;
 
 
 
 
 
 
347
  }
348
- .gr-samples-table td, .examples-holder td {
349
- background: var(--card) !important;
350
- border: 1px solid var(--border) !important;
351
- border-radius: 8px !important;
352
- color: var(--sub) !important;
353
- font-size: 13px !important; padding: 10px 15px !important;
354
- transition: all 0.15s !important; cursor: pointer !important;
355
- font-family: 'DM Sans', sans-serif !important;
 
 
 
 
 
 
 
 
356
  }
357
- .gr-samples-table tr:hover td, .examples-holder tr:hover td {
358
- background: var(--accent) !important;
359
- color: #1c1714 !important;
360
- border-color: var(--accent) !important;
361
  }
362
- .gr-samples-header, .gr-samples-header th { display: none !important; }
363
- label[data-testid="block-label"] { display: none !important; }
364
- .examples-holder > label { display: none !important; }
365
-
366
- /* ── SCROLLBAR ── */
367
- ::-webkit-scrollbar { width: 5px; }
368
- ::-webkit-scrollbar-track { background: var(--bg); }
369
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
370
- ::-webkit-scrollbar-thumb:hover { background: var(--faint); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  """
372
 
373
 
374
- with gr.Blocks(
375
- title="Anime Oracle",
376
- css=css,
377
- theme=gr.themes.Base(
378
- primary_hue="orange",
379
- neutral_hue="slate",
380
- font=gr.themes.GoogleFont("DM Sans"),
381
- ).set(
382
- body_background_fill="#1c1714",
383
- body_text_color="#f5ede3",
384
- block_background_fill="#2d2520",
385
- block_border_color="#3d3028",
386
- input_background_fill="#241e1a",
387
- input_border_color="#3d3028",
388
- button_primary_background_fill="#d4845a",
389
- button_primary_text_color="#1c1714",
390
- )
391
- ) as demo:
392
-
393
- gr.HTML("""
394
- <div class="app-header">
395
- <h1>Find your next <em>obsession.</em></h1>
396
- <p>Describe your mood we'll find the perfect anime.</p>
397
- <div class="header-line"></div>
398
- </div>
399
- """)
400
-
401
- # Step 1
402
- gr.HTML('<div class="step-card"><div class="step-label"><span class="num">1</span> What are you in the mood for?</div>')
403
- query_input = gr.Textbox(
404
- label="", show_label=False, lines=3,
405
- placeholder='"Something like Attack on Titan but more emotional" or "Cozy slice-of-life with great friendships"'
406
- )
407
- gr.HTML("</div>")
408
-
409
- # Step 2
410
- gr.HTML('<div class="step-card"><div class="step-label"><span class="num">2</span> Any preferences? <span style="font-weight:300;color:#6a5a4a;margin-left:4px;font-size:10px;">optional</span></div>')
411
- with gr.Row():
412
- genre_dropdown = gr.Dropdown(
413
- choices=["None", "Action", "Comedy", "Drama",
414
- "Romance", "Sci-Fi", "Fantasy", "Thriller"],
415
- value="None", label="Genre", scale=1
 
 
 
 
 
 
 
 
 
 
 
416
  )
417
- min_score_slider = gr.Slider(
418
- minimum=0, maximum=10, value=0, step=0.5, label="Minimum rating (0 = any)", scale=2)
419
- gr.HTML("</div>")
420
-
421
- # Step 3
422
- gr.HTML('<div class="step-card"><div class="step-label"><span class="num">3</span> How many results?</div>')
423
- n_results_slider = gr.Slider(
424
- minimum=1, maximum=8, value=3, step=1, label="", show_label=False)
425
- gr.HTML("</div>")
426
-
427
- submit_btn = gr.Button(
428
- "Find my anime →", variant="primary", elem_id="submit-btn")
429
-
430
- # Loading buffer
431
- gr.HTML("""
432
- <div id="ao-loader">
433
- <div class="ao-loader-phrase">Scanning the anime universe…</div>
434
- <div class="ao-dots"><span></span><span></span><span></span></div>
435
- </div>
436
- """)
437
-
438
- # Output — elem_id so CSS can target it reliably
439
- output = gr.Markdown(
440
- value="*Your recommendations will appear here ✦*",
441
- elem_id="ao-output-wrap"
442
- )
443
 
444
- # Examples
445
- gr.HTML('<div class="examples-label">Try an example</div>')
446
- gr.Examples(
447
- examples=[
448
- ["Something like Death Note but with a lighter tone", 0, "None", 3],
449
- ["Romantic comedy with lovable characters", 7.5, "Comedy", 4],
450
- ["Dark psychological thriller with twists", 8.0, "Thriller", 2],
451
- ["Epic action with incredible fights", 7.0, "Action", 5],
452
- ["Wholesome feel-good slice of life", 6.0, "Drama", 3],
453
- ],
454
- inputs=[query_input, min_score_slider,
455
- genre_dropdown, n_results_slider],
456
- label=""
457
- )
458
 
459
- # Loader JS
460
- gr.HTML("""
461
- <script>
462
- (function() {
463
- const PHRASES = [
464
- "Scanning the anime universe\u2026",
465
- "Consulting the oracle\u2026",
466
- "Traversing story arcs\u2026",
467
- "Matching your vibe\u2026",
468
- "Sifting through thousands of titles\u2026",
469
- "Almost there\u2026"
470
- ];
471
- let phraseTimer = null, phraseIdx = 0;
472
-
473
- function startLoader() {
474
- const loader = document.getElementById('ao-loader');
475
- const wrap = document.getElementById('ao-output-wrap');
476
- if (loader) loader.style.display = 'block';
477
- if (wrap) wrap.style.opacity = '0.15';
478
- phraseIdx = 0;
479
- const el = document.querySelector('.ao-loader-phrase');
480
- if (el) el.textContent = PHRASES[0];
481
- clearInterval(phraseTimer);
482
- phraseTimer = setInterval(() => {
483
- phraseIdx = (phraseIdx + 1) % PHRASES.length;
484
- if (el) {
485
- el.style.opacity = '0';
486
- setTimeout(() => { el.textContent = PHRASES[phraseIdx]; el.style.opacity = '1'; }, 300);
487
- }
488
- }, 1800);
489
- }
490
 
491
- function stopLoader() {
492
- clearInterval(phraseTimer);
493
- const loader = document.getElementById('ao-loader');
494
- const wrap = document.getElementById('ao-output-wrap');
495
- if (loader) loader.style.display = 'none';
496
- if (wrap) { wrap.style.transition = 'opacity 0.5s ease'; wrap.style.opacity = '1'; }
497
- }
498
 
499
- document.addEventListener('click', function(e) {
500
- if (e.target && e.target.closest('#submit-btn')) {
501
- startLoader();
502
- setTimeout(stopLoader, 25000);
503
- }
504
- });
 
505
 
506
- function watchOutput() {
507
- const target = document.getElementById('ao-output-wrap');
508
- if (!target) { setTimeout(watchOutput, 500); return; }
509
- new MutationObserver(stopLoader).observe(target, { childList: true, subtree: true, characterData: true });
510
- }
511
- watchOutput();
512
- })();
513
- </script>
514
- """)
515
 
516
- submit_btn.click(fn=get_recommendations, inputs=[
517
- query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
518
- query_input.submit(fn=get_recommendations, inputs=[
519
- query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
520
 
521
  if __name__ == "__main__":
522
- print("Starting Anime Oracle...")
523
- demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ from gradio import themes
3
  import requests
4
  import os
5
 
 
7
 
8
 
9
  def get_recommendations(query, min_score, genre_filter, n_results):
10
+ if not query or not query.strip():
11
+ return (
12
+ "<div class='status-badge warning'>Waiting for prompt</div>",
13
+ "Tell me what you're in the mood for to get started."
14
+ )
15
+
16
  try:
17
+ payload = {
18
+ "query": query.strip(),
19
+ "n_results": int(n_results)
20
+ }
21
  if min_score > 0:
22
+ payload["min_score"] = float(min_score)
23
+ if genre_filter and genre_filter != "Any":
24
  payload["genre_filter"] = genre_filter
25
 
26
  response = requests.post(
27
  f"{API_URL}/recommend", json=payload, timeout=30)
28
  response.raise_for_status()
29
  result = response.json()
30
+
31
+ recommendations_text = result.get("recommendations", "")
32
+ retrieved_count = result.get("retrieved_count", 0)
33
+ time_taken = result.get("metadata", {}).get(
34
+ "Time taken for LLM + vector search", "unknown")
35
+
36
+ if not recommendations_text or recommendations_text.strip() == "":
37
+ return (
38
+ "<div class='status-badge error'>No Matches</div>",
39
+ "Couldn't find anything matching that exact vibe. Try tweaking your search."
40
+ )
41
+
42
+ header_html = f"""
43
+ <div class='results-dashboard'>
44
+ <div class='metric'>
45
+ <span class='label'>TITLES SCANNED</span>
46
+ <span class='value'>{retrieved_count}</span>
47
+ </div>
48
+ <div class='metric'>
49
+ <span class='label'>SEARCH TIME</span>
50
+ <span class='value'>{time_taken}</span>
51
+ </div>
52
+ <div class='status-badge success'>Search Complete</div>
53
+ </div>
54
+ """
55
+ return header_html, recommendations_text
56
+
57
+ except requests.exceptions.ConnectionError:
58
+ return (
59
+ "<div class='status-badge error'>Offline</div>",
60
+ "**Error:** Cannot connect to backend. Make sure the FastAPI server is running."
61
+ )
62
  except Exception as e:
63
+ return ("<div class='status-badge error'>Error</div>", f"**Something went wrong:** {str(e)}")
64
 
65
 
66
+ CSS = """
67
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Inter:wght@400;500&display=swap');
68
 
69
  :root {
70
+ --bg-base: #0b0f19;
71
+ --bg-panel: #111827;
72
+ --accent: #6366f1;
73
+ --accent-hover: #4f46e5;
74
+ --text-main: #f3f4f6;
75
+ --text-muted: #9ca3af;
76
+ --border-dim: #1f2937;
 
 
 
 
 
77
  }
78
 
 
79
  body, .gradio-container {
80
+ background-color: var(--bg-base) !important;
81
+ font-family: 'Inter', sans-serif !important;
82
+ color: var(--text-main) !important;
83
  }
84
+
85
  .gradio-container {
86
+ max-width: 960px !important;
87
  margin: 0 auto !important;
88
+ padding: 50px 20px !important;
89
  }
90
 
91
+ /* Typography & Headers */
92
+ .app-header {
93
+ text-align: center;
94
+ margin-bottom: 40px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
+
97
+ .app-title {
98
+ font-family: 'Outfit', sans-serif;
99
+ font-size: 42px;
100
+ font-weight: 800;
101
+ color: #ffffff;
102
+ margin: 0 0 8px 0;
103
+ letter-spacing: -1px;
104
  }
105
+
106
+ .app-title span { color: var(--accent); }
107
+ .app-subtitle { font-size: 16px; color: var(--text-muted); line-height: 1.5; }
108
+
109
+ /* 🔥 NEW Unified Chat Input Bar 🔥 */
110
+ #chat-input-container {
111
+ background: var(--bg-panel);
112
+ border: 1px solid var(--border-dim);
113
+ border-radius: 24px;
114
+ padding: 8px 8px 8px 16px;
115
+ margin-bottom: 16px;
116
+ display: flex;
117
+ align-items: flex-end;
118
+ transition: all 0.2s ease;
119
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
120
  }
121
 
122
+ #chat-input-container:focus-within {
123
+ border-color: var(--accent);
124
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25);
 
 
 
 
 
 
125
  }
126
+
127
+ /* Override Gradio's internal Textbox styling */
128
+ #query-input {
129
+ border: none !important;
130
+ box-shadow: none !important;
131
+ background: transparent !important;
132
+ flex-grow: 1;
133
  }
134
+
135
+ #query-input textarea {
136
+ background: transparent !important;
137
+ border: none !important;
138
+ box-shadow: none !important;
139
+ color: #ffffff !important;
140
+ font-family: 'Inter', sans-serif !important;
141
+ font-size: 16px !important;
142
+ padding: 12px 0 !important;
143
+ resize: none !important; /* Removes the little drag handle */
144
  }
145
 
146
+ #query-input textarea:focus {
147
+ box-shadow: none !important;
148
+ border: none !important;
149
+ }
150
 
151
+ /* 🔥 The Circle Send Button 🔥 */
152
+ #send-btn {
 
153
  background: var(--accent) !important;
154
+ color: #ffffff !important;
155
  border: none !important;
156
+ border-radius: 50% !important;
157
+ width: 44px !important;
158
+ height: 44px !important;
159
+ min-width: 44px !important;
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ font-size: 20px !important;
164
+ padding: 0 !important;
165
+ margin-left: 12px;
166
+ margin-bottom: 4px; /* Keeps it aligned to the bottom when text expands */
167
+ cursor: pointer;
168
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
 
171
+ #send-btn:hover {
172
+ background: var(--accent-hover) !important;
173
+ transform: scale(1.05);
174
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4) !important;
 
 
 
 
 
 
 
 
175
  }
176
+
177
+ #send-btn:active {
178
+ transform: scale(0.95);
 
 
179
  }
180
+
181
+ /* Controls Row */
182
+ .control-panel {
183
+ background: transparent;
184
+ margin-bottom: 30px;
 
185
  }
186
+
187
+ .control-panel .gr-box, .control-panel select {
188
+ background: var(--bg-panel) !important;
189
+ border: 1px solid var(--border-dim) !important;
190
+ border-radius: 8px !important;
 
 
 
 
191
  }
192
+
193
+ label {
194
+ font-family: 'Outfit', sans-serif !important;
195
+ font-size: 12px !important;
196
+ color: var(--text-muted) !important;
197
+ text-transform: uppercase;
198
+ letter-spacing: 0.5px;
 
199
  }
200
+
201
+ /* Output Dashboard Elements */
202
+ .results-dashboard {
203
+ display: flex;
204
+ gap: 30px;
205
+ align-items: center;
206
+ background: var(--bg-panel);
207
+ border: 1px solid var(--border-dim);
208
+ padding: 16px 24px;
209
+ border-radius: 8px;
210
+ margin-bottom: 16px;
211
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
212
  }
213
+
214
+ .metric { display: flex; flex-direction: column; }
215
+ .metric .label { font-family: 'Outfit', sans-serif; font-size: 11px; color: var(--text-muted); letter-spacing: 0.5px; }
216
+ .metric .value { font-family: 'Outfit', sans-serif; font-size: 16px; font-weight: 600; color: #fff; }
217
+
218
+ .status-badge {
219
+ margin-left: auto;
220
+ font-family: 'Outfit', sans-serif;
221
+ font-size: 12px;
222
+ padding: 6px 12px;
223
+ border-radius: 20px;
224
+ font-weight: 600;
225
  }
226
 
227
+ .status-badge.success { background: rgba(16, 185, 129, 0.1); color: #10b981; }
228
+ .status-badge.error { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
229
+ .status-badge.warning { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
230
+
231
+ /* Markdown Styling for LLM Output */
232
+ .markdown-output {
233
+ background: transparent;
234
+ font-size: 16px;
235
+ line-height: 1.8;
236
+ color: var(--text-main);
237
  }
238
+ .markdown-output h1, .markdown-output h2, .markdown-output h3 { font-family: 'Outfit', sans-serif; color: #fff; margin-top: 24px; margin-bottom: 12px; }
239
+ .markdown-output strong { color: #a855f7; font-weight: 600; }
240
+ .markdown-output a { color: var(--accent); text-decoration: none; }
241
+ .markdown-output a:hover { text-decoration: underline; }
242
+
243
+ /* Quick Tags */
244
+ .quick-tags { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; justify-content: center; }
245
+ .quick-tag {
246
+ font-size: 13px;
247
+ color: var(--text-muted);
248
+ background: var(--bg-panel);
249
+ border: 1px solid var(--border-dim);
250
+ padding: 6px 14px;
251
+ border-radius: 20px;
252
+ cursor: pointer;
253
+ transition: all 0.2s;
254
  }
255
+ .quick-tag:hover {
256
+ color: #fff;
257
+ border-color: var(--accent);
258
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);
259
  }
260
+ """
261
+
262
+ JS_INTERACTION = """
263
+ <script>
264
+ (function() {
265
+ // 1. Handle clicking the quick suggestion tags
266
+ document.addEventListener('click', (e) => {
267
+ const tag = e.target.closest('.quick-tag');
268
+ if (tag) {
269
+ const text = tag.getAttribute('data-text');
270
+ const textarea = document.querySelector('textarea');
271
+ if (textarea && text) {
272
+ textarea.value = text;
273
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
274
+
275
+ // Find and click the send button
276
+ let btn = document.querySelector('#send-btn');
277
+ if (btn && btn.tagName !== 'BUTTON') btn = btn.querySelector('button') || btn;
278
+ if (btn) btn.click();
279
+ }
280
+ }
281
+ });
282
+
283
+ // 2. Override Gradio's internal key bindings
284
+ document.addEventListener('keydown', (e) => {
285
+ // Target our specific textarea
286
+ if (e.target.tagName.toLowerCase() === 'textarea') {
287
+
288
+ if (e.key === 'Enter') {
289
+
290
+ if (e.shiftKey) {
291
+ // SHIFT + ENTER: We want a new line.
292
+ // Stop Gradio from seeing this and triggering its default "Submit"
293
+ e.stopPropagation();
294
+ e.stopImmediatePropagation();
295
+ // We DO NOT preventDefault, so the browser natively adds the new line.
296
+ }
297
+ else if (!e.ctrlKey && !e.metaKey) {
298
+ // PLAIN ENTER: We want to submit.
299
+ // Stop the browser from making a new line
300
+ e.preventDefault();
301
+ // Stop Gradio from seeing this event at all
302
+ e.stopPropagation();
303
+ e.stopImmediatePropagation();
304
+
305
+ // Click our send button
306
+ let btn = document.querySelector('#send-btn');
307
+ if (btn && btn.tagName !== 'BUTTON') btn = btn.querySelector('button') || btn;
308
+ if (btn) btn.click();
309
+ }
310
+ }
311
+ }
312
+ }, true); // Use the capture phase to intercept before React/Gradio does
313
+ })();
314
+ </script>
315
  """
316
 
317
 
318
+ def create_interface():
319
+ with gr.Blocks(title="Anime Recommender") as demo:
320
+
321
+ gr.HTML("""
322
+ <div class="app-header">
323
+ <h1 class="app-title">Find your next <span>anime</span>.</h1>
324
+ <p class="app-subtitle">Describe the exact vibe, mood, or story you're looking for.</p>
325
+
326
+ <div class="quick-tags">
327
+ <div class="quick-tag" data-text="Cozy slice of life set in the countryside">Cozy Countryside</div>
328
+ <div class="quick-tag" data-text="Dark fantasy with an unreliable narrator">Dark Fantasy</div>
329
+ <div class="quick-tag" data-text="Fast-paced cyberpunk action with great animation">Cyberpunk Action</div>
330
+ </div>
331
+ </div>
332
+ """)
333
+
334
+ # 🔥 NEW UNIFIED INPUT BAR 🔥
335
+ with gr.Row(elem_id="chat-input-container"):
336
+ query_input = gr.Textbox(
337
+ label="",
338
+ show_label=False,
339
+ lines=2,
340
+ placeholder="Message the recommender... (e.g., A melancholic sci-fi)",
341
+ elem_id="query-input",
342
+ scale=10
343
+ )
344
+
345
+ # Using an upward arrow icon (↑) for the send button
346
+ submit_btn = gr.Button(
347
+ "➔",
348
+ elem_id="send-btn",
349
+ scale=1
350
+ )
351
+
352
+ with gr.Group(elem_classes="control-panel"):
353
+ with gr.Row():
354
+ genre_dropdown = gr.Dropdown(
355
+ choices=["Any", "Action", "Sci-Fi", "Slice of Life",
356
+ "Fantasy", "Psychological", "Horror", "Romance"],
357
+ value="Any",
358
+ label="Genre Filter",
359
+ )
360
+ min_score_slider = gr.Slider(
361
+ minimum=0, maximum=10, value=0, step=0.5,
362
+ label="Minimum Rating"
363
+ )
364
+ n_results_slider = gr.Slider(
365
+ minimum=1, maximum=8, value=3, step=1,
366
+ label="Number of Results"
367
+ )
368
+
369
+ output_header = gr.HTML(
370
+ value="<div class='results-dashboard'><div class='status-badge warning'>Waiting for input</div></div>"
371
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
+ output_text = gr.Markdown(
374
+ value="",
375
+ elem_classes="markdown-output"
376
+ )
 
 
 
 
 
 
 
 
 
 
377
 
378
+ gr.HTML(JS_INTERACTION)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
+ submit_btn.click(
381
+ fn=get_recommendations,
382
+ inputs=[query_input, min_score_slider,
383
+ genre_dropdown, n_results_slider],
384
+ outputs=[output_header, output_text]
385
+ )
 
386
 
387
+ # Allow native submission as a fallback
388
+ query_input.submit(
389
+ fn=get_recommendations,
390
+ inputs=[query_input, min_score_slider,
391
+ genre_dropdown, n_results_slider],
392
+ outputs=[output_header, output_text]
393
+ )
394
 
395
+ return demo
 
 
 
 
 
 
 
 
396
 
 
 
 
 
397
 
398
  if __name__ == "__main__":
399
+ demo = create_interface()
400
+
401
+ theme = themes.Base(
402
+ font=[themes.GoogleFont("Inter"), "ui-sans-serif",
403
+ "system-ui", "sans-serif"],
404
+ ).set(
405
+ body_background_fill="transparent",
406
+ block_background_fill="transparent",
407
+ block_border_width="0px",
408
+ input_background_fill="transparent",
409
+ )
410
+
411
+ demo.launch(
412
+ server_name="0.0.0.0",
413
+ server_port=7860,
414
+ share=False,
415
+ css=CSS,
416
+ theme=theme
417
+ )