thomasht86 commited on
Commit
e0b54fb
β€’
1 Parent(s): a0b3781

Upload folder using huggingface_hub

Browse files
Files changed (8) hide show
  1. README.md +0 -2
  2. backend/vespa_app.py +20 -0
  3. frontend/app.py +108 -48
  4. frontend/layout.py +7 -7
  5. globals.css +12 -0
  6. icons.py +1 -1
  7. main.py +31 -9
  8. output.css +57 -49
README.md CHANGED
@@ -78,8 +78,6 @@ python hello.py
78
 
79
  First, set up your `.env` file by renaming `.env.example` to `.env` and filling in the required values.
80
  (Token can be shared with 1password, `HF_TOKEN` is personal and must be created at huggingface)
81
- If you are just connecting to a deployed Vespa app, you can skip
82
- to [Connecting to the Vespa app](#connecting-to-the-vespa-app-and-querying).
83
 
84
  ### Deploying the Vespa app
85
 
 
78
 
79
  First, set up your `.env` file by renaming `.env.example` to `.env` and filling in the required values.
80
  (Token can be shared with 1password, `HF_TOKEN` is personal and must be created at huggingface)
 
 
81
 
82
  ### Deploying the Vespa app
83
 
backend/vespa_app.py CHANGED
@@ -330,3 +330,23 @@ class VespaQueryClient:
330
  )
331
  assert response.is_successful(), response.json
332
  return self.format_query_results(query, response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  )
331
  assert response.is_successful(), response.json
332
  return self.format_query_results(query, response)
333
+
334
+ async def keepalive(self) -> bool:
335
+ """
336
+ Query Vespa to keep the connection alive.
337
+
338
+ Returns:
339
+ bool: True if the connection is alive.
340
+ """
341
+ async with self.app.asyncio(connections=1) as session:
342
+ response: VespaQueryResponse = await session.query(
343
+ body={
344
+ "yql": f"select title from {self.VESPA_SCHEMA_NAME} where true limit 1;",
345
+ "ranking": "unranked",
346
+ "query": "keepalive",
347
+ "timeout": "3s",
348
+ "hits": 1,
349
+ },
350
+ )
351
+ assert response.is_successful(), response.json
352
+ return True
frontend/app.py CHANGED
@@ -62,6 +62,28 @@ image_swapping = Script(
62
  """
63
  )
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  def SearchBox(with_border=False, query_value="", ranking_value="nn+colpali"):
67
  grid_cls = "grid gap-2 items-center p-3 bg-muted/80 dark:bg-muted/40 w-full"
@@ -131,13 +153,11 @@ def SearchBox(with_border=False, query_value="", ranking_value="nn+colpali"):
131
 
132
  def SampleQueries():
133
  sample_queries = [
134
- "Proportion of female new hires 2021-2023?",
135
  "Total amount of fixed salaries paid in 2023?",
136
- "What is the percentage distribution of employees with performance-based pay relative to the limit in 2023?",
137
- "What is the breakdown of management costs by investment strategy in 2023?",
138
- "2023 profit loss portfolio",
139
- "net cash flow operating activities",
140
- "fund currency basket returns",
141
  ]
142
 
143
  query_badges = []
@@ -167,13 +187,13 @@ def Hero():
167
  return Div(
168
  H1(
169
  "Vespa.ai + ColPali",
170
- cls="text-4xl md:text-7xl font-bold tracking-wide md:tracking-wider bg-clip-text text-transparent bg-gradient-to-r from-black to-gray-700 dark:from-white dark:to-gray-300 animate-fade-in",
171
  ),
172
  P(
173
  "Efficient Document Retrieval with Vision Language Models",
174
  cls="text-lg md:text-2xl text-muted-foreground md:tracking-wide",
175
  ),
176
- cls="grid gap-5 text-center pt-5",
177
  )
178
 
179
 
@@ -183,7 +203,7 @@ def Home():
183
  Hero(),
184
  SearchBox(with_border=True),
185
  SampleQueries(),
186
- cls="grid gap-8 md:-mt-[34vh]", # Negative margin only on medium and larger screens
187
  ),
188
  cls="grid w-full h-full max-w-screen-md items-center gap-4 mx-auto",
189
  )
@@ -219,6 +239,15 @@ def LoadingMessage(display_text="Retrieving search results"):
219
  )
220
 
221
 
 
 
 
 
 
 
 
 
 
222
  def SimMapButtonReady(query_id, idx, token, img_src):
223
  return Button(
224
  token,
@@ -310,76 +339,107 @@ def SearchResult(results: list, query_id: Optional[str] = None):
310
  Div(
311
  Div(
312
  Div(
313
- tokens_button,
314
- *sim_map_buttons,
315
- reset_button,
316
- cls="flex flex-wrap gap-px w-full pointer-events-none",
317
  ),
318
  Div(
319
- Div(
320
- Img(
321
- src=blur_image_base64,
322
- hx_get=f"/full_image?docid={fields['id']}&query_id={query_id}&idx={idx}",
323
- style="filter: blur(5px);",
324
- hx_trigger="load",
325
- hx_swap="outerHTML",
326
- alt=fields["title"],
327
- cls="result-image w-full h-full object-contain",
328
- ),
329
- Div(
330
- cls="overlay-container absolute top-0 left-0 w-full h-full pointer-events-none"
331
- ),
332
- cls="relative w-full h-full",
333
  ),
334
- cls="grid bg-border p-2",
335
  ),
336
- cls="relative grid content-start bg-background px-3 py-5",
337
  ),
338
  Div(
339
  Div(
340
- H2(fields["title"], cls="text-xl font-semibold"),
341
- P(
342
- "Page " + str(fields["page_number"]),
343
- cls="text-foreground font-mono bold",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  ),
 
 
 
 
345
  Div(
 
 
 
 
346
  Badge(
347
  f"Relevance score: {result['relevance']:.4f}",
348
  cls="flex gap-1.5 items-center justify-center",
349
  ),
 
350
  ),
351
- P(
352
- NotStr(fields.get("snippet", "")),
353
- cls="text-highlight text-muted-foreground",
354
- ),
355
- P(
356
- NotStr(fields.get("text", "")),
357
- cls="text-highlight text-muted-foreground",
 
 
 
 
 
 
 
 
 
358
  ),
359
- cls="text-sm grid gap-y-4",
 
360
  ),
361
- cls="bg-background px-3 py-5 hidden md:block",
 
362
  ),
363
- cls="grid grid-cols-1 md:grid-cols-2 col-span-2 border-t",
364
- )
365
  )
366
 
367
  return Div(
368
  *result_items,
369
  image_swapping,
 
370
  id="search-results",
371
- cls="grid grid-cols-2 gap-px bg-border",
372
  )
373
 
374
 
375
  def ChatResult(query_id: str, query: str):
376
  return Div(
377
- Div("Chat", cls="text-xl font-semibold p-3"),
378
  Div(
379
  Div(
380
  Div(
381
- LoadingMessage(display_text="Waiting for response..."),
382
- cls="bg-muted/80 dark:bg-muted/40 text-black dark:text-white p-2 rounded-md",
383
  hx_ext="sse",
384
  sse_connect=f"/get-message?query_id={query_id}&query={quote_plus(query)}",
385
  sse_swap="message",
 
62
  """
63
  )
