siddhm11 commited on
Commit
cd54524
ยท
1 Parent(s): a74c7e3

feat: UI Polish and AI Search Summaries

Browse files

- Add mobile-responsive navbar with hamburger menu
- Redesign index hero section and empty saved state
- Polish paper cards with year pills and citation icons
- Add AI Overview generation via Groq using llama-3.3-70b-versatile
- Async load AI summaries in search results via HTMX
- Add site footer

app/groq_svc.py CHANGED
@@ -158,3 +158,69 @@ def _run_rewrite(client, query: str) -> str:
158
  timeout=2.0, # Hard 2s timeout โ€” search must not stall
159
  )
160
  return response.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  timeout=2.0, # Hard 2s timeout โ€” search must not stall
159
  )
160
  return response.choices[0].message.content
161
+
162
+
163
+ # โ”€โ”€ AI Search Summaries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
164
+
165
+ async def generate_search_summary(query: str, papers: list[dict]) -> str | None:
166
+ """
167
+ Generate a short 3-4 sentence AI summary synthesizing the top papers.
168
+ Uses llama-3.3-70b-versatile via Groq.
169
+
170
+ Returns:
171
+ Summary HTML string, or None if error or not enough papers.
172
+ """
173
+ if not papers or len(papers) < 2:
174
+ return None
175
+
176
+ client = _get_client()
177
+ if client is None:
178
+ return None
179
+
180
+ # Build context from top 5 papers max
181
+ context_lines = []
182
+ for i, p in enumerate(papers[:5]):
183
+ context_lines.append(f"Paper {i+1}: {p['title']}\nAbstract: {p['abstract'][:800]}...")
184
+ context_str = "\n\n".join(context_lines)
185
+
186
+ prompt = f"""You are an expert AI research assistant.
187
+ The user searched for: "{query}"
188
+
189
+ Here are the top papers returned for this query:
190
+ {context_str}
191
+
192
+ Task: Write a concise, synthesized overview (3-4 sentences max) that answers the user's query based ONLY on these papers.
193
+ Format: Return plain text with basic markdown (bolding key terms is good). DO NOT start with "Here is a summary" or similar filler. DO NOT output bullet points. Be direct, educational, and authoritative."""
194
+
195
+ try:
196
+ import asyncio
197
+ loop = asyncio.get_event_loop()
198
+ result = await loop.run_in_executor(None, _run_summary, client, prompt)
199
+
200
+ summary = result.strip()
201
+ if not summary:
202
+ return None
203
+
204
+ # Basic markdown to HTML (bolding)
205
+ import re
206
+ summary_html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', summary)
207
+
208
+ return summary_html
209
+
210
+ except Exception as e:
211
+ print(f"[groq_svc] Summary generation failed: {e}")
212
+ return None
213
+
214
+
215
+ def _run_summary(client, prompt: str) -> str:
216
+ """Sync helper: call Groq chat completion for summaries with 4s timeout."""
217
+ response = client.chat.completions.create(
218
+ messages=[
219
+ {"role": "user", "content": prompt},
220
+ ],
221
+ model="llama-3.3-70b-versatile",
222
+ temperature=0.3,
223
+ max_tokens=150,
224
+ timeout=4.0, # 4s timeout so it doesn't hang indefinitely
225
+ )
226
+ return response.choices[0].message.content
app/routers/search.py CHANGED
@@ -112,3 +112,44 @@ async def search(
112
 
113
  resp.set_cookie(COOKIE_NAME, user_id, max_age=365 * 24 * 3600, httponly=True)
114
  return resp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  resp.set_cookie(COOKIE_NAME, user_id, max_age=365 * 24 * 3600, httponly=True)
114
  return resp
115
+
116
+
117
+ @router.get("/api/search/summary", response_class=HTMLResponse)
118
+ async def search_summary(
119
+ request: Request,
120
+ q: str = "",
121
+ ids: str = "",
122
+ ):
123
+ """
124
+ Generate an AI Overview for a given search query and set of arXiv IDs.
125
+ Called asynchronously by HTMX after search results load.
126
+ """
127
+ if not q or not ids:
128
+ return ""
129
+
130
+ arxiv_ids = [aid.strip() for aid in ids.split(",") if aid.strip()]
131
+ if not arxiv_ids:
132
+ return ""
133
+
134
+ from app import groq_svc
135
+
136
+ # Fetch metadata for the top papers (need abstracts for summary)
137
+ try:
138
+ meta = await turso_svc.fetch_metadata_batch(arxiv_ids[:5])
139
+ papers = [meta[aid] for aid in arxiv_ids[:5] if aid in meta]
140
+ except Exception as e:
141
+ print(f"[search] Turso fetch failed for summary: {e}")
142
+ return ""
143
+
144
+ if not papers:
145
+ return ""
146
+
147
+ summary_html = await groq_svc.generate_search_summary(q, papers)
148
+ if not summary_html:
149
+ return ""
150
+
151
+ return templates.TemplateResponse(
152
+ request,
153
+ "partials/ai_summary.html",
154
+ {"summary_html": summary_html},
155
+ )
app/static/styles.css CHANGED
@@ -282,3 +282,102 @@ html {
282
  .seed-card:hover {
283
  border-color: rgba(99, 102, 241, 0.3);
284
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  .seed-card:hover {
283
  border-color: rgba(99, 102, 241, 0.3);
284
  }
285
+
286
+ /* โ”€โ”€ UI Polish Additions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
287
+
288
+ /* Mobile Menu */
289
+ .mobile-menu-toggle:checked ~ .mobile-menu {
290
+ display: flex;
291
+ }
292
+ .mobile-menu {
293
+ display: none;
294
+ flex-direction: column;
295
+ position: absolute;
296
+ top: 100%;
297
+ left: 0;
298
+ right: 0;
299
+ background: rgba(20, 20, 30, 0.95);
300
+ backdrop-filter: blur(16px);
301
+ border-bottom: 1px solid rgba(255,255,255,0.1);
302
+ padding: 1rem;
303
+ z-index: 50;
304
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
305
+ }
306
+ [data-theme="light"] .mobile-menu {
307
+ background: rgba(255, 255, 255, 0.95);
308
+ border-bottom: 1px solid rgba(0,0,0,0.1);
309
+ }
310
+
311
+ /* AI Summary Card */
312
+ .ai-summary-card {
313
+ position: relative;
314
+ background: rgba(30, 30, 45, 0.5);
315
+ border-radius: 12px;
316
+ padding: 1.25rem;
317
+ margin-bottom: 1.5rem;
318
+ overflow: hidden;
319
+ border: 1px solid rgba(255, 255, 255, 0.08);
320
+ }
321
+ [data-theme="light"] .ai-summary-card {
322
+ background: rgba(255, 255, 255, 0.8);
323
+ border: 1px solid rgba(0, 0, 0, 0.08);
324
+ }
325
+ .ai-summary-card::before {
326
+ content: "";
327
+ position: absolute;
328
+ top: 0; left: 0; right: 0;
329
+ height: 2px;
330
+ background: linear-gradient(90deg, #6366f1, #a78bfa, #6366f1);
331
+ background-size: 200% auto;
332
+ animation: shimmer 3s linear infinite;
333
+ }
334
+ .ai-summary-card .typewriter {
335
+ display: inline;
336
+ }
337
+
338
+ /* Paper Card Meta */
339
+ .year-pill {
340
+ font-size: 0.7rem;
341
+ font-weight: 600;
342
+ padding: 2px 6px;
343
+ border-radius: 6px;
344
+ background: rgba(148, 163, 184, 0.15);
345
+ color: #94a3b8;
346
+ }
347
+ .year-pill.recent {
348
+ background: rgba(34, 197, 94, 0.15);
349
+ color: #4ade80;
350
+ }
351
+ [data-theme="light"] .year-pill {
352
+ background: rgba(148, 163, 184, 0.2);
353
+ color: #475569;
354
+ }
355
+ [data-theme="light"] .year-pill.recent {
356
+ background: rgba(34, 197, 94, 0.2);
357
+ color: #16a34a;
358
+ }
359
+
360
+ .cite-count {
361
+ font-size: 0.75rem;
362
+ display: inline-flex;
363
+ align-items: center;
364
+ gap: 4px;
365
+ color: #94a3b8;
366
+ }
367
+ [data-theme="light"] .cite-count {
368
+ color: #64748b;
369
+ }
370
+
371
+ /* Footer */
372
+ .app-footer {
373
+ border-top: 1px solid rgba(255,255,255,0.06);
374
+ margin-top: 4rem;
375
+ padding: 2rem 0;
376
+ text-align: center;
377
+ color: rgba(255,255,255,0.4);
378
+ font-size: 0.8rem;
379
+ }
380
+ [data-theme="light"] .app-footer {
381
+ border-top: 1px solid rgba(0,0,0,0.06);
382
+ color: rgba(0,0,0,0.4);
383
+ }
app/templates/base.html CHANGED
@@ -35,30 +35,59 @@
35
  <body class="bg-base-200 min-h-screen">
36
 
37
  <!-- Navbar -->
38
- <nav class="navbar navbar-glass sticky top-0 z-50 px-6">
39
  <div class="flex-1">
40
  <a href="/" class="flex items-center gap-2 text-xl">
41
  <span class="brand-text">ResearchIT</span>
42
  </a>
43
  </div>
44
- <div class="flex-none flex items-center gap-1">
 
 
45
  <a href="/" class="btn btn-ghost btn-sm">Home</a>
46
  <a href="/search" class="btn btn-ghost btn-sm">Search</a>
47
  <a href="/saved" class="btn btn-ghost btn-sm">Saved</a>
48
  <!-- Theme toggle -->
49
- <button id="theme-toggle" class="btn btn-ghost btn-sm btn-circle" title="Toggle theme"
50
  onclick="toggleTheme()">
51
  <span id="theme-icon-dark" class="hidden">โ˜€๏ธ</span>
52
  <span id="theme-icon-light" class="hidden">๐ŸŒ™</span>
53
  </button>
54
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  </nav>
56
 
57
  <!-- Main content -->
58
- <main class="container mx-auto px-4 py-6 max-w-4xl">
59
  {% block content %}{% endblock %}
60
  </main>
61
 
 
 
 
 
 
 
 
62
  <!-- Theme toggle logic -->
63
  <script>
64
  function toggleTheme() {
@@ -72,8 +101,16 @@
72
 
73
  function updateThemeIcon() {
74
  const theme = document.documentElement.getAttribute('data-theme');
75
- document.getElementById('theme-icon-dark').classList.toggle('hidden', theme !== 'dark');
76
- document.getElementById('theme-icon-light').classList.toggle('hidden', theme !== 'light');
 
 
 
 
 
 
 
 
77
  }
78
 
79
  // Set icon on load
 
35
  <body class="bg-base-200 min-h-screen">
36
 
37
  <!-- Navbar -->
38
+ <nav class="navbar navbar-glass sticky top-0 z-50 px-4 md:px-6">
39
  <div class="flex-1">
40
  <a href="/" class="flex items-center gap-2 text-xl">
41
  <span class="brand-text">ResearchIT</span>
42
  </a>
43
  </div>
44
+
45
+ <!-- Desktop Menu -->
46
+ <div class="hidden md:flex flex-none items-center gap-1">
47
  <a href="/" class="btn btn-ghost btn-sm">Home</a>
48
  <a href="/search" class="btn btn-ghost btn-sm">Search</a>
49
  <a href="/saved" class="btn btn-ghost btn-sm">Saved</a>
50
  <!-- Theme toggle -->
51
+ <button id="theme-toggle" class="btn btn-ghost btn-sm btn-circle ml-2" title="Toggle theme"
52
  onclick="toggleTheme()">
53
  <span id="theme-icon-dark" class="hidden">โ˜€๏ธ</span>
54
  <span id="theme-icon-light" class="hidden">๐ŸŒ™</span>
55
  </button>
56
  </div>
57
+
58
+ <!-- Mobile Menu Toggle -->
59
+ <div class="flex-none md:hidden">
60
+ <label class="btn btn-ghost btn-sm btn-circle swap swap-rotate">
61
+ <input type="checkbox" class="mobile-menu-toggle hidden" />
62
+ <svg class="swap-off fill-current" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path d="M64,384H448V341.33H64Zm0-106.67H448V234.67H64ZM64,128v42.67H448V128Z"/></svg>
63
+ <svg class="swap-on fill-current" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><polygon points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49"/></svg>
64
+ </label>
65
+ <!-- Mobile Menu Dropdown -->
66
+ <div class="mobile-menu hidden">
67
+ <a href="/" class="btn btn-ghost justify-start">Home</a>
68
+ <a href="/search" class="btn btn-ghost justify-start">Search</a>
69
+ <a href="/saved" class="btn btn-ghost justify-start">Saved</a>
70
+ <div class="divider my-1"></div>
71
+ <button class="btn btn-ghost justify-start" onclick="toggleTheme()">
72
+ <span class="theme-text-dark hidden">Switch to Light Mode โ˜€๏ธ</span>
73
+ <span class="theme-text-light hidden">Switch to Dark Mode ๐ŸŒ™</span>
74
+ </button>
75
+ </div>
76
+ </div>
77
  </nav>
78
 
79
  <!-- Main content -->
80
+ <main class="container mx-auto px-4 py-6 md:py-8 max-w-4xl min-h-[70vh]">
81
  {% block content %}{% endblock %}
82
  </main>
83
 
84
+ <footer class="app-footer">
85
+ <div class="container mx-auto px-4 max-w-4xl">
86
+ <p>Built with โค๏ธ โ€” <span class="brand-text font-semibold">ResearchIT</span></p>
87
+ <p class="mt-2 text-xs opacity-60">AI-powered arXiv recommendations using Qdrant, Zilliz & Turso.</p>
88
+ </div>
89
+ </footer>
90
+
91
  <!-- Theme toggle logic -->
92
  <script>
93
  function toggleTheme() {
 
101
 
102
  function updateThemeIcon() {
103
  const theme = document.documentElement.getAttribute('data-theme');
104
+
105
+ // Desktop icons
106
+ const iconDark = document.getElementById('theme-icon-dark');
107
+ const iconLight = document.getElementById('theme-icon-light');
108
+ if (iconDark) iconDark.classList.toggle('hidden', theme !== 'dark');
109
+ if (iconLight) iconLight.classList.toggle('hidden', theme !== 'light');
110
+
111
+ // Mobile menu text
112
+ document.querySelectorAll('.theme-text-dark').forEach(el => el.classList.toggle('hidden', theme !== 'dark'));
113
+ document.querySelectorAll('.theme-text-light').forEach(el => el.classList.toggle('hidden', theme !== 'light'));
114
  }
115
 
116
  // Set icon on load
app/templates/index.html CHANGED
@@ -6,21 +6,35 @@
6
  <div class="space-y-8 fade-in">
7
 
8
  <!-- Hero / search bar -->
9
- <div class="card bg-base-100 shadow-md rounded-xl p-6">
10
- <h1 class="text-2xl font-bold mb-1">
11
  <span class="brand-text">Discover Research</span>
12
  </h1>
13
- <p class="text-sm text-base-content/60 mb-4">
14
- Search arXiv, save papers you like โ€” get personalised recommendations.
15
  </p>
16
- <form action="/search" method="get" class="flex gap-2">
17
- <input type="text"
18
- name="q"
19
- placeholder="e.g. transformer attention mechanism"
20
- class="input input-bordered flex-1"
21
- autofocus />
22
- <button class="btn btn-primary" type="submit">Search</button>
23
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
 
26
  <!-- Recommendations section -->
 
6
  <div class="space-y-8 fade-in">
7
 
8
  <!-- Hero / search bar -->
9
+ <div class="py-8 md:py-12 text-center">
10
+ <h1 class="text-4xl md:text-5xl font-extrabold mb-4 tracking-tight">
11
  <span class="brand-text">Discover Research</span>
12
  </h1>
13
+ <p class="text-base md:text-lg text-base-content/70 mb-8 max-w-xl mx-auto">
14
+ Search 1.6M+ arXiv papers, get AI summaries, and save papers to build your personalized recommendation feed.
15
  </p>
16
+
17
+ <div class="max-w-2xl mx-auto relative group">
18
+ <div class="absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-500"></div>
19
+ <div class="card bg-base-100 shadow-xl rounded-xl relative">
20
+ <form action="/search" method="get" class="flex gap-2 p-2">
21
+ <input type="text"
22
+ name="q"
23
+ placeholder="e.g. transformer attention mechanism..."
24
+ class="input input-lg w-full bg-transparent border-none focus:outline-none focus:ring-0 text-base md:text-lg px-4"
25
+ autofocus />
26
+ <button class="btn btn-primary btn-lg rounded-lg" type="submit">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
28
+ Search
29
+ </button>
30
+ </form>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="flex justify-center gap-4 mt-6 text-sm text-base-content/50">
35
+ <span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> Qdrant Vector Search</span>
36
+ <span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg> Groq AI Summaries</span>
37
+ </div>
38
  </div>
39
 
40
  <!-- Recommendations section -->
app/templates/partials/ai_summary.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {# Partial: AI Summary Card #}
2
+ {% if summary_html %}
3
+ <div class="ai-summary-card mb-6">
4
+ <div class="flex items-center gap-2 mb-3">
5
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor">
6
+ <path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
7
+ </svg>
8
+ <span class="font-semibold text-sm brand-text">AI Overview</span>
9
+ </div>
10
+ <div class="text-sm text-base-content/90 leading-relaxed typewriter">
11
+ {{ summary_html | safe }}
12
+ </div>
13
+ <div class="mt-3 flex justify-end">
14
+ <span class="text-[10px] text-base-content/40 uppercase tracking-wide">Powered by Groq</span>
15
+ </div>
16
+ </div>
17
+ {% endif %}
app/templates/partials/paper_card.html CHANGED
@@ -49,12 +49,30 @@
49
  </div>
50
 
51
  <!-- Meta: arXiv ID + year + authors (max 3) + citations -->
52
- <div class="text-xs text-base-content/50 mono">
53
- [{{ paper.arxiv_id }}]
54
- {% if paper.published %} ยท {{ paper.published[:4] }}{% endif %}
55
- {% if authors_list %} ยท <span class="font-sans">{{ authors_list[:3] | join(", ") }}{% if authors_list | length > 3 %} et al. ({{ authors_list | length }} authors){% endif %}</span>{% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  {% if paper.citation_count %}
57
- ยท <span class="font-medium text-base-content/70 font-sans" title="{{ paper.influential_citations|default(0) }} influential">๐Ÿ“Š {{ paper.citation_count }} citations</span>
 
 
 
 
58
  {% endif %}
59
  </div>
60
 
 
49
  </div>
50
 
51
  <!-- Meta: arXiv ID + year + authors (max 3) + citations -->
52
+ <div class="flex flex-wrap items-center gap-2 mb-1">
53
+ {% set p_year = (paper.year | string) if paper.year else (paper.published[:4] if paper.published else "") %}
54
+ {% if p_year %}
55
+ <span class="year-pill {% if p_year in ['2025', '2026', '2024'] %}recent{% endif %}">{{ p_year }}</span>
56
+ {% endif %}
57
+
58
+ <span class="text-xs text-base-content/40 mono font-medium">[{{ paper.arxiv_id }}]</span>
59
+
60
+ {% if authors_list %}
61
+ <span class="text-xs text-base-content/60 mx-1">โ€ข</span>
62
+ <span class="text-xs text-base-content/70">
63
+ {{ authors_list[:3] | join(", ") }}
64
+ {% if authors_list | length > 3 %}
65
+ <span class="opacity-60 italic">et al. ({{ authors_list | length }} authors)</span>
66
+ {% endif %}
67
+ </span>
68
+ {% endif %}
69
+
70
  {% if paper.citation_count %}
71
+ <span class="text-xs text-base-content/60 mx-1 hidden sm:inline">โ€ข</span>
72
+ <span class="cite-count" title="{{ paper.influential_citations|default(0) }} influential">
73
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
74
+ {{ paper.citation_count }} citations
75
+ </span>
76
  {% endif %}
77
  </div>
78
 
app/templates/partials/search_results.html CHANGED
@@ -1,7 +1,7 @@
1
  {# Partial: list of search result cards #}
2
  {% if papers %}
3
  <div class="space-y-3">
4
- <div class="flex flex-col gap-1 mb-4">
5
  <div class="flex justify-between items-center text-sm text-base-content/50">
6
  <span>{{ papers | length }} results for "{{ query }}"</span>
7
  {% if search_meta and search_meta.total_time_ms is defined %}
@@ -9,28 +9,41 @@
9
  {% endif %}
10
  </div>
11
 
12
- {# Groq rewrite result โ€” show both rewritten AND skipped cases #}
13
  {% if search_meta %}
14
  {% if search_meta.rewritten_query %}
15
- <div class="alert bg-base-200 border-l-4 border-primary p-3 text-sm flex gap-2">
16
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-primary shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
17
- <div class="flex-1">
18
- <span class="font-semibold">Groq expanded query:</span> "{{ search_meta.rewritten_query }}"
19
- <span class="text-xs text-base-content/50 ml-2">({{ search_meta.groq_time_ms }}ms)</span>
20
- </div>
21
- </div>
22
- {% elif search_meta.groq_status is defined and search_meta.groq_status != 'rewritten' %}
23
- <div class="alert bg-base-200/50 border-l-4 border-base-300 p-3 text-sm flex gap-2">
24
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-base-content/30 shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
25
- <div class="flex-1 text-base-content/50">
26
- <span class="font-semibold">Groq rewrite:</span> {{ search_meta.groq_status }}
27
- โ€” searching with original query as-is
28
- </div>
29
  </div>
30
  {% endif %}
31
  {% endif %}
32
  </div>
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  {% for paper in papers %}
35
  {% set position = loop.index0 %}
36
  {% set source = "search" %}
@@ -40,48 +53,51 @@
40
 
41
  {# Pipeline timing breakdown #}
42
  {% if search_meta %}
43
- <div class="mt-4 p-3 rounded-lg bg-base-200/50 border border-base-300/30">
44
- <div class="flex items-center gap-2 mb-2">
45
- <span class="text-xs font-semibold text-base-content/60">โšก Search Pipeline Breakdown</span>
 
46
  {% if search_meta.total_time_ms is defined %}
47
- <span class="text-xs text-base-content/40">({{ search_meta.total_time_ms }}ms total)</span>
48
- {% endif %}
49
- </div>
50
- <div class="flex flex-wrap gap-x-4 gap-y-1 text-xs font-mono text-base-content/50">
51
- {% if search_meta.groq_time_ms is defined %}
52
- <span>Groq Rewrite: <span class="text-primary">{{ search_meta.groq_time_ms }}ms</span>
53
- {% if search_meta.groq_status is defined and search_meta.groq_status != 'rewritten' %}
54
- <span class="text-warning/60">({{ search_meta.groq_status }})</span>
55
- {% endif %}
56
- </span>
57
- {% endif %}
58
- {% if search_meta.encode_time_ms is defined %}
59
- <span>BGE-M3 Encode: <span class="text-primary">{{ search_meta.encode_time_ms }}ms</span></span>
60
- {% endif %}
61
- {% if search_meta.retrieval_time_ms is defined %}
62
- <span>Qdrant+Zilliz Retrieval: <span class="text-primary">{{ search_meta.retrieval_time_ms }}ms</span>
63
- {% if search_meta.n_retrieval_tasks is defined %}
64
- <span class="text-base-content/30">({{ search_meta.n_retrieval_tasks }} parallel tasks)</span>
65
- {% endif %}
66
- </span>
67
- {% endif %}
68
- {% if search_meta.rrf_time_ms is defined %}
69
- <span>RRF Fusion: <span class="text-primary">{{ search_meta.rrf_time_ms }}ms</span></span>
70
- {% endif %}
71
- {% if search_meta.bge_rerank_time_ms is defined %}
72
- <span>BGE Rerank: <span class="text-primary">{{ search_meta.bge_rerank_time_ms }}ms</span></span>
73
- {% endif %}
74
- {% if search_meta.turso_boost_fetch_ms is defined %}
75
- <span>Turso Title Fetch: <span class="text-primary">{{ search_meta.turso_boost_fetch_ms }}ms</span></span>
76
- <span>Rerank Compute: <span class="text-primary">{{ search_meta.rerank_compute_ms }}ms</span></span>
77
- {% elif search_meta.rerank_time_ms is defined %}
78
- <span>Title+Citation Rerank: <span class="text-primary">{{ search_meta.rerank_time_ms }}ms</span></span>
79
- {% endif %}
80
- {% if search_meta.meta_time_ms is defined %}
81
- <span>Final Metadata: <span class="text-primary">{{ search_meta.meta_time_ms }}ms</span></span>
82
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  </div>
84
- </div>
85
  {% endif %}
86
 
87
  {% elif query %}
 
1
  {# Partial: list of search result cards #}
2
  {% if papers %}
3
  <div class="space-y-3">
4
+ <div class="flex flex-col gap-2 mb-4">
5
  <div class="flex justify-between items-center text-sm text-base-content/50">
6
  <span>{{ papers | length }} results for "{{ query }}"</span>
7
  {% if search_meta and search_meta.total_time_ms is defined %}
 
9
  {% endif %}
10
  </div>
11
 
12
+ {# Groq rewrite result #}
13
  {% if search_meta %}
14
  {% if search_meta.rewritten_query %}
15
+ <div class="text-xs text-base-content/60 bg-base-200/50 rounded px-2 py-1 inline-flex w-fit items-center gap-1 border border-base-300/50">
16
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
17
+ Searched for: <span class="font-medium">"{{ search_meta.rewritten_query }}"</span>
 
 
 
 
 
 
 
 
 
 
 
18
  </div>
19
  {% endif %}
20
  {% endif %}
21
  </div>
22
 
23
+ {# AI Summary Slot #}
24
+ {% if query and papers %}
25
+ {% set arxiv_ids = papers | map(attribute='arxiv_id') | join(',') %}
26
+ <div id="ai-summary-container"
27
+ hx-get="/api/search/summary?q={{ query | urlencode }}&ids={{ arxiv_ids }}"
28
+ hx-trigger="load"
29
+ hx-swap="innerHTML">
30
+ <!-- Skeleton loader for AI summary -->
31
+ <div class="ai-summary-card">
32
+ <div class="flex items-center gap-2 mb-2">
33
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor">
34
+ <path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
35
+ </svg>
36
+ <span class="font-semibold text-sm brand-text">AI Overview</span>
37
+ </div>
38
+ <div class="space-y-2 animate-pulse opacity-60">
39
+ <div class="h-3 bg-base-content/20 rounded w-full"></div>
40
+ <div class="h-3 bg-base-content/20 rounded w-5/6"></div>
41
+ <div class="h-3 bg-base-content/20 rounded w-4/6"></div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ {% endif %}
46
+
47
  {% for paper in papers %}
48
  {% set position = loop.index0 %}
49
  {% set source = "search" %}
 
53
 
54
  {# Pipeline timing breakdown #}
55
  {% if search_meta %}
56
+ <details class="mt-6 group">
57
+ <summary class="text-xs font-semibold text-base-content/50 cursor-pointer list-none flex items-center gap-1 w-fit">
58
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 transition-transform group-open:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
59
+ Search Pipeline Details
60
  {% if search_meta.total_time_ms is defined %}
61
+ <span class="font-normal opacity-70">({{ search_meta.total_time_ms }}ms)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  {% endif %}
63
+ </summary>
64
+ <div class="mt-2 p-3 rounded-lg bg-base-200/50 border border-base-300/30">
65
+ <div class="flex flex-wrap gap-x-4 gap-y-1 text-xs font-mono text-base-content/60">
66
+ {% if search_meta.groq_time_ms is defined %}
67
+ <span>Groq Rewrite: <span class="text-primary">{{ search_meta.groq_time_ms }}ms</span>
68
+ {% if search_meta.groq_status is defined and search_meta.groq_status != 'rewritten' %}
69
+ <span class="text-warning/60">({{ search_meta.groq_status }})</span>
70
+ {% endif %}
71
+ </span>
72
+ {% endif %}
73
+ {% if search_meta.encode_time_ms is defined %}
74
+ <span>BGE-M3 Encode: <span class="text-primary">{{ search_meta.encode_time_ms }}ms</span></span>
75
+ {% endif %}
76
+ {% if search_meta.retrieval_time_ms is defined %}
77
+ <span>Qdrant+Zilliz: <span class="text-primary">{{ search_meta.retrieval_time_ms }}ms</span>
78
+ {% if search_meta.n_retrieval_tasks is defined %}
79
+ <span class="text-base-content/30">({{ search_meta.n_retrieval_tasks }} parallel tasks)</span>
80
+ {% endif %}
81
+ </span>
82
+ {% endif %}
83
+ {% if search_meta.rrf_time_ms is defined %}
84
+ <span>RRF Fusion: <span class="text-primary">{{ search_meta.rrf_time_ms }}ms</span></span>
85
+ {% endif %}
86
+ {% if search_meta.bge_rerank_time_ms is defined %}
87
+ <span>BGE Rerank: <span class="text-primary">{{ search_meta.bge_rerank_time_ms }}ms</span></span>
88
+ {% endif %}
89
+ {% if search_meta.turso_boost_fetch_ms is defined %}
90
+ <span>Turso Title Fetch: <span class="text-primary">{{ search_meta.turso_boost_fetch_ms }}ms</span></span>
91
+ <span>Rerank Compute: <span class="text-primary">{{ search_meta.rerank_compute_ms }}ms</span></span>
92
+ {% elif search_meta.rerank_time_ms is defined %}
93
+ <span>Title+Citation Rerank: <span class="text-primary">{{ search_meta.rerank_time_ms }}ms</span></span>
94
+ {% endif %}
95
+ {% if search_meta.meta_time_ms is defined %}
96
+ <span>Final Metadata: <span class="text-primary">{{ search_meta.meta_time_ms }}ms</span></span>
97
+ {% endif %}
98
+ </div>
99
  </div>
100
+ </details>
101
  {% endif %}
102
 
103
  {% elif query %}
app/templates/saved.html CHANGED
@@ -23,15 +23,17 @@
23
  </div>
24
  {% else %}
25
  <!-- Empty state -->
26
- <div class="card bg-base-100 shadow-md rounded-xl p-10 text-center space-y-3">
27
- <div class="text-5xl">๐Ÿ“š</div>
28
- <p class="text-lg font-semibold text-base-content/70">No saved papers yet</p>
29
- <p class="text-sm text-base-content/50">
30
- Search for papers and click <strong>โญ Save</strong> on ones you find interesting.
31
- Once you save a paper it will appear here.
32
  </p>
33
- <div class="pt-2">
34
- <a href="/search" class="btn btn-primary btn-sm">Start Searching</a>
 
 
 
35
  </div>
36
  </div>
37
  {% endif %}
 
23
  </div>
24
  {% else %}
25
  <!-- Empty state -->
26
+ <div class="card bg-base-100/50 border border-base-200/50 shadow-sm rounded-xl p-12 text-center space-y-4 max-w-lg mx-auto mt-10">
27
+ <div class="text-6xl mb-4 opacity-80">๐Ÿ“š</div>
28
+ <h2 class="text-xl font-semibold brand-text">No saved papers yet</h2>
29
+ <p class="text-sm text-base-content/60 leading-relaxed">
30
+ Building your personal research feed starts here. Search for topics you care about and save papers you find interesting to teach the AI your preferences.
 
31
  </p>
32
+ <div class="pt-4">
33
+ <a href="/search" class="btn btn-primary shadow-lg shadow-primary/20">
34
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
35
+ Start Searching
36
+ </a>
37
  </div>
38
  </div>
39
  {% endif %}