64
 
65
+ toggle_text_content = Script(
66
+ """
67
+ function toggleTextContent(idx) {
68
+ const textColumn = document.getElementById(`text-column-${idx}`);
69
+ const imageTextColumns = document.getElementById(`image-text-columns-${idx}`);
70
+ const toggleButton = document.getElementById(`toggle-button-${idx}`);
71
+
72
+ if (textColumn.classList.contains('md-grid-text-column')) {
73
+ // Hide the text column
74
+ textColumn.classList.remove('md-grid-text-column');
75
+ imageTextColumns.classList.remove('grid-image-text-columns');
76
+ toggleButton.innerText = `Show Text`;
77
+ } else {
78
+ // Show the text column
79
+ textColumn.classList.add('md-grid-text-column');
80
+ imageTextColumns.classList.add('grid-image-text-columns');
81
+ toggleButton.innerText = `Hide Text`;
82
+ }
83
+ }
84
+ """
85
+ )
86
+
87
 
88
  def SearchBox(with_border=False, query_value="", ranking_value="nn+colpali"):
89
  grid_cls = "grid gap-2 items-center p-3 bg-muted/80 dark:bg-muted/40 w-full"
 
153
 
154
  def SampleQueries():
155
  sample_queries = [
 
156
  "Total amount of fixed salaries paid in 2023?",
157
+ "Proportion of female new hires 2021-2023?",
158
+ "Value of unlisted real estate 2023?",
159
+ "Fund currency basket returns 2023",
160
+ "Employees per office site?",
 
161
  ]
162
 
163
  query_badges = []
 
187
  return Div(
188
  H1(
189
  "Vespa.ai + ColPali",
190
+ cls="text-5xl md:text-7xl font-bold tracking-wide md:tracking-wider bg-clip-text text-transparent bg-gradient-to-r from-black to-gray-700 dark:from-white dark:to-gray-300 animate-fade-in",
191
  ),
192
  P(
193
  "Efficient Document Retrieval with Vision Language Models",
194
  cls="text-lg md:text-2xl text-muted-foreground md:tracking-wide",
195
  ),
196
+ cls="grid gap-5 text-center",
197
  )
198
 
199
 
 
203
  Hero(),
204
  SearchBox(with_border=True),
205
  SampleQueries(),
206
+ cls="grid gap-8 -mt-[21vh]",
207
  ),
208
  cls="grid w-full h-full max-w-screen-md items-center gap-4 mx-auto",
209
  )
 
239
  )
240
 
241
 
242
+ def LoadingSkeleton():
243
+ return Div(
244
+ Div(cls="h-5 bg-muted"),
245
+ Div(cls="h-5 bg-muted"),
246
+ Div(cls="h-5 bg-muted"),
247
+ cls="grid gap-2 animate-pulse",
248
+ )
249
+
250
+
251
  def SimMapButtonReady(query_id, idx, token, img_src):
252
  return Button(
253
  token,
 
339
  Div(
340
  Div(
341
  Div(
342
+ Lucide(icon="file-text"),
343
+ H2(fields["title"], cls="text-xl md:text-2xl font-semibold"),
344
+ cls="flex items-center gap-2",
 
345
  ),
346
  Div(
347
+ Button(
348
+ "Show Text",
349
+ size="sm",
350
+ id=f"toggle-button-{idx}",
351
+ onclick=f"toggleTextContent({idx})",
 
 
 
 
 
 
 
 
 
352
  ),
 
353
  ),
354
+ cls="flex flex-wrap items-center justify-between bg-background px-3 py-4",
355
  ),
356
  Div(
357
  Div(
358
+ Div(
359
+ tokens_button,
360
+ *sim_map_buttons,
361
+ reset_button,
362
+ cls="flex flex-wrap gap-px w-full pointer-events-none",
363
+ ),
364
+ Div(
365
+ Div(
366
+ Div(
367
+ Img(
368
+ src=blur_image_base64,
369
+ hx_get=f"/full_image?docid={fields['id']}&query_id={query_id}&idx={idx}",
370
+ style="backdrop-filter: blur(5px);",
371
+ hx_trigger="load",
372
+ hx_swap="outerHTML",
373
+ alt=fields["title"],
374
+ cls="result-image w-full h-full object-contain",
375
+ ),
376
+ Div(
377
+ cls="overlay-container absolute top-0 left-0 w-full h-full pointer-events-none"
378
+ ),
379
+ cls="relative w-full h-full",
380
+ ),
381
+ cls="grid bg-border p-2",
382
+ ),
383
+ cls="block",
384
  ),
385
+ id=f"image-column-{idx}",
386
+ cls="image-column relative bg-background px-3 py-5 grid-image-column",
387
+ ),
388
+ Div(
389
  Div(
390
+ P(
391
+ "Page " + str(fields["page_number"]),
392
+ cls="text-foreground font-mono bold text-sm",
393
+ ),
394
  Badge(
395
  f"Relevance score: {result['relevance']:.4f}",
396
  cls="flex gap-1.5 items-center justify-center",
397
  ),
398
+ cls="flex items-center justify-between",
399
  ),
400
+ Div(
401
+ Div(
402
+ Div(
403
+ P(
404
+ NotStr(fields.get("snippet", "")),
405
+ cls="text-highlight text-muted-foreground",
406
+ ),
407
+ P(
408
+ NotStr(fields.get("text", "")),
409
+ cls="text-highlight text-muted-foreground",
410
+ ),
411
+ cls="grid gap-y-3 p-5 text-sm",
412
+ ),
413
+ cls="grid bg-background content-start ",
414
+ ),
415
+ cls="grid bg-border p-2",
416
  ),
417
+ id=f"text-column-{idx}",
418
+ cls="text-column relative bg-background px-3 py-5 hidden",
419
  ),
420
+ id=f"image-text-columns-{idx}",
421
+ cls="relative grid grid-cols-1 border-t",
422
  ),
423
+ cls="grid grid-cols-1 grid-rows-[auto_1fr]",
424
+ ),
425
  )
426
 
427
  return Div(
428
  *result_items,
429
  image_swapping,
430
+ toggle_text_content,
431
  id="search-results",
432
+ cls="grid grid-cols-1 gap-px bg-border",
433
  )
434
 
435
 
436
  def ChatResult(query_id: str, query: str):
437
  return Div(
438
+ Div("LLM Response", cls="text-xl font-semibold p-3"),
439
  Div(
440
  Div(
441
  Div(
442
+ LoadingSkeleton(),
 
443
  hx_ext="sse",
444
  sse_connect=f"/get-message?query_id={query_id}&query={quote_plus(query)}",
445
  sse_swap="message",
frontend/layout.py CHANGED
@@ -3,7 +3,7 @@ from fasthtml.xtend import A, Script
3
  from lucide_fasthtml import Lucide
4
  from shad4fast import Button, Separator
5
 
6
- script = Script(
7
  """
8
  document.addEventListener("DOMContentLoaded", function () {
9
  const main = document.querySelector('main');
@@ -11,12 +11,12 @@ script = Script(
11
  const body = document.body;
12
 
13
  if (main && aside && main.nextElementSibling === aside) {
14
- // Main + Aside layout
15
- body.classList.add('grid-cols-[minmax(0,_4fr)_minmax(0,_1fr)]');
16
- aside.classList.remove('hidden');
17
  } else if (main) {
18
- // Only Main layout (full width)
19
- body.classList.add('grid-cols-[1fr]');
20
  }
21
  });
22
  """
@@ -136,6 +136,6 @@ def Layout(*c, **kwargs):
136
  **kwargs,
137
  cls="grid grid-rows-[55px_1fr] min-h-0",
138
  ),
139
- script,
140
  overlay_scrollbars,
141
  )
 
3
  from lucide_fasthtml import Lucide
4
  from shad4fast import Button, Separator
5
 
6
+ layout_script = Script(
7
  """
8
  document.addEventListener("DOMContentLoaded", function () {
9
  const main = document.querySelector('main');
 
11
  const body = document.body;
12
 
13
  if (main && aside && main.nextElementSibling === aside) {
14
+ // If we have both main and aside, adjust the layout for larger screens
15
+ body.classList.remove('grid-cols-1'); // Remove single-column layout
16
+ body.classList.add('md:grid-cols-[minmax(0,_45fr)_minmax(0,_15fr)]'); // Two-column layout on larger screens
17
  } else if (main) {
18
+ // If only main, keep it full width
19
+ body.classList.add('grid-cols-1');
20
  }
21
  });
22
  """
 
136
  **kwargs,
137
  cls="grid grid-rows-[55px_1fr] min-h-0",
138
  ),
139
+ layout_script,
140
  overlay_scrollbars,
141
  )
globals.css CHANGED
@@ -205,3 +205,15 @@ aside {
205
  background-color: #61D790;
206
  color: #2E2F27;
207
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  background-color: #61D790;
206
  color: #2E2F27;
207
  }
208
+
209
+ .grid-image-text-columns {
210
+ @apply md:grid-cols-2 md:col-span-2
211
+ }
212
+
213
+ .grid-image-column {
214
+ @apply grid grid-rows-subgrid row-span-2 content-start;
215
+ }
216
+
217
+ .md-grid-text-column {
218
+ @apply md:grid md:grid-rows-subgrid md:row-span-2 md:content-start;
219
+ }
icons.py CHANGED
@@ -1 +1 @@
1
- ICONS = {"chevrons-right": "<path d=\"m6 17 5-5-5-5\"></path><path d=\"m13 17 5-5-5-5\"></path>", "moon": "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path>", "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\"></circle><path d=\"M12 2v2\"></path><path d=\"M12 20v2\"></path><path d=\"m4.93 4.93 1.41 1.41\"></path><path d=\"m17.66 17.66 1.41 1.41\"></path><path d=\"M2 12h2\"></path><path d=\"M20 12h2\"></path><path d=\"m6.34 17.66-1.41 1.41\"></path><path d=\"m19.07 4.93-1.41 1.41\"></path>", "github": "<path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"></path><path d=\"M9 18c-4.51 2-5-2-7-2\"></path>", "slack": "<rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"13\" y=\"2\"></rect><path d=\"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5\"></path><rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"8\" y=\"14\"></rect><path d=\"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"14\" y=\"13\"></rect><path d=\"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"2\" y=\"8\"></rect><path d=\"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5\"></path>", "settings": "<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle>", "arrow-right": "<path d=\"M5 12h14\"></path><path d=\"m12 5 7 7-7 7\"></path>", "search": "<circle cx=\"11\" cy=\"11\" r=\"8\"></circle><path d=\"m21 21-4.3-4.3\"></path>", "file-search": "<path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3\"></path><path d=\"m9 18-1.5-1.5\"></path><circle cx=\"5\" cy=\"14\" r=\"3\"></circle>", "message-circle-question": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><path d=\"M12 17h.01\"></path>", "text-search": "<path d=\"M21 6H3\"></path><path d=\"M10 12H3\"></path><path d=\"M10 18H3\"></path><circle cx=\"17\" cy=\"15\" r=\"3\"></circle><path d=\"m21 19-1.9-1.9\"></path>", "maximize": "<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"></path><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"></path><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"></path>", "expand": "<path d=\"m21 21-6-6m6 6v-4.8m0 4.8h-4.8\"></path><path d=\"M3 16.2V21m0 0h4.8M3 21l6-6\"></path><path d=\"M21 7.8V3m0 0h-4.8M21 3l-6 6\"></path><path d=\"M3 7.8V3m0 0h4.8M3 3l6 6\"></path>", "fullscreen": "<path d=\"M3 7V5a2 2 0 0 1 2-2h2\"></path><path d=\"M17 3h2a2 2 0 0 1 2 2v2\"></path><path d=\"M21 17v2a2 2 0 0 1-2 2h-2\"></path><path d=\"M7 21H5a2 2 0 0 1-2-2v-2\"></path><rect height=\"8\" rx=\"1\" width=\"10\" x=\"7\" y=\"8\"></rect>", "images": "<path d=\"M18 22H4a2 2 0 0 1-2-2V6\"></path><path d=\"m22 13-1.296-1.296a2.41 2.41 0 0 0-3.408 0L11 18\"></path><circle cx=\"12\" cy=\"8\" r=\"2\"></circle><rect height=\"16\" rx=\"2\" width=\"16\" x=\"6\" y=\"2\"></rect>", "circle": "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>", "loader-circle": "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path>"}
 
1
+ ICONS = {"chevrons-right": "<path d=\"m6 17 5-5-5-5\"></path><path d=\"m13 17 5-5-5-5\"></path>", "moon": "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path>", "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\"></circle><path d=\"M12 2v2\"></path><path d=\"M12 20v2\"></path><path d=\"m4.93 4.93 1.41 1.41\"></path><path d=\"m17.66 17.66 1.41 1.41\"></path><path d=\"M2 12h2\"></path><path d=\"M20 12h2\"></path><path d=\"m6.34 17.66-1.41 1.41\"></path><path d=\"m19.07 4.93-1.41 1.41\"></path>", "github": "<path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"></path><path d=\"M9 18c-4.51 2-5-2-7-2\"></path>", "slack": "<rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"13\" y=\"2\"></rect><path d=\"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5\"></path><rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"8\" y=\"14\"></rect><path d=\"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"14\" y=\"13\"></rect><path d=\"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"2\" y=\"8\"></rect><path d=\"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5\"></path>", "settings": "<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle>", "arrow-right": "<path d=\"M5 12h14\"></path><path d=\"m12 5 7 7-7 7\"></path>", "search": "<circle cx=\"11\" cy=\"11\" r=\"8\"></circle><path d=\"m21 21-4.3-4.3\"></path>", "file-search": "<path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3\"></path><path d=\"m9 18-1.5-1.5\"></path><circle cx=\"5\" cy=\"14\" r=\"3\"></circle>", "message-circle-question": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><path d=\"M12 17h.01\"></path>", "text-search": "<path d=\"M21 6H3\"></path><path d=\"M10 12H3\"></path><path d=\"M10 18H3\"></path><circle cx=\"17\" cy=\"15\" r=\"3\"></circle><path d=\"m21 19-1.9-1.9\"></path>", "maximize": "<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"></path><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"></path><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"></path>", "expand": "<path d=\"m21 21-6-6m6 6v-4.8m0 4.8h-4.8\"></path><path d=\"M3 16.2V21m0 0h4.8M3 21l6-6\"></path><path d=\"M21 7.8V3m0 0h-4.8M21 3l-6 6\"></path><path d=\"M3 7.8V3m0 0h4.8M3 3l6 6\"></path>", "fullscreen": "<path d=\"M3 7V5a2 2 0 0 1 2-2h2\"></path><path d=\"M17 3h2a2 2 0 0 1 2 2v2\"></path><path d=\"M21 17v2a2 2 0 0 1-2 2h-2\"></path><path d=\"M7 21H5a2 2 0 0 1-2-2v-2\"></path><rect height=\"8\" rx=\"1\" width=\"10\" x=\"7\" y=\"8\"></rect>", "images": "<path d=\"M18 22H4a2 2 0 0 1-2-2V6\"></path><path d=\"m22 13-1.296-1.296a2.41 2.41 0 0 0-3.408 0L11 18\"></path><circle cx=\"12\" cy=\"8\" r=\"2\"></circle><rect height=\"16\" rx=\"2\" width=\"16\" x=\"6\" y=\"2\"></rect>", "circle": "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>", "loader-circle": "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path>", "file-text": "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"></path><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M10 9H8\"></path><path d=\"M16 13H8\"></path><path d=\"M16 17H8\"></path>"}
main.py CHANGED
@@ -1,11 +1,15 @@
1
  import asyncio
 
2
  import hashlib
 
 
3
  import time
4
  from concurrent.futures import ThreadPoolExecutor
5
  from functools import partial
6
- import os
7
 
 
8
  from fasthtml.common import *
 
9
  from shad4fast import *
10
  from vespa.application import Vespa
11
 
@@ -27,10 +31,6 @@ from frontend.app import (
27
  SimMapButtonReady,
28
  )
29
  from frontend.layout import Layout
30
- import google.generativeai as genai
31
- from PIL import Image
32
- import io
33
- import base64
34
 
35
  highlight_js_theme_link = Link(id="highlight-theme", rel="stylesheet", href="")
36
  highlight_js_theme = Script(src="/static/js/highlightjs-theme.js")
@@ -73,7 +73,9 @@ thread_pool = ThreadPoolExecutor()
73
 
74
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
75
  GEMINI_SYSTEM_PROMPT = """If the user query is a question, try your best to answer it based on the provided images.
76
- If the user query is not an obvious question, reply with 'No question detected.'. Your response should be HTML formatted.
 
 
77
  This means that newlines will be replaced with <br> tags, bold text will be enclosed in <b> tags, and so on.
78
  But, you should NOT include backticks (`) or HTML tags in your response.
79
  """
@@ -88,6 +90,12 @@ def load_model_on_startup():
88
  return
89
 
90
 
 
 
 
 
 
 
91
  def generate_query_id(query):
92
  return hashlib.md5(query.encode("utf-8")).hexdigest()
93
 
@@ -139,7 +147,8 @@ def get(request):
139
  return Layout(
140
  Main(Search(request), data_overlayscrollbars_initialize=True, cls="border-t"),
141
  Aside(
142
- ChatResult(query_id=query_id, query=query_value), cls="border-t border-l"
 
143
  ),
144
  ) # Show SearchBox and Loading message initially
145
 
@@ -207,6 +216,13 @@ def get_results_children(result):
207
  return search_results
208
 
209
 
 
 
 
 
 
 
 
210
  async def generate_similarity_map(
211
  model, processor, query, q_embs, token_to_idx, result, query_id
212
  ):
@@ -290,7 +306,9 @@ async def message_generator(query_id: str, query: str):
290
  images = []
291
  result = None
292
  all_images_ready = False
293
- while not all_images_ready:
 
 
294
  result = result_cache.get(query_id)
295
  if result is None:
296
  await asyncio.sleep(0.1)
@@ -308,6 +326,10 @@ async def message_generator(query_id: str, query: str):
308
 
309
  # from b64 to PIL image
310
  images = [Image.open(io.BytesIO(base64.b64decode(img))) for img in images]
 
 
 
 
311
 
312
  # If newlines are present in the response, the connection will be closed.
313
  def replace_newline_with_br(text):
@@ -321,7 +343,7 @@ async def message_generator(query_id: str, query: str):
321
  response_text += chunk.text
322
  response_text = replace_newline_with_br(response_text)
323
  yield f"event: message\ndata: {response_text}\n\n"
324
- await asyncio.sleep(0.5)
325
  yield "event: close\ndata: \n\n"
326
 
327
 
 
1
  import asyncio
2
+ import base64
3
  import hashlib
4
+ import io
5
+ import os
6
  import time
7
  from concurrent.futures import ThreadPoolExecutor
8
  from functools import partial
 
9
 
10
+ import google.generativeai as genai
11
  from fasthtml.common import *
12
+ from PIL import Image
13
  from shad4fast import *
14
  from vespa.application import Vespa
15
 
 
31
  SimMapButtonReady,
32
  )
33
  from frontend.layout import Layout
 
 
 
 
34
 
35
  highlight_js_theme_link = Link(id="highlight-theme", rel="stylesheet", href="")
36
  highlight_js_theme = Script(src="/static/js/highlightjs-theme.js")
 
73
 
74
  genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
75
  GEMINI_SYSTEM_PROMPT = """If the user query is a question, try your best to answer it based on the provided images.
76
+ If the user query can not be interpreted as a question, or if the answer to the query can not be inferred from the images,
77
+ answer with the exact phrase "I am sorry, I do not have enough information in the image to answer your question.".
78
+ Your response should be HTML formatted, but only simple tags, such as <b>. <p>, <i>, <br> <ul> and <li> are allowed. No HTML tables.
79
  This means that newlines will be replaced with <br> tags, bold text will be enclosed in <b> tags, and so on.
80
  But, you should NOT include backticks (`) or HTML tags in your response.
81
  """
 
90
  return
91
 
92
 
93
+ @app.on_event("startup")
94
+ async def keepalive():
95
+ asyncio.create_task(poll_vespa_keepalive())
96
+ return
97
+
98
+
99
  def generate_query_id(query):
100
  return hashlib.md5(query.encode("utf-8")).hexdigest()
101
 
 
147
  return Layout(
148
  Main(Search(request), data_overlayscrollbars_initialize=True, cls="border-t"),
149
  Aside(
150
+ ChatResult(query_id=query_id, query=query_value),
151
+ cls="border-t border-l hidden md:block",
152
  ),
153
  ) # Show SearchBox and Loading message initially
154
 
 
216
  return search_results
217
 
218
 
219
+ async def poll_vespa_keepalive():
220
+ while True:
221
+ await asyncio.sleep(5)
222
+ await vespa_app.keepalive()
223
+ print(f"Vespa keepalive: {time.time()}")
224
+
225
+
226
  async def generate_similarity_map(
227
  model, processor, query, q_embs, token_to_idx, result, query_id
228
  ):
 
306
  images = []
307
  result = None
308
  all_images_ready = False
309
+ max_wait = 10 # seconds
310
+ start_time = time.time()
311
+ while not all_images_ready and time.time() - start_time < max_wait:
312
  result = result_cache.get(query_id)
313
  if result is None:
314
  await asyncio.sleep(0.1)
 
326
 
327
  # from b64 to PIL image
328
  images = [Image.open(io.BytesIO(base64.b64decode(img))) for img in images]
329
+ if not images:
330
+ yield "event: message\ndata: I am sorry, I do not have enough information in the image to answer your question.\n\n"
331
+ yield "event: close\ndata: \n\n"
332
+ return
333
 
334
  # If newlines are present in the response, the connection will be closed.
335
  def replace_newline_with_br(text):
 
343
  response_text += chunk.text
344
  response_text = replace_newline_with_br(response_text)
345
  yield f"event: message\ndata: {response_text}\n\n"
346
+ await asyncio.sleep(0.1)
347
  yield "event: close\ndata: \n\n"
348
 
349
 
output.css CHANGED
@@ -766,10 +766,6 @@ body {
766
  z-index: 100;
767
  }
768
 
769
- .col-span-2 {
770
- grid-column: span 2 / span 2;
771
- }
772
-
773
  .-mx-1 {
774
  margin-left: -0.25rem;
775
  margin-right: -0.25rem;
@@ -793,8 +789,8 @@ body {
793
  margin-top: -1rem;
794
  }
795
 
796
- .-mt-\[34vh\] {
797
- margin-top: -34vh;
798
  }
799
 
800
  .mb-1 {
@@ -1056,6 +1052,16 @@ body {
1056
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1057
  }
1058
 
 
 
 
 
 
 
 
 
 
 
1059
  @keyframes spin {
1060
  to {
1061
  transform: rotate(360deg);
@@ -1096,22 +1102,14 @@ body {
1096
  grid-template-columns: repeat(1, minmax(0, 1fr));
1097
  }
1098
 
1099
- .grid-cols-2 {
1100
- grid-template-columns: repeat(2, minmax(0, 1fr));
1101
- }
1102
-
1103
- .grid-cols-\[1fr\] {
1104
- grid-template-columns: 1fr;
1105
- }
1106
-
1107
- .grid-cols-\[minmax\(0\2c _4fr\)_minmax\(0\2c _1fr\)\] {
1108
- grid-template-columns: minmax(0, 4fr) minmax(0, 1fr);
1109
- }
1110
-
1111
  .grid-rows-\[55px_1fr\] {
1112
  grid-template-rows: 55px 1fr;
1113
  }
1114
 
 
 
 
 
1115
  .grid-rows-\[auto_1fr_auto\] {
1116
  grid-template-rows: auto 1fr auto;
1117
  }
@@ -1140,10 +1138,6 @@ body {
1140
  align-items: center;
1141
  }
1142
 
1143
- .justify-end {
1144
- justify-content: flex-end;
1145
- }
1146
-
1147
  .justify-center {
1148
  justify-content: center;
1149
  }
@@ -1198,8 +1192,8 @@ body {
1198
  column-gap: 1.25rem;
1199
  }
1200
 
1201
- .gap-y-4 {
1202
- row-gap: 1rem;
1203
  }
1204
 
1205
  .space-x-2 > :not([hidden]) ~ :not([hidden]) {
@@ -1232,10 +1226,6 @@ body {
1232
  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
1233
  }
1234
 
1235
- .self-end {
1236
- align-self: flex-end;
1237
- }
1238
-
1239
  .self-stretch {
1240
  align-self: stretch;
1241
  }
@@ -1288,11 +1278,6 @@ body {
1288
  border-width: 2px;
1289
  }
1290
 
1291
- .border-x {
1292
- border-left-width: 1px;
1293
- border-right-width: 1px;
1294
- }
1295
-
1296
  .border-b {
1297
  border-bottom-width: 1px;
1298
  }
@@ -1448,6 +1433,10 @@ body {
1448
  padding: 1rem;
1449
  }
1450
 
 
 
 
 
1451
  .p-6 {
1452
  padding: 1.5rem;
1453
  }
@@ -1501,6 +1490,11 @@ body {
1501
  padding-bottom: 0.5rem;
1502
  }
1503
 
 
 
 
 
 
1504
  .py-5 {
1505
  padding-top: 1.25rem;
1506
  padding-bottom: 1.25rem;
@@ -1534,10 +1528,6 @@ body {
1534
  padding-top: 1rem;
1535
  }
1536
 
1537
- .pr-3 {
1538
- padding-right: 0.75rem;
1539
- }
1540
-
1541
  .text-left {
1542
  text-align: left;
1543
  }
@@ -1622,11 +1612,6 @@ body {
1622
  letter-spacing: 0.025em;
1623
  }
1624
 
1625
- .text-black {
1626
- --tw-text-opacity: 1;
1627
- color: rgb(0 0 0 / var(--tw-text-opacity));
1628
- }
1629
-
1630
  .text-card-foreground {
1631
  color: hsl(var(--card-foreground));
1632
  }
@@ -1770,6 +1755,11 @@ body {
1770
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1771
  }
1772
 
 
 
 
 
 
1773
  .transition {
1774
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1775
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@@ -2064,6 +2054,29 @@ aside {
2064
  color: #2E2F27;
2065
  }
2066
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2067
  :root:has(.data-\[state\=open\]\:no-bg-scroll[data-state="open"]) {
2068
  overflow: hidden;
2069
  }
@@ -2555,8 +2568,8 @@ aside {
2555
  max-width: 420px;
2556
  }
2557
 
2558
- .md\:grid-cols-2 {
2559
- grid-template-columns: repeat(2, minmax(0, 1fr));
2560
  }
2561
 
2562
  .md\:text-2xl {
@@ -2608,11 +2621,6 @@ aside {
2608
  --tw-gradient-to: #d1d5db var(--tw-gradient-to-position);
2609
  }
2610
 
2611
- .dark\:text-white:where(.dark, .dark *) {
2612
- --tw-text-opacity: 1;
2613
- color: rgb(255 255 255 / var(--tw-text-opacity));
2614
- }
2615
-
2616
  .dark\:hover\:border-white:hover:where(.dark, .dark *) {
2617
  --tw-border-opacity: 1;
2618
  border-color: rgb(255 255 255 / var(--tw-border-opacity));
 
766
  z-index: 100;
767
  }
768
 
 
 
 
 
769
  .-mx-1 {
770
  margin-left: -0.25rem;
771
  margin-right: -0.25rem;
 
789
  margin-top: -1rem;
790
  }
791
 
792
+ .-mt-\[21vh\] {
793
+ margin-top: -21vh;
794
  }
795
 
796
  .mb-1 {
 
1052
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1053
  }
1054
 
1055
+ @keyframes pulse {
1056
+ 50% {
1057
+ opacity: .5;
1058
+ }
1059
+ }
1060
+
1061
+ .animate-pulse {
1062
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1063
+ }
1064
+
1065
  @keyframes spin {
1066
  to {
1067
  transform: rotate(360deg);
 
1102
  grid-template-columns: repeat(1, minmax(0, 1fr));
1103
  }
1104
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  .grid-rows-\[55px_1fr\] {
1106
  grid-template-rows: 55px 1fr;
1107
  }
1108
 
1109
+ .grid-rows-\[auto_1fr\] {
1110
+ grid-template-rows: auto 1fr;
1111
+ }
1112
+
1113
  .grid-rows-\[auto_1fr_auto\] {
1114
  grid-template-rows: auto 1fr auto;
1115
  }
 
1138
  align-items: center;
1139
  }
1140
 
 
 
 
 
1141
  .justify-center {
1142
  justify-content: center;
1143
  }
 
1192
  column-gap: 1.25rem;
1193
  }
1194
 
1195
+ .gap-y-3 {
1196
+ row-gap: 0.75rem;
1197
  }
1198
 
1199
  .space-x-2 > :not([hidden]) ~ :not([hidden]) {
 
1226
  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
1227
  }
1228
 
 
 
 
 
1229
  .self-stretch {
1230
  align-self: stretch;
1231
  }
 
1278
  border-width: 2px;
1279
  }
1280
 
 
 
 
 
 
1281
  .border-b {
1282
  border-bottom-width: 1px;
1283
  }
 
1433
  padding: 1rem;
1434
  }
1435
 
1436
+ .p-5 {
1437
+ padding: 1.25rem;
1438
+ }
1439
+
1440
  .p-6 {
1441
  padding: 1.5rem;
1442
  }
 
1490
  padding-bottom: 0.5rem;
1491
  }
1492
 
1493
+ .py-4 {
1494
+ padding-top: 1rem;
1495
+ padding-bottom: 1rem;
1496
+ }
1497
+
1498
  .py-5 {
1499
  padding-top: 1.25rem;
1500
  padding-bottom: 1.25rem;
 
1528
  padding-top: 1rem;
1529
  }
1530
 
 
 
 
 
1531
  .text-left {
1532
  text-align: left;
1533
  }
 
1612
  letter-spacing: 0.025em;
1613
  }
1614
 
 
 
 
 
 
1615
  .text-card-foreground {
1616
  color: hsl(var(--card-foreground));
1617
  }
 
1755
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1756
  }
1757
 
1758
+ .backdrop-filter {
1759
+ -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1760
+ backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1761
+ }
1762
+
1763
  .transition {
1764
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1765
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
 
2054
  color: #2E2F27;
2055
  }
2056
 
2057
+ @media (min-width: 768px) {
2058
+ .grid-image-text-columns {
2059
+ grid-column: span 2 / span 2;
2060
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2061
+ }
2062
+ }
2063
+
2064
+ .grid-image-column {
2065
+ grid-row: span 2 / span 2;
2066
+ display: grid;
2067
+ grid-template-rows: subgrid;
2068
+ align-content: flex-start;
2069
+ }
2070
+
2071
+ @media (min-width: 768px) {
2072
+ .md-grid-text-column {
2073
+ grid-row: span 2 / span 2;
2074
+ display: grid;
2075
+ grid-template-rows: subgrid;
2076
+ align-content: flex-start;
2077
+ }
2078
+ }
2079
+
2080
  :root:has(.data-\[state\=open\]\:no-bg-scroll[data-state="open"]) {
2081
  overflow: hidden;
2082
  }
 
2568
  max-width: 420px;
2569
  }
2570
 
2571
+ .md\:grid-cols-\[minmax\(0\2c _45fr\)_minmax\(0\2c _15fr\)\] {
2572
+ grid-template-columns: minmax(0, 45fr) minmax(0, 15fr);
2573
  }
2574
 
2575
  .md\:text-2xl {
 
2621
  --tw-gradient-to: #d1d5db var(--tw-gradient-to-position);
2622
  }
2623
 
 
 
 
 
 
2624
  .dark\:hover\:border-white:hover:where(.dark, .dark *) {
2625
  --tw-border-opacity: 1;
2626
  border-color: rgb(255 255 255 / var(--tw-border-opacity));