thomasht86 commited on
Commit
8ce4d25
β€’
1 Parent(s): 947aa12

Upload folder using huggingface_hub

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ VESPA_APP_URL=https://abcde.z.vespa-app.cloud
2
+ HF_TOKEN=hf_xxxxxxxxxx
3
+ VESPA_CLOUD_SECRET_TOKEN=vespa_cloud_xxxxxxxx
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tailwindcss filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .sesskey
2
+ .venv/
3
+ __pycache__/
4
+ .python-version
5
+ .env
6
+ template/
7
+ *.json
8
+ output/
README.md CHANGED
@@ -1,14 +1,109 @@
1
  ---
2
- title: Colpali Vespa Visual Retrieval
3
- emoji: πŸ‘
4
- colorFrom: red
5
- colorTo: red
 
6
  sdk: gradio
7
- sdk_version: 4.44.1
8
- app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
- short_description: ColPali 🀝 Vespa - Visual Retrieval
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ColPali 🀝 Vespa - Visual Retrieval
3
+ short_description: Visual Retrieval with ColPali and Vespa
4
+ emoji: πŸ‘€
5
+ colorFrom: purple
6
+ colorTo: blue
7
  sdk: gradio
8
+ sdk_version: 4.44.0
9
+ app_file: main.py
10
  pinned: false
11
  license: apache-2.0
12
+ models:
13
+ - vidore/colpaligemma-3b-pt-448-base
14
+ - vidore/colpali-v1.2
15
+ preload_from_hub:
16
+ - vidore/colpaligemma-3b-pt-448-base config.json,model-00001-of-00002.safetensors,model-00002-of-00002.safetensors,model.safetensors.index.json,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 12c59eb7e23bc4c26876f7be7c17760d5d3a1ffa
17
+ - vidore/colpali-v1.2 adapter_config.json,adapter_model.safetensors,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 9912ce6f8a462d8cf2269f5606eabbd2784e764f
18
  ---
19
 
20
+ <!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
21
+
22
+ <picture>
23
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.vespa.ai/logos/Vespa-logo-green-RGB.svg">
24
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.vespa.ai/logos/Vespa-logo-dark-RGB.svg">
25
+ <img alt="#Vespa" width="200" src="https://assets.vespa.ai/logos/Vespa-logo-dark-RGB.svg" style="margin-bottom: 25px;">
26
+ </picture>
27
+
28
+ # Visual Retrieval ColPali
29
+
30
+
31
+ # Developing
32
+
33
+ First, install `uv`:
34
+
35
+ ```bash
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh
37
+ ```
38
+
39
+ Then, in this directory, run:
40
+
41
+ ```bash
42
+ uv sync --extra dev
43
+ ```
44
+
45
+ This will generate a virtual environment with the required dependencies at `.venv`.
46
+
47
+ To activate the virtual environment, run:
48
+
49
+ ```bash
50
+ source .venv/bin/activate
51
+ ```
52
+
53
+ And run development server:
54
+
55
+ ```bash
56
+ python hello.py
57
+ ```
58
+
59
+ ## Preparation
60
+
61
+ First, set up your `.env` file by renaming `.env.example` to `.env` and filling in the required values.
62
+ (Token can be shared with 1password, `HF_TOKEN` is personal and must be created at huggingface)
63
+ If you are just connecting to a deployed Vespa app, you can skip to [Connecting to the Vespa app](#connecting-to-the-vespa-app-and-querying).
64
+
65
+ ### Deploying the Vespa app
66
+
67
+ To deploy the Vespa app, run:
68
+
69
+ ```bash
70
+ python deploy_vespa_app.py --tenant_name mytenant --vespa_application_name myapp --token_id_write mytokenid_write --token_id_read mytokenid_read
71
+ ```
72
+
73
+ You should get an output like:
74
+
75
+ ```bash
76
+ Found token endpoint: https://abcde.z.vespa-app.cloud
77
+ ````
78
+
79
+ ### Feeding the data
80
+
81
+ #### Dependencies
82
+
83
+ In addition to the python dependencies, you also need `poppler`
84
+ On Mac:
85
+
86
+ ```bash
87
+ brew install poppler
88
+ ```
89
+
90
+ First, you need to create a huggingface token, after you have accepted the term to use the model at https://huggingface.co/google/paligemma-3b-mix-448.
91
+ Add the token to your environment variables as `HF_TOKEN`:
92
+
93
+ ```bash
94
+ export HF_TOKEN=yourtoken
95
+ ```
96
+
97
+ To feed the data, run:
98
+
99
+ ```bash
100
+ python feed_vespa.py --vespa_app_url https://myapp.z.vespa-app.cloud --vespa_cloud_secret_token mysecrettoken
101
+ ```
102
+
103
+ ### Connecting to the Vespa app and querying
104
+
105
+ As a first step, until we hook up to frontend, you can run the `query_vespa.py` script to run some sample queries against the Vespa app:
106
+
107
+ ```bash
108
+ python query_vespa.py
109
+ ```
backend/__init__.py ADDED
File without changes
backend/colpali.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import torch
4
+ from PIL import Image
5
+ import numpy as np
6
+ from typing import cast
7
+ import pprint
8
+ from pathlib import Path
9
+ import base64
10
+ from io import BytesIO
11
+ from typing import Union, Tuple
12
+ import matplotlib
13
+ import re
14
+
15
+ from colpali_engine.models import ColPali, ColPaliProcessor
16
+ from colpali_engine.utils.torch_utils import get_torch_device
17
+ from einops import rearrange
18
+ from vidore_benchmark.interpretability.plot_utils import plot_similarity_heatmap
19
+ from vidore_benchmark.interpretability.torch_utils import (
20
+ normalize_similarity_map_per_query_token,
21
+ )
22
+ from vidore_benchmark.interpretability.vit_configs import VIT_CONFIG
23
+ from vidore_benchmark.utils.image_utils import scale_image
24
+ from vespa.application import Vespa
25
+ from vespa.io import VespaQueryResponse
26
+
27
+ matplotlib.use("Agg")
28
+
29
+ MAX_QUERY_TERMS = 64
30
+ OUTPUT_DIR = Path(__file__).parent.parent / "output" / "sim_maps"
31
+ OUTPUT_DIR.mkdir(exist_ok=True)
32
+
33
+ COLPALI_GEMMA_MODEL_ID = "vidore--colpaligemma-3b-pt-448-base"
34
+ COLPALI_GEMMA_MODEL_SNAPSHOT = "12c59eb7e23bc4c26876f7be7c17760d5d3a1ffa"
35
+ COLPALI_GEMMA_MODEL_PATH = (
36
+ Path().home()
37
+ / f".cache/huggingface/hub/models--{COLPALI_GEMMA_MODEL_ID}/snapshots/{COLPALI_GEMMA_MODEL_SNAPSHOT}"
38
+ )
39
+ COLPALI_MODEL_ID = "vidore--colpali-v1.2"
40
+ COLPALI_MODEL_SNAPSHOT = "9912ce6f8a462d8cf2269f5606eabbd2784e764f"
41
+ COLPALI_MODEL_PATH = (
42
+ Path().home()
43
+ / f".cache/huggingface/hub/models--{COLPALI_MODEL_ID}/snapshots/{COLPALI_MODEL_SNAPSHOT}"
44
+ )
45
+ COLPALI_GEMMA_MODEL_NAME = COLPALI_GEMMA_MODEL_ID.replace("--", "/")
46
+
47
+
48
+ def load_model() -> Tuple[ColPali, ColPaliProcessor]:
49
+ model_name = "vidore/colpali-v1.2"
50
+
51
+ device = get_torch_device("auto")
52
+ print(f"Using device: {device}")
53
+
54
+ # Load the model
55
+ model = cast(
56
+ ColPali,
57
+ ColPali.from_pretrained(
58
+ model_name,
59
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
60
+ device_map=device,
61
+ ),
62
+ ).eval()
63
+
64
+ # Load the processor
65
+ processor = cast(ColPaliProcessor, ColPaliProcessor.from_pretrained(model_name))
66
+ return model, processor
67
+
68
+
69
+ def load_vit_config(model):
70
+ # Load the ViT config
71
+ print(f"VIT config: {VIT_CONFIG}")
72
+ vit_config = VIT_CONFIG[COLPALI_GEMMA_MODEL_NAME]
73
+ return vit_config
74
+
75
+
76
+ # Create dummy image
77
+ dummy_image = Image.new("RGB", (448, 448), (255, 255, 255))
78
+
79
+
80
+ def gen_similarity_map(
81
+ model, processor, device, vit_config, query, image: Union[Path, str]
82
+ ):
83
+ # Should take in the b64 image from Vespa query result
84
+ # And possibly the tensor representing the output_image
85
+ if isinstance(image, Path):
86
+ # image is a file path
87
+ try:
88
+ image = Image.open(image)
89
+ except Exception as e:
90
+ raise ValueError(f"Failed to open image from path: {e}")
91
+ elif isinstance(image, str):
92
+ # image is b64 string
93
+ try:
94
+ image = Image.open(BytesIO(base64.b64decode(image)))
95
+ except Exception as e:
96
+ raise ValueError(f"Failed to open image from b64: {e}")
97
+
98
+ # Preview the image
99
+ scale_image(image, 512)
100
+ # Preprocess inputs
101
+ input_text_processed = processor.process_queries([query]).to(device)
102
+ input_image_processed = processor.process_images([image]).to(device)
103
+ # Forward passes
104
+ with torch.no_grad():
105
+ output_text = model.forward(**input_text_processed)
106
+ output_image = model.forward(**input_image_processed)
107
+ # output_image is the tensor that we could get from the Vespa query
108
+ # Print shape of output_text and output_image
109
+ # Output image shape: torch.Size([1, 1030, 128])
110
+ # Remove the special tokens from the output
111
+ output_image = output_image[
112
+ :, : processor.image_seq_length, :
113
+ ] # (1, n_patches_x * n_patches_y, dim)
114
+
115
+ # Rearrange the output image tensor to explicitly represent the 2D grid of patches
116
+ output_image = rearrange(
117
+ output_image,
118
+ "b (h w) c -> b h w c",
119
+ h=vit_config.n_patch_per_dim,
120
+ w=vit_config.n_patch_per_dim,
121
+ ) # (1, n_patches_x, n_patches_y, dim)
122
+ # Get the similarity map
123
+ similarity_map = torch.einsum(
124
+ "bnk,bijk->bnij", output_text, output_image
125
+ ) # (1, query_tokens, n_patches_x, n_patches_y)
126
+
127
+ # Normalize the similarity map
128
+ similarity_map_normalized = normalize_similarity_map_per_query_token(
129
+ similarity_map
130
+ ) # (1, query_tokens, n_patches_x, n_patches_y)
131
+ # Use this cell output to choose a token using its index
132
+ query_tokens = processor.tokenizer.tokenize(
133
+ processor.decode(input_text_processed.input_ids[0])
134
+ )
135
+ # Choose a token
136
+ token_idx = (
137
+ 10 # e.g. if "12: '▁Kazakhstan',", set 12 to choose the token 'Kazakhstan'
138
+ )
139
+ selected_token = processor.decode(input_text_processed.input_ids[0, token_idx])
140
+ # strip whitespace
141
+ selected_token = selected_token.strip()
142
+ print(f"Selected token: `{selected_token}`")
143
+ # Retrieve the similarity map for the chosen token
144
+ pprint.pprint({idx: val for idx, val in enumerate(query_tokens)})
145
+ # Resize the image to square
146
+ input_image_square = image.resize((vit_config.resolution, vit_config.resolution))
147
+
148
+ # Plot the similarity map
149
+ fig, ax = plot_similarity_heatmap(
150
+ input_image_square,
151
+ patch_size=vit_config.patch_size,
152
+ image_resolution=vit_config.resolution,
153
+ similarity_map=similarity_map_normalized[0, token_idx, :, :],
154
+ )
155
+ ax = annotate_plot(ax, selected_token)
156
+ return fig, ax
157
+
158
+
159
+ def save_figure(fig, filename: str = "similarity_map.png"):
160
+ fig.savefig(
161
+ OUTPUT_DIR / filename,
162
+ bbox_inches="tight",
163
+ pad_inches=0,
164
+ )
165
+
166
+
167
+ def annotate_plot(ax, query, selected_token):
168
+ # Add the query text
169
+ ax.set_title(query, fontsize=18)
170
+ # Add annotation with selected token
171
+ ax.annotate(
172
+ f"Selected token:`{selected_token}`",
173
+ xy=(0.5, 0.95),
174
+ xycoords="axes fraction",
175
+ ha="center",
176
+ va="center",
177
+ fontsize=18,
178
+ color="black",
179
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="black", lw=1),
180
+ )
181
+ return ax
182
+
183
+
184
+ def gen_similarity_map_new(
185
+ processor: ColPaliProcessor,
186
+ model: ColPali,
187
+ device,
188
+ vit_config,
189
+ query: str,
190
+ query_embs: torch.Tensor,
191
+ token_idx_map: dict,
192
+ token_to_show: str,
193
+ image: Union[Path, str],
194
+ ):
195
+ if isinstance(image, Path):
196
+ # image is a file path
197
+ try:
198
+ image = Image.open(image)
199
+ except Exception as e:
200
+ raise ValueError(f"Failed to open image from path: {e}")
201
+ elif isinstance(image, str):
202
+ # image is b64 string
203
+ try:
204
+ image = Image.open(BytesIO(base64.b64decode(image)))
205
+ except Exception as e:
206
+ raise ValueError(f"Failed to open image from b64: {e}")
207
+ token_idx = token_idx_map[token_to_show]
208
+ print(f"Selected token: `{token_to_show}`")
209
+ # strip whitespace
210
+ # Preview the image
211
+ # scale_image(image, 512)
212
+ # Preprocess inputs
213
+ input_image_processed = processor.process_images([image]).to(device)
214
+ # Forward passes
215
+ with torch.no_grad():
216
+ output_image = model.forward(**input_image_processed)
217
+ # output_image is the tensor that we could get from the Vespa query
218
+ # Print shape of output_text and output_image
219
+ # Output image shape: torch.Size([1, 1030, 128])
220
+ # Remove the special tokens from the output
221
+ print(f"Output image shape before dim: {output_image.shape}")
222
+ output_image = output_image[
223
+ :, : processor.image_seq_length, :
224
+ ] # (1, n_patches_x * n_patches_y, dim)
225
+ print(f"Output image shape after dim: {output_image.shape}")
226
+ # Rearrange the output image tensor to explicitly represent the 2D grid of patches
227
+ output_image = rearrange(
228
+ output_image,
229
+ "b (h w) c -> b h w c",
230
+ h=vit_config.n_patch_per_dim,
231
+ w=vit_config.n_patch_per_dim,
232
+ ) # (1, n_patches_x, n_patches_y, dim)
233
+ # Get the similarity map
234
+ print(f"Query embs shape: {query_embs.shape}")
235
+ # Add 1 extra dim to start of query_embs
236
+ query_embs = query_embs.unsqueeze(0).to(device)
237
+ print(f"Output image shape: {output_image.shape}")
238
+ similarity_map = torch.einsum(
239
+ "bnk,bijk->bnij", query_embs, output_image
240
+ ) # (1, query_tokens, n_patches_x, n_patches_y)
241
+ print(f"Similarity map shape: {similarity_map.shape}")
242
+ # Normalize the similarity map
243
+ similarity_map_normalized = normalize_similarity_map_per_query_token(
244
+ similarity_map
245
+ ) # (1, query_tokens, n_patches_x, n_patches_y)
246
+ print(f"Similarity map normalized shape: {similarity_map_normalized.shape}")
247
+ # Use this cell output to choose a token using its index
248
+ input_image_square = image.resize((vit_config.resolution, vit_config.resolution))
249
+
250
+ # Plot the similarity map
251
+ fig, ax = plot_similarity_heatmap(
252
+ input_image_square,
253
+ patch_size=vit_config.patch_size,
254
+ image_resolution=vit_config.resolution,
255
+ similarity_map=similarity_map_normalized[0, token_idx, :, :],
256
+ )
257
+ ax = annotate_plot(ax, query, token_to_show)
258
+ # save the figure
259
+ save_figure(fig, f"similarity_map_{token_to_show}.png")
260
+ return fig, ax
261
+
262
+
263
+ def get_query_embeddings_and_token_map(
264
+ processor, model, query, image
265
+ ) -> Tuple[torch.Tensor, dict]:
266
+ inputs = processor.process_queries([query]).to(model.device)
267
+ with torch.no_grad():
268
+ embeddings_query = model(**inputs)
269
+ q_emb = embeddings_query.to("cpu")[0] # Extract the single embedding
270
+ # Use this cell output to choose a token using its index
271
+ query_tokens = processor.tokenizer.tokenize(processor.decode(inputs.input_ids[0]))
272
+ # reverse key, values in dictionary
273
+ print(query_tokens)
274
+ token_to_idx = {val: idx for idx, val in enumerate(query_tokens)}
275
+ return q_emb, token_to_idx
276
+
277
+
278
+ def format_query_results(query, response, hits=5) -> dict:
279
+ query_time = response.json.get("timing", {}).get("searchtime", -1)
280
+ query_time = round(query_time, 2)
281
+ count = response.json.get("root", {}).get("fields", {}).get("totalCount", 0)
282
+ result_text = f"Query text: '{query}', query time {query_time}s, count={count}, top results:\n"
283
+ print(result_text)
284
+ return response.json
285
+
286
+
287
+ async def query_vespa_default(
288
+ app: Vespa,
289
+ query: str,
290
+ q_emb: torch.Tensor,
291
+ hits: int = 3,
292
+ timeout: str = "10s",
293
+ **kwargs,
294
+ ) -> dict:
295
+ async with app.asyncio(connections=1, total_timeout=120) as session:
296
+ query_embedding = format_q_embs(q_emb)
297
+ response: VespaQueryResponse = await session.query(
298
+ body={
299
+ "yql": "select id,title,url,image,page_number,text from pdf_page where userQuery();",
300
+ "ranking": "default",
301
+ "query": query,
302
+ "timeout": timeout,
303
+ "hits": hits,
304
+ "input.query(qt)": query_embedding,
305
+ "presentation.timing": True,
306
+ **kwargs,
307
+ },
308
+ )
309
+ assert response.is_successful(), response.json
310
+ return format_query_results(query, response)
311
+
312
+
313
+ def float_to_binary_embedding(float_query_embedding: dict) -> dict:
314
+ binary_query_embeddings = {}
315
+ for k, v in float_query_embedding.items():
316
+ binary_vector = (
317
+ np.packbits(np.where(np.array(v) > 0, 1, 0)).astype(np.int8).tolist()
318
+ )
319
+ binary_query_embeddings[k] = binary_vector
320
+ if len(binary_query_embeddings) >= MAX_QUERY_TERMS:
321
+ print(f"Warning: Query has more than {MAX_QUERY_TERMS} terms. Truncating.")
322
+ break
323
+ return binary_query_embeddings
324
+
325
+
326
+ def create_nn_query_strings(
327
+ binary_query_embeddings: dict, target_hits_per_query_tensor: int = 20
328
+ ) -> Tuple[str, dict]:
329
+ # Query tensors for nearest neighbor calculations
330
+ nn_query_dict = {}
331
+ for i in range(len(binary_query_embeddings)):
332
+ nn_query_dict[f"input.query(rq{i})"] = binary_query_embeddings[i]
333
+ nn = " OR ".join(
334
+ [
335
+ f"({{targetHits:{target_hits_per_query_tensor}}}nearestNeighbor(embedding,rq{i}))"
336
+ for i in range(len(binary_query_embeddings))
337
+ ]
338
+ )
339
+ return nn, nn_query_dict
340
+
341
+
342
+ def format_q_embs(q_embs: torch.Tensor) -> dict:
343
+ float_query_embedding = {k: v.tolist() for k, v in enumerate(q_embs)}
344
+ return float_query_embedding
345
+
346
+
347
+ async def query_vespa_nearest_neighbor(
348
+ app: Vespa,
349
+ query: str,
350
+ q_emb: torch.Tensor,
351
+ target_hits_per_query_tensor: int = 20,
352
+ hits: int = 3,
353
+ timeout: str = "10s",
354
+ **kwargs,
355
+ ) -> dict:
356
+ # Hyperparameter for speed vs. accuracy
357
+ async with app.asyncio(connections=1, total_timeout=180) as session:
358
+ float_query_embedding = format_q_embs(q_emb)
359
+ binary_query_embeddings = float_to_binary_embedding(float_query_embedding)
360
+
361
+ # Mixed tensors for MaxSim calculations
362
+ query_tensors = {
363
+ "input.query(qtb)": binary_query_embeddings,
364
+ "input.query(qt)": float_query_embedding,
365
+ }
366
+ nn_string, nn_query_dict = create_nn_query_strings(
367
+ binary_query_embeddings, target_hits_per_query_tensor
368
+ )
369
+ query_tensors.update(nn_query_dict)
370
+ response: VespaQueryResponse = await session.query(
371
+ body={
372
+ **query_tensors,
373
+ "presentation.timing": True,
374
+ "yql": f"select id,title,text,url,image,page_number from pdf_page where {nn_string}",
375
+ "ranking.profile": "retrieval-and-rerank",
376
+ "timeout": timeout,
377
+ "hits": hits,
378
+ **kwargs,
379
+ },
380
+ )
381
+ assert response.is_successful(), response.json
382
+ return format_query_results(query, response)
383
+
384
+
385
+ def is_special_token(token: str) -> bool:
386
+ # Pattern for tokens that start with '<', numbers, whitespace, or single characters
387
+ pattern = re.compile(r"^<.*$|^\d+$|^\s+$|^.$")
388
+ if pattern.match(token):
389
+ return True
390
+ return False
391
+
392
+
393
+ async def get_result_from_query(
394
+ app: Vespa,
395
+ processor: ColPaliProcessor,
396
+ model: ColPali,
397
+ query: str,
398
+ nn=False,
399
+ gen_sim_map=False,
400
+ ):
401
+ # Get the query embeddings and token map
402
+ print(query)
403
+ q_embs, token_to_idx = get_query_embeddings_and_token_map(
404
+ processor, model, query, dummy_image
405
+ )
406
+ print(token_to_idx)
407
+ # Use the token map to choose a token randomly for now
408
+ # Dynamically select a token containing 'water'
409
+
410
+ if nn:
411
+ result = await query_vespa_nearest_neighbor(app, query, q_embs)
412
+ else:
413
+ result = await query_vespa_default(app, query, q_embs)
414
+ # Print score, title id and text of the results
415
+ for idx, child in enumerate(result["root"]["children"]):
416
+ print(
417
+ f"Result {idx+1}: {child['relevance']}, {child['fields']['title']}, {child['fields']['id']}"
418
+ )
419
+
420
+ if gen_sim_map:
421
+ for single_result in result["root"]["children"]:
422
+ img = single_result["fields"]["image"]
423
+ for token in token_to_idx:
424
+ if is_special_token(token):
425
+ print(f"Skipping special token: {token}")
426
+ continue
427
+ fig, ax = gen_similarity_map_new(
428
+ processor,
429
+ model,
430
+ model.device,
431
+ load_vit_config(model),
432
+ query,
433
+ q_embs,
434
+ token_to_idx,
435
+ token,
436
+ img,
437
+ )
438
+ sim_map = base64.b64encode(fig.canvas.tostring_rgb()).decode("utf-8")
439
+ single_result["fields"][f"sim_map_{token}"] = sim_map
440
+ return result
441
+
442
+
443
+ def get_result_dummy(query: str, nn: bool = False):
444
+ result = {}
445
+ result["timing"] = {}
446
+ result["timing"]["querytime"] = 0.23700000000000002
447
+ result["timing"]["summaryfetchtime"] = 0.001
448
+ result["timing"]["searchtime"] = 0.23900000000000002
449
+ result["root"] = {}
450
+ result["root"]["id"] = "toplevel"
451
+ result["root"]["relevance"] = 1
452
+ result["root"]["fields"] = {}
453
+ result["root"]["fields"]["totalCount"] = 59
454
+ result["root"]["coverage"] = {}
455
+ result["root"]["coverage"]["coverage"] = 100
456
+ result["root"]["coverage"]["documents"] = 155
457
+ result["root"]["coverage"]["full"] = True
458
+ result["root"]["coverage"]["nodes"] = 1
459
+ result["root"]["coverage"]["results"] = 1
460
+ result["root"]["coverage"]["resultsFull"] = 1
461
+ result["root"]["children"] = []
462
+ elt0 = {}
463
+ elt0["id"] = "index:colpalidemo_content/0/424c85e7dece761d226f060f"
464
+ elt0["relevance"] = 2354.050122871995
465
+ elt0["source"] = "colpalidemo_content"
466
+ elt0["fields"] = {}
467
+ elt0["fields"]["id"] = "a767cb1868be9a776cd56b768347b089"
468
+ elt0["fields"]["url"] = (
469
+ "https://static.conocophillips.com/files/resources/conocophillips-2023-sustainability-report.pdf"
470
+ )
471
+ elt0["fields"]["title"] = "ConocoPhillips 2023 Sustainability Report"
472
+ elt0["fields"]["page_number"] = 50
473
+ elt0["fields"]["image"] = "empty for now - is base64 encoded image"
474
+ result["root"]["children"].append(elt0)
475
+ elt1 = {}
476
+ elt1["id"] = "index:colpalidemo_content/0/b927c4979f0beaf0d7fab8e9"
477
+ elt1["relevance"] = 2313.7529950886965
478
+ elt1["source"] = "colpalidemo_content"
479
+ elt1["fields"] = {}
480
+ elt1["fields"]["id"] = "9f2fc0aa02c9561adfaa1451c875658f"
481
+ elt1["fields"]["url"] = (
482
+ "https://static.conocophillips.com/files/resources/conocophillips-2023-managing-climate-related-risks.pdf"
483
+ )
484
+ elt1["fields"]["title"] = "ConocoPhillips Managing Climate Related Risks"
485
+ elt1["fields"]["page_number"] = 44
486
+ elt1["fields"]["image"] = "empty for now - is base64 encoded image"
487
+ result["root"]["children"].append(elt1)
488
+ elt2 = {}
489
+ elt2["id"] = "index:colpalidemo_content/0/9632d72238829d6afefba6c9"
490
+ elt2["relevance"] = 2312.230182081461
491
+ elt2["source"] = "colpalidemo_content"
492
+ elt2["fields"] = {}
493
+ elt2["fields"]["id"] = "d638ded1ddcb446268b289b3f65430fd"
494
+ elt2["fields"]["url"] = (
495
+ "https://static.conocophillips.com/files/resources/24-0976-sustainability-highlights_nature.pdf"
496
+ )
497
+ elt2["fields"]["title"] = (
498
+ "ConocoPhillips Sustainability Highlights - Nature (24-0976)"
499
+ )
500
+ elt2["fields"]["page_number"] = 0
501
+ elt2["fields"]["image"] = "empty for now - is base64 encoded image"
502
+ result["root"]["children"].append(elt2)
503
+ return result
504
+
505
+
506
+ if __name__ == "__main__":
507
+ model, processor = load_model()
508
+ vit_config = load_vit_config(model)
509
+ query = "How many percent of source water is fresh water?"
510
+ image_filepath = (
511
+ Path(__file__).parent.parent
512
+ / "static"
513
+ / "assets"
514
+ / "ConocoPhillips Sustainability Highlights - Nature (24-0976).png"
515
+ )
516
+ gen_similarity_map(
517
+ model, processor, model.device, vit_config, query=query, image=image_filepath
518
+ )
519
+ result = get_result_dummy("dummy query")
520
+ print(result)
521
+ print("Done")
backend/vespa_app.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from vespa.application import Vespa
3
+ from dotenv import load_dotenv
4
+
5
+
6
+ def get_vespa_app():
7
+ load_dotenv()
8
+ vespa_app_url = os.environ.get(
9
+ "VESPA_APP_URL"
10
+ ) # Ensure this is set to your Vespa app URL
11
+ vespa_cloud_secret_token = os.environ.get("VESPA_CLOUD_SECRET_TOKEN")
12
+
13
+ if not vespa_app_url or not vespa_cloud_secret_token:
14
+ raise ValueError(
15
+ "Please set the VESPA_APP_URL and VESPA_CLOUD_SECRET_TOKEN environment variables"
16
+ )
17
+ # Instantiate Vespa connection
18
+ vespa_app = Vespa(
19
+ url=vespa_app_url, vespa_cloud_secret_token=vespa_cloud_secret_token
20
+ )
21
+ vespa_app.wait_for_application_up()
22
+ print(f"Connected to Vespa at {vespa_app_url}")
23
+ return vespa_app
colpalidemo/schemas/pdf_page.sd ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema pdf_page {
2
+ document pdf_page {
3
+ field id type string {
4
+ indexing: summary | index
5
+ match {
6
+ word
7
+ }
8
+ }
9
+ field url type string {
10
+ indexing: summary | index
11
+ }
12
+ field title type string {
13
+ indexing: summary | index
14
+ index: enable-bm25
15
+ match {
16
+ text
17
+ }
18
+ }
19
+ field page_number type int {
20
+ indexing: summary | attribute
21
+ }
22
+ field image type raw {
23
+ indexing: summary
24
+ }
25
+ field text type string {
26
+ indexing: summary | index
27
+ index: enable-bm25
28
+ match {
29
+ text
30
+ }
31
+ }
32
+ field embedding type tensor<int8>(patch{}, v[16]) {
33
+ indexing: attribute | index
34
+ attribute {
35
+ distance-metric: hamming
36
+ }
37
+ index {
38
+ hnsw {
39
+ max-links-per-node: 32
40
+ neighbors-to-explore-at-insert: 400
41
+ }
42
+ }
43
+ }
44
+ }
45
+ fieldset default {
46
+ fields: title, text
47
+ }
48
+ rank-profile default {
49
+ inputs {
50
+ query(qt) tensor<float>(querytoken{}, v[128])
51
+
52
+ }
53
+ function max_sim() {
54
+ expression {
55
+
56
+ sum(
57
+ reduce(
58
+ sum(
59
+ query(qt) * unpack_bits(attribute(embedding)) , v
60
+ ),
61
+ max, patch
62
+ ),
63
+ querytoken
64
+ )
65
+
66
+ }
67
+ }
68
+ function bm25_score() {
69
+ expression {
70
+ bm25(title) + bm25(text)
71
+ }
72
+ }
73
+ first-phase {
74
+ expression {
75
+ bm25_score
76
+ }
77
+ }
78
+ second-phase {
79
+ rerank-count: 10
80
+ expression {
81
+ max_sim
82
+ }
83
+ }
84
+ }
85
+ rank-profile retrieval-and-rerank {
86
+ inputs {
87
+ query(rq0) tensor<int8>(v[16])
88
+ query(rq1) tensor<int8>(v[16])
89
+ query(rq2) tensor<int8>(v[16])
90
+ query(rq3) tensor<int8>(v[16])
91
+ query(rq4) tensor<int8>(v[16])
92
+ query(rq5) tensor<int8>(v[16])
93
+ query(rq6) tensor<int8>(v[16])
94
+ query(rq7) tensor<int8>(v[16])
95
+ query(rq8) tensor<int8>(v[16])
96
+ query(rq9) tensor<int8>(v[16])
97
+ query(rq10) tensor<int8>(v[16])
98
+ query(rq11) tensor<int8>(v[16])
99
+ query(rq12) tensor<int8>(v[16])
100
+ query(rq13) tensor<int8>(v[16])
101
+ query(rq14) tensor<int8>(v[16])
102
+ query(rq15) tensor<int8>(v[16])
103
+ query(rq16) tensor<int8>(v[16])
104
+ query(rq17) tensor<int8>(v[16])
105
+ query(rq18) tensor<int8>(v[16])
106
+ query(rq19) tensor<int8>(v[16])
107
+ query(rq20) tensor<int8>(v[16])
108
+ query(rq21) tensor<int8>(v[16])
109
+ query(rq22) tensor<int8>(v[16])
110
+ query(rq23) tensor<int8>(v[16])
111
+ query(rq24) tensor<int8>(v[16])
112
+ query(rq25) tensor<int8>(v[16])
113
+ query(rq26) tensor<int8>(v[16])
114
+ query(rq27) tensor<int8>(v[16])
115
+ query(rq28) tensor<int8>(v[16])
116
+ query(rq29) tensor<int8>(v[16])
117
+ query(rq30) tensor<int8>(v[16])
118
+ query(rq31) tensor<int8>(v[16])
119
+ query(rq32) tensor<int8>(v[16])
120
+ query(rq33) tensor<int8>(v[16])
121
+ query(rq34) tensor<int8>(v[16])
122
+ query(rq35) tensor<int8>(v[16])
123
+ query(rq36) tensor<int8>(v[16])
124
+ query(rq37) tensor<int8>(v[16])
125
+ query(rq38) tensor<int8>(v[16])
126
+ query(rq39) tensor<int8>(v[16])
127
+ query(rq40) tensor<int8>(v[16])
128
+ query(rq41) tensor<int8>(v[16])
129
+ query(rq42) tensor<int8>(v[16])
130
+ query(rq43) tensor<int8>(v[16])
131
+ query(rq44) tensor<int8>(v[16])
132
+ query(rq45) tensor<int8>(v[16])
133
+ query(rq46) tensor<int8>(v[16])
134
+ query(rq47) tensor<int8>(v[16])
135
+ query(rq48) tensor<int8>(v[16])
136
+ query(rq49) tensor<int8>(v[16])
137
+ query(rq50) tensor<int8>(v[16])
138
+ query(rq51) tensor<int8>(v[16])
139
+ query(rq52) tensor<int8>(v[16])
140
+ query(rq53) tensor<int8>(v[16])
141
+ query(rq54) tensor<int8>(v[16])
142
+ query(rq55) tensor<int8>(v[16])
143
+ query(rq56) tensor<int8>(v[16])
144
+ query(rq57) tensor<int8>(v[16])
145
+ query(rq58) tensor<int8>(v[16])
146
+ query(rq59) tensor<int8>(v[16])
147
+ query(rq60) tensor<int8>(v[16])
148
+ query(rq61) tensor<int8>(v[16])
149
+ query(rq62) tensor<int8>(v[16])
150
+ query(rq63) tensor<int8>(v[16])
151
+ query(qt) tensor<float>(querytoken{}, v[128])
152
+ query(qtb) tensor<int8>(querytoken{}, v[16])
153
+
154
+ }
155
+ function max_sim() {
156
+ expression {
157
+
158
+ sum(
159
+ reduce(
160
+ sum(
161
+ query(qt) * unpack_bits(attribute(embedding)) , v
162
+ ),
163
+ max, patch
164
+ ),
165
+ querytoken
166
+ )
167
+
168
+ }
169
+ }
170
+ function max_sim_binary() {
171
+ expression {
172
+
173
+ sum(
174
+ reduce(
175
+ 1/(1 + sum(
176
+ hamming(query(qtb), attribute(embedding)) ,v)
177
+ ),
178
+ max,
179
+ patch
180
+ ),
181
+ querytoken
182
+ )
183
+
184
+ }
185
+ }
186
+ first-phase {
187
+ expression {
188
+ max_sim_binary
189
+ }
190
+ }
191
+ second-phase {
192
+ rerank-count: 10
193
+ expression {
194
+ max_sim
195
+ }
196
+ }
197
+ }
198
+ }
colpalidemo/search/query-profiles/default.xml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <query-profile id="default" type="root">
2
+ </query-profile>
colpalidemo/search/query-profiles/types/root.xml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <query-profile-type id="root">
2
+ </query-profile-type>
colpalidemo/services.xml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <services version="1.0">
3
+ <container id="colpalidemo_container" version="1.0">
4
+ <search></search>
5
+ <document-api></document-api>
6
+ <document-processing></document-processing>
7
+ <clients>
8
+ <client id="mtls" permissions="read,write">
9
+ <certificate file="security/clients.pem"/>
10
+ </client>
11
+ <client id="token_write" permissions="read,write">
12
+ <token id="colpalidemo_write"/>
13
+ </client>
14
+ <client id="token_read" permissions="read">
15
+ <token id="colpalidemo_read"/>
16
+ </client>
17
+ </clients>
18
+ </container>
19
+ <content id="colpalidemo_content" version="1.0">
20
+ <redundancy>1</redundancy>
21
+ <documents>
22
+ <document type="pdf_page" mode="index"></document>
23
+ </documents>
24
+ <nodes>
25
+ <node distribution-key="0" hostalias="node1"></node>
26
+ </nodes>
27
+ </content>
28
+ </services>
deploy_vespa_app.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ from vespa.package import (
5
+ ApplicationPackage,
6
+ Field,
7
+ Schema,
8
+ Document,
9
+ HNSW,
10
+ RankProfile,
11
+ Function,
12
+ AuthClient,
13
+ Parameter,
14
+ FieldSet,
15
+ SecondPhaseRanking,
16
+ )
17
+ from vespa.deployment import VespaCloud
18
+ import os
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description="Deploy Vespa application")
23
+ parser.add_argument("--tenant_name", required=True, help="Vespa Cloud tenant name")
24
+ parser.add_argument(
25
+ "--vespa_application_name", required=True, help="Vespa application name"
26
+ )
27
+ parser.add_argument(
28
+ "--token_id_write", required=True, help="Vespa Cloud token ID for write access"
29
+ )
30
+ parser.add_argument(
31
+ "--token_id_read", required=True, help="Vespa Cloud token ID for read access"
32
+ )
33
+
34
+ args = parser.parse_args()
35
+ tenant_name = args.tenant_name
36
+ vespa_app_name = args.vespa_application_name
37
+ token_id_write = args.token_id_write
38
+ token_id_read = args.token_id_read
39
+
40
+ # Define the Vespa schema
41
+ colpali_schema = Schema(
42
+ name="pdf_page",
43
+ document=Document(
44
+ fields=[
45
+ Field(
46
+ name="id",
47
+ type="string",
48
+ indexing=["summary", "index"],
49
+ match=["word"],
50
+ ),
51
+ Field(name="url", type="string", indexing=["summary", "index"]),
52
+ Field(
53
+ name="title",
54
+ type="string",
55
+ indexing=["summary", "index"],
56
+ match=["text"],
57
+ index="enable-bm25",
58
+ ),
59
+ Field(
60
+ name="page_number", type="int", indexing=["summary", "attribute"]
61
+ ),
62
+ Field(name="image", type="raw", indexing=["summary"]),
63
+ Field(
64
+ name="text",
65
+ type="string",
66
+ indexing=["summary", "index"],
67
+ match=["text"],
68
+ index="enable-bm25",
69
+ ),
70
+ Field(
71
+ name="embedding",
72
+ type="tensor<int8>(patch{}, v[16])",
73
+ indexing=[
74
+ "attribute",
75
+ "index",
76
+ ], # adds HNSW index for candidate retrieval.
77
+ ann=HNSW(
78
+ distance_metric="hamming",
79
+ max_links_per_node=32,
80
+ neighbors_to_explore_at_insert=400,
81
+ ),
82
+ ),
83
+ ]
84
+ ),
85
+ fieldsets=[
86
+ FieldSet(name="default", fields=["title", "url", "page_number", "text"]),
87
+ FieldSet(name="image", fields=["image"]),
88
+ ],
89
+ )
90
+
91
+ # Define rank profiles
92
+ colpali_profile = RankProfile(
93
+ name="default",
94
+ inputs=[("query(qt)", "tensor<float>(querytoken{}, v[128])")],
95
+ functions=[
96
+ Function(
97
+ name="max_sim",
98
+ expression="""
99
+ sum(
100
+ reduce(
101
+ sum(
102
+ query(qt) * unpack_bits(attribute(embedding)) , v
103
+ ),
104
+ max, patch
105
+ ),
106
+ querytoken
107
+ )
108
+ """,
109
+ ),
110
+ Function(name="bm25_score", expression="bm25(title) + bm25(text)"),
111
+ ],
112
+ first_phase="bm25_score",
113
+ second_phase=SecondPhaseRanking(expression="max_sim", rerank_count=10),
114
+ )
115
+ colpali_schema.add_rank_profile(colpali_profile)
116
+
117
+ # Add retrieval-and-rerank rank profile
118
+ input_query_tensors = []
119
+ MAX_QUERY_TERMS = 64
120
+ for i in range(MAX_QUERY_TERMS):
121
+ input_query_tensors.append((f"query(rq{i})", "tensor<int8>(v[16])"))
122
+
123
+ input_query_tensors.append(("query(qt)", "tensor<float>(querytoken{}, v[128])"))
124
+ input_query_tensors.append(("query(qtb)", "tensor<int8>(querytoken{}, v[16])"))
125
+
126
+ colpali_retrieval_profile = RankProfile(
127
+ name="retrieval-and-rerank",
128
+ inputs=input_query_tensors,
129
+ functions=[
130
+ Function(
131
+ name="max_sim",
132
+ expression="""
133
+ sum(
134
+ reduce(
135
+ sum(
136
+ query(qt) * unpack_bits(attribute(embedding)) , v
137
+ ),
138
+ max, patch
139
+ ),
140
+ querytoken
141
+ )
142
+ """,
143
+ ),
144
+ Function(
145
+ name="max_sim_binary",
146
+ expression="""
147
+ sum(
148
+ reduce(
149
+ 1/(1 + sum(
150
+ hamming(query(qtb), attribute(embedding)) ,v)
151
+ ),
152
+ max,
153
+ patch
154
+ ),
155
+ querytoken
156
+ )
157
+ """,
158
+ ),
159
+ ],
160
+ first_phase="max_sim_binary",
161
+ second_phase=SecondPhaseRanking(expression="max_sim", rerank_count=10),
162
+ )
163
+ colpali_schema.add_rank_profile(colpali_retrieval_profile)
164
+
165
+ # Create the Vespa application package
166
+ vespa_application_package = ApplicationPackage(
167
+ name=vespa_app_name,
168
+ schema=[colpali_schema],
169
+ auth_clients=[
170
+ AuthClient(
171
+ id="mtls", # Note that you still need to include the mtls client.
172
+ permissions=["read", "write"],
173
+ parameters=[Parameter("certificate", {"file": "security/clients.pem"})],
174
+ ),
175
+ AuthClient(
176
+ id="token_write",
177
+ permissions=["read", "write"],
178
+ parameters=[Parameter("token", {"id": token_id_write})],
179
+ ),
180
+ AuthClient(
181
+ id="token_read",
182
+ permissions=["read"],
183
+ parameters=[Parameter("token", {"id": token_id_read})],
184
+ ),
185
+ ],
186
+ )
187
+ vespa_team_api_key = os.getenv("VESPA_TEAM_API_KEY")
188
+ # Deploy the application to Vespa Cloud
189
+ vespa_cloud = VespaCloud(
190
+ tenant=tenant_name,
191
+ application=vespa_app_name,
192
+ key_content=vespa_team_api_key,
193
+ application_package=vespa_application_package,
194
+ )
195
+
196
+ app = vespa_cloud.deploy()
197
+
198
+ # Output the endpoint URL
199
+ endpoint_url = vespa_cloud.get_token_endpoint()
200
+ print(f"Application deployed. Token endpoint URL: {endpoint_url}")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
feed_vespa.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import torch
5
+ from torch.utils.data import DataLoader
6
+ from tqdm import tqdm
7
+ from io import BytesIO
8
+ from typing import cast
9
+ import os
10
+ import json
11
+ import hashlib
12
+
13
+ from colpali_engine.models import ColPali, ColPaliProcessor
14
+ from colpali_engine.utils.torch_utils import get_torch_device
15
+ from vidore_benchmark.utils.image_utils import scale_image, get_base64_image
16
+ import requests
17
+ from pdf2image import convert_from_path
18
+ from pypdf import PdfReader
19
+ import numpy as np
20
+ from vespa.application import Vespa
21
+ from vespa.io import VespaResponse
22
+ from dotenv import load_dotenv
23
+
24
+ load_dotenv()
25
+
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(description="Feed data into Vespa application")
29
+ parser.add_argument(
30
+ "--application_name",
31
+ required=True,
32
+ default="colpalidemo",
33
+ help="Vespa application name",
34
+ )
35
+ parser.add_argument(
36
+ "--vespa_schema_name",
37
+ required=True,
38
+ default="pdf_page",
39
+ help="Vespa schema name",
40
+ )
41
+ args = parser.parse_args()
42
+
43
+ vespa_app_url = os.getenv("VESPA_APP_URL")
44
+ vespa_cloud_secret_token = os.getenv("VESPA_CLOUD_SECRET_TOKEN")
45
+ # Set application and schema names
46
+ application_name = args.application_name
47
+ schema_name = args.vespa_schema_name
48
+ # Instantiate Vespa connection using token
49
+ app = Vespa(url=vespa_app_url, vespa_cloud_secret_token=vespa_cloud_secret_token)
50
+ app.get_application_status()
51
+ model_name = "vidore/colpali-v1.2"
52
+
53
+ device = get_torch_device("auto")
54
+ print(f"Using device: {device}")
55
+
56
+ # Load the model
57
+ model = cast(
58
+ ColPali,
59
+ ColPali.from_pretrained(
60
+ model_name,
61
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
62
+ device_map=device,
63
+ ),
64
+ ).eval()
65
+
66
+ # Load the processor
67
+ processor = cast(ColPaliProcessor, ColPaliProcessor.from_pretrained(model_name))
68
+
69
+ # Define functions to work with PDFs
70
+ def download_pdf(url):
71
+ response = requests.get(url)
72
+ if response.status_code == 200:
73
+ return BytesIO(response.content)
74
+ else:
75
+ raise Exception(
76
+ f"Failed to download PDF: Status code {response.status_code}"
77
+ )
78
+
79
+ def get_pdf_images(pdf_url):
80
+ # Download the PDF
81
+ pdf_file = download_pdf(pdf_url)
82
+ # Save the PDF temporarily to disk (pdf2image requires a file path)
83
+ temp_file = "temp.pdf"
84
+ with open(temp_file, "wb") as f:
85
+ f.write(pdf_file.read())
86
+ reader = PdfReader(temp_file)
87
+ page_texts = []
88
+ for page_number in range(len(reader.pages)):
89
+ page = reader.pages[page_number]
90
+ text = page.extract_text()
91
+ page_texts.append(text)
92
+ images = convert_from_path(temp_file)
93
+ assert len(images) == len(page_texts)
94
+ return (images, page_texts)
95
+
96
+ # Define sample PDFs
97
+ sample_pdfs = [
98
+ {
99
+ "title": "ConocoPhillips Sustainability Highlights - Nature (24-0976)",
100
+ "url": "https://static.conocophillips.com/files/resources/24-0976-sustainability-highlights_nature.pdf",
101
+ },
102
+ {
103
+ "title": "ConocoPhillips Managing Climate Related Risks",
104
+ "url": "https://static.conocophillips.com/files/resources/conocophillips-2023-managing-climate-related-risks.pdf",
105
+ },
106
+ {
107
+ "title": "ConocoPhillips 2023 Sustainability Report",
108
+ "url": "https://static.conocophillips.com/files/resources/conocophillips-2023-sustainability-report.pdf",
109
+ },
110
+ ]
111
+
112
+ # Check if vespa_feed.json exists
113
+ if os.path.exists("vespa_feed.json"):
114
+ print("Loading vespa_feed from vespa_feed.json")
115
+ with open("vespa_feed.json", "r") as f:
116
+ vespa_feed_saved = json.load(f)
117
+ vespa_feed = []
118
+ for doc in vespa_feed_saved:
119
+ put_id = doc["put"]
120
+ fields = doc["fields"]
121
+ # Extract document_id from put_id
122
+ # Format: 'id:application_name:schema_name::document_id'
123
+ parts = put_id.split("::")
124
+ document_id = parts[1] if len(parts) > 1 else ""
125
+ page = {"id": document_id, "fields": fields}
126
+ vespa_feed.append(page)
127
+ else:
128
+ print("Generating vespa_feed")
129
+ # Process PDFs
130
+ for pdf in sample_pdfs:
131
+ page_images, page_texts = get_pdf_images(pdf["url"])
132
+ pdf["images"] = page_images
133
+ pdf["texts"] = page_texts
134
+
135
+ # Generate embeddings
136
+ for pdf in sample_pdfs:
137
+ page_embeddings = []
138
+ dataloader = DataLoader(
139
+ pdf["images"],
140
+ batch_size=2,
141
+ shuffle=False,
142
+ collate_fn=lambda x: processor.process_images(x),
143
+ )
144
+ for batch_doc in tqdm(dataloader):
145
+ with torch.no_grad():
146
+ batch_doc = {k: v.to(model.device) for k, v in batch_doc.items()}
147
+ embeddings_doc = model(**batch_doc)
148
+ page_embeddings.extend(list(torch.unbind(embeddings_doc.to("cpu"))))
149
+ pdf["embeddings"] = page_embeddings
150
+
151
+ # Prepare Vespa feed
152
+ vespa_feed = []
153
+ for pdf in sample_pdfs:
154
+ url = pdf["url"]
155
+ title = pdf["title"]
156
+ for page_number, (page_text, embedding, image) in enumerate(
157
+ zip(pdf["texts"], pdf["embeddings"], pdf["images"])
158
+ ):
159
+ base_64_image = get_base64_image(
160
+ scale_image(image, 640), add_url_prefix=False
161
+ )
162
+ embedding_dict = dict()
163
+ for idx, patch_embedding in enumerate(embedding):
164
+ binary_vector = (
165
+ np.packbits(np.where(patch_embedding > 0, 1, 0))
166
+ .astype(np.int8)
167
+ .tobytes()
168
+ .hex()
169
+ )
170
+ embedding_dict[idx] = binary_vector
171
+ # id_hash should be md5 hash of url and page_number
172
+ id_hash = hashlib.md5(f"{url}_{page_number}".encode()).hexdigest()
173
+ page = {
174
+ "id": id_hash,
175
+ "fields": {
176
+ "id": id_hash,
177
+ "url": url,
178
+ "title": title,
179
+ "page_number": page_number,
180
+ "image": base_64_image,
181
+ "text": page_text,
182
+ "embedding": embedding_dict,
183
+ },
184
+ }
185
+ vespa_feed.append(page)
186
+
187
+ # Save vespa_feed to vespa_feed.json in the specified format
188
+ vespa_feed_to_save = []
189
+ for page in vespa_feed:
190
+ document_id = page["id"]
191
+ put_id = f"id:{application_name}:{schema_name}::{document_id}"
192
+ vespa_feed_to_save.append({"put": put_id, "fields": page["fields"]})
193
+ with open("vespa_feed.json", "w") as f:
194
+ json.dump(vespa_feed_to_save, f)
195
+
196
+ def callback(response: VespaResponse, id: str):
197
+ if not response.is_successful():
198
+ print(
199
+ f"Failed to feed document {id} with status code {response.status_code}: Reason {response.get_json()}"
200
+ )
201
+
202
+ # Feed data into Vespa
203
+ app.feed_iterable(vespa_feed, schema=schema_name, callback=callback)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ main()
frontend/__init__.py ADDED
File without changes
frontend/app.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from urllib.parse import quote_plus
2
+
3
+ from fasthtml.components import Div, H1, P, Img, H2, Form, Span
4
+ from fasthtml.xtend import Script, A
5
+ from lucide_fasthtml import Lucide
6
+ from shad4fast import Button, Input, Badge
7
+
8
+
9
+ def check_input_script():
10
+ return Script(
11
+ """
12
+ window.onload = function() {
13
+ const input = document.getElementById('search-input');
14
+ const button = document.querySelector('[data-button="search-button"]');
15
+ function checkInputValue() { button.disabled = input.value.trim() === ""; }
16
+ input.addEventListener('input', checkInputValue);
17
+ checkInputValue();
18
+ };
19
+ """
20
+ )
21
+
22
+
23
+ def SearchBox(with_border=False, query_value=""):
24
+ grid_cls = "grid gap-2 items-center p-3 bg-muted/80 dark:bg-muted/40 w-full"
25
+
26
+ if with_border:
27
+ grid_cls = "grid gap-2 p-3 rounded-md border border-input bg-muted/80 dark:bg-muted/40 w-full ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:border-input"
28
+
29
+ return Form(
30
+ Div(
31
+ Lucide(icon="search", cls="absolute left-2 top-2 text-muted-foreground"),
32
+ Input(
33
+ placeholder="Enter your search query...",
34
+ name="query",
35
+ value=query_value,
36
+ id="search-input",
37
+ cls="text-base pl-10 border-transparent ring-offset-transparent ring-0 focus-visible:ring-transparent",
38
+ style="font-size: 1rem",
39
+ autofocus=True,
40
+ ),
41
+ cls="relative",
42
+ ),
43
+ Div(
44
+ Span("controls", cls="text-muted-foreground"),
45
+ Button(
46
+ Lucide(icon="arrow-right", size="21"),
47
+ size="sm",
48
+ type="submit",
49
+ data_button="search-button",
50
+ disabled=True,
51
+ ),
52
+ cls="flex justify-between",
53
+ ),
54
+ check_input_script(),
55
+ action=f"/search?query={quote_plus(query_value)}", # This takes the user to /search with the loading message
56
+ method="GET",
57
+ hx_get=f"/fetch_results?query={quote_plus(query_value)}", # This fetches the results asynchronously
58
+ hx_trigger="load", # Trigger this after the page loads
59
+ hx_target="#search-results", # Update the search results div dynamically
60
+ hx_swap="outerHTML", # Replace the search results div entirely
61
+ hx_indicator="#loading-indicator", # Show the loading indicator while fetching results
62
+ cls=grid_cls,
63
+ )
64
+
65
+
66
+ def SampleQueries():
67
+ sample_queries = [
68
+ "What is the future of energy storage?",
69
+ "What is sustainable energy?",
70
+ "How to reduce carbon emissions?",
71
+ ]
72
+
73
+ query_badges = []
74
+ for query in sample_queries:
75
+ query_badges.append(
76
+ A(
77
+ Badge(
78
+ Div(
79
+ Lucide(
80
+ icon="text-search", size="18", cls="text-muted-foreground"
81
+ ),
82
+ Span(query, cls="text-base font-normal"),
83
+ cls="flex gap-2 items-center",
84
+ ),
85
+ variant="outline",
86
+ cls="text-base font-normal text-muted-foreground",
87
+ ),
88
+ href=f"/search?query={quote_plus(query)}",
89
+ cls="no-underline",
90
+ )
91
+ )
92
+
93
+ return Div(*query_badges, cls="grid gap-2 justify-items-center")
94
+
95
+
96
+ def Hero():
97
+ return Div(
98
+ H1(
99
+ "Vespa.Ai + ColPali",
100
+ 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",
101
+ ),
102
+ P(
103
+ "Efficient Document Retrieval with Vision Language Models",
104
+ cls="text-lg md:text-2xl text-muted-foreground md:tracking-wide",
105
+ ),
106
+ cls="grid gap-5 text-center",
107
+ )
108
+
109
+
110
+ def Home():
111
+ return Div(
112
+ Div(
113
+ Hero(),
114
+ SearchBox(with_border=True),
115
+ SampleQueries(),
116
+ cls="grid gap-8 -mt-[34vh]",
117
+ ),
118
+ cls="grid w-full h-full max-w-screen-md items-center gap-4 mx-auto",
119
+ )
120
+
121
+
122
+ def Search(request, search_results=[]):
123
+ query_value = request.query_params.get("query", "").strip()
124
+
125
+ return Div(
126
+ Div(
127
+ SearchBox(
128
+ query_value=query_value
129
+ ), # Pass the query value to pre-fill the SearchBox
130
+ Div(
131
+ LoadingMessage(), # Show the loading message initially
132
+ id="search-results", # This will be replaced by the search results
133
+ ),
134
+ cls="grid",
135
+ ),
136
+ cls="grid",
137
+ )
138
+
139
+
140
+ def LoadingMessage():
141
+ return Div(
142
+ P("Loading... Please wait.", cls="text-base text-center"),
143
+ cls="p-10 text-center text-muted-foreground",
144
+ id="loading-indicator",
145
+ )
146
+
147
+
148
+ def SearchResult(results=[]):
149
+ if not results:
150
+ return Div(
151
+ P(
152
+ "No results found for your query.",
153
+ cls="text-muted-foreground text-base text-center",
154
+ ),
155
+ cls="grid p-10",
156
+ )
157
+
158
+ # Otherwise, display the search results
159
+ result_items = []
160
+ for result in results:
161
+ fields = result["fields"] # Extract the 'fields' part of each result
162
+ base64_image = (
163
+ f"data:image/jpeg;base64,{fields['image']}" # Format base64 image
164
+ )
165
+ # Print the fields that start with 'sim_map'
166
+ for key, value in fields.items():
167
+ if key.startswith("sim_map"):
168
+ print(f"{key}")
169
+ result_items.append(
170
+ Div(
171
+ Div(
172
+ Img(src=base64_image, alt=fields["title"], cls="max-w-full h-auto"),
173
+ cls="bg-background px-3 py-5",
174
+ ),
175
+ Div(
176
+ Div(
177
+ H2(fields["title"], cls="text-xl font-semibold"),
178
+ P(
179
+ fields["text"], cls="text-muted-foreground"
180
+ ), # Use the URL as the description
181
+ cls="text-sm grid gap-y-4",
182
+ ),
183
+ cls="bg-background px-3 py-5",
184
+ ),
185
+ cls="grid grid-cols-subgrid col-span-2",
186
+ )
187
+ )
188
+
189
+ return Div(
190
+ *result_items,
191
+ cls="grid grid-cols-2 gap-px bg-border",
192
+ id="search-results", # This will be the target for HTMX updates
193
+ )
frontend/layout.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fasthtml.components import Div, Img, Nav, Title, Body, Header, Main
2
+ from fasthtml.xtend import A
3
+ from lucide_fasthtml import Lucide
4
+ from shad4fast import Button, Separator
5
+
6
+
7
+ def Logo():
8
+ return Div(
9
+ Img(src='https://assets.vespa.ai/logos/vespa-logo-black.svg', alt='Vespa Logo', cls='h-full dark:hidden'),
10
+ Img(src='https://assets.vespa.ai/logos/vespa-logo-white.svg', alt='Vespa Logo Dark Mode',
11
+ cls='h-full hidden dark:block'),
12
+ cls='h-[27px]'
13
+ )
14
+
15
+
16
+ def ThemeToggle(variant="ghost", cls=None, **kwargs):
17
+ return Button(
18
+ Lucide("sun", cls="dark:flex hidden"),
19
+ Lucide("moon", cls="dark:hidden"),
20
+ variant=variant,
21
+ size="icon",
22
+ cls=f"theme-toggle {cls}",
23
+ **kwargs,
24
+ )
25
+
26
+
27
+ def Links():
28
+ return Nav(
29
+ A(
30
+ Button(Lucide(icon="github"), size="icon", variant="ghost"),
31
+ href="https://github.com/vespa-engine/vespa",
32
+ target="_blank",
33
+ ),
34
+ A(
35
+ Button(Lucide(icon="slack"), size="icon", variant="ghost"),
36
+ href="https://slack.vespa.ai",
37
+ target="_blank",
38
+ ),
39
+ Separator(orientation="vertical"),
40
+ ThemeToggle(),
41
+ cls='flex items-center space-x-3'
42
+ )
43
+
44
+
45
+ def Layout(*c, **kwargs):
46
+ return (
47
+ Title('Visual Retrieval ColPali'),
48
+ Body(
49
+ Header(
50
+ A(Logo(), href="/"),
51
+ Links(),
52
+ cls='min-h-[55px] h-[55px] w-full flex items-center justify-between px-4'
53
+ ),
54
+ Main(
55
+ *c, **kwargs,
56
+ cls='flex-1 h-full'
57
+ ),
58
+ cls='h-full flex flex-col'
59
+ ),
60
+ )
globals.css ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 240 10% 3.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 240 10% 3.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 240 10% 3.9%;
13
+ --primary: 240 5.9% 10%;
14
+ --primary-foreground: 0 0% 98%;
15
+ --secondary: 240 4.8% 95.9%;
16
+ --secondary-foreground: 240 5.9% 10%;
17
+ --muted: 240 4.8% 95.9%;
18
+ --muted-foreground: 240 3.8% 46.1%;
19
+ --accent: 240 4.8% 95.9%;
20
+ --accent-foreground: 240 5.9% 10%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 0 0% 98%;
23
+ --border: 240 5.9% 90%;
24
+ --input: 240 5.9% 90%;
25
+ --ring: 240 5.9% 10%;
26
+ --radius: 0.5rem;
27
+ --chart-1: 12 76% 61%;
28
+ --chart-2: 173 58% 39%;
29
+ --chart-3: 197 37% 24%;
30
+ --chart-4: 43 74% 66%;
31
+ --chart-5: 27 87% 67%;
32
+ }
33
+
34
+ .dark {
35
+ --background: 240 10% 3.9%;
36
+ --foreground: 0 0% 98%;
37
+ --card: 240 10% 3.9%;
38
+ --card-foreground: 0 0% 98%;
39
+ --popover: 240 10% 3.9%;
40
+ --popover-foreground: 0 0% 98%;
41
+ --primary: 0 0% 98%;
42
+ --primary-foreground: 240 5.9% 10%;
43
+ --secondary: 240 3.7% 15.9%;
44
+ --secondary-foreground: 0 0% 98%;
45
+ --muted: 240 3.7% 15.9%;
46
+ --muted-foreground: 240 5% 64.9%;
47
+ --accent: 240 3.7% 15.9%;
48
+ --accent-foreground: 0 0% 98%;
49
+ --destructive: 0 62.8% 30.6%;
50
+ --destructive-foreground: 0 0% 98%;
51
+ --border: 240 3.7% 15.9%;
52
+ --input: 240 3.7% 15.9%;
53
+ --ring: 240 4.9% 83.9%;
54
+ --chart-1: 220 70% 50%;
55
+ --chart-2: 160 60% 45%;
56
+ --chart-3: 30 80% 55%;
57
+ --chart-4: 280 65% 60%;
58
+ --chart-5: 340 75% 55%;
59
+ }
60
+ }
61
+
62
+ @layer base {
63
+ :root:has(.no-bg-scroll) {
64
+ overflow: hidden;
65
+ }
66
+
67
+ * {
68
+ @apply border-border;
69
+ }
70
+
71
+ body {
72
+ @apply bg-background text-foreground antialiased min-h-screen;
73
+ font-feature-settings: "rlig" 1, "calt" 1;
74
+ }
75
+ }
76
+
77
+ @layer utilities {
78
+
79
+ /* Hide scrollbar for Chrome, Safari and Opera */
80
+ .no-scrollbar::-webkit-scrollbar {
81
+ display: none;
82
+ }
83
+
84
+ /* Hide scrollbar for IE, Edge and Firefox */
85
+ .no-scrollbar {
86
+ -webkit-overflow-scrolling: touch;
87
+ -ms-overflow-style: none;
88
+ /* IE and Edge */
89
+ scrollbar-width: none;
90
+ /* Firefox */
91
+ }
92
+ }
93
+
94
+ @keyframes slideInFromTop {
95
+ from {
96
+ transform: translateY(-100%);
97
+ }
98
+
99
+ to {
100
+ transform: translateY(0);
101
+ }
102
+ }
103
+
104
+ @keyframes slideInFromBottom {
105
+ from {
106
+ transform: translateY(100%);
107
+ }
108
+
109
+ to {
110
+ transform: translateY(0);
111
+ }
112
+ }
113
+
114
+ .toast {
115
+ animation-duration: 0.2s;
116
+ animation-fill-mode: forwards;
117
+ }
118
+
119
+ @media (max-width: 640px) {
120
+ .toast {
121
+ animation-name: slideInFromTop;
122
+ }
123
+ }
124
+
125
+ @media (min-width: 641px) {
126
+ .toast {
127
+ animation-name: slideInFromBottom;
128
+ }
129
+ }
130
+
131
+ @keyframes fade-in {
132
+ from {
133
+ opacity: 0;
134
+ }
135
+ to {
136
+ opacity: 1;
137
+ }
138
+ }
139
+
140
+ @keyframes slide-up {
141
+ from {
142
+ transform: translateY(20px);
143
+ opacity: 0;
144
+ }
145
+ to {
146
+ transform: translateY(0);
147
+ opacity: 1;
148
+ }
149
+ }
150
+
151
+ .animate-fade-in {
152
+ animation: fade-in 1s ease-out forwards;
153
+ }
154
+
155
+ .animate-slide-up {
156
+ animation: slide-up 1s ease-out forwards;
157
+ }
hello.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fasthtml.common import *
2
+ from importlib.util import find_spec
3
+
4
+ # Run find_spec for all the modules (imports will be removed by ruff if not used. This is just to check if the modules are available, and should be removed)ß
5
+ for module in ["torch", "einops", "PIL", "vidore_benchmark", "colpali_engine"]:
6
+ spec = find_spec(module)
7
+ assert spec is not None, f"Module {module} not found"
8
+
9
+ app, rt = fast_app()
10
+
11
+
12
+ @rt("/")
13
+ def get():
14
+ return Div(P("Hello World!"), hx_get="/change")
15
+
16
+
17
+ serve()
icons.py ADDED
@@ -0,0 +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>"}
main.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+
4
+ from fasthtml.common import *
5
+ from shad4fast import *
6
+ from vespa.application import Vespa
7
+
8
+ from backend.colpali import load_model, get_result_from_query
9
+ from backend.vespa_app import get_vespa_app
10
+ from frontend.app import Home, Search, SearchResult, SearchBox
11
+ from frontend.layout import Layout
12
+
13
+ highlight_js_theme_link = Link(id="highlight-theme", rel="stylesheet", href="")
14
+ highlight_js_theme = Script(src="/static/js/highlightjs-theme.js")
15
+ highlight_js = HighlightJS(
16
+ langs=["python", "javascript", "java", "json", "xml"],
17
+ dark="github-dark",
18
+ light="github",
19
+ )
20
+
21
+ app, rt = fast_app(
22
+ htmlkw={"cls": "h-full"},
23
+ pico=False,
24
+ hdrs=(
25
+ ShadHead(tw_cdn=False, theme_handle=True),
26
+ highlight_js,
27
+ highlight_js_theme_link,
28
+ highlight_js_theme,
29
+ ),
30
+ )
31
+ vespa_app: Vespa = get_vespa_app()
32
+
33
+
34
+ class ModelManager:
35
+ _instance = None
36
+ model = None
37
+ processor = None
38
+
39
+ @staticmethod
40
+ def get_instance():
41
+ if ModelManager._instance is None:
42
+ ModelManager._instance = ModelManager()
43
+ ModelManager._instance.initialize_model_and_processor()
44
+ return ModelManager._instance
45
+
46
+ def initialize_model_and_processor(self):
47
+ if self.model is None or self.processor is None: # Ensure no reinitialization
48
+ self.model, self.processor = load_model()
49
+ if self.model is None or self.processor is None:
50
+ print("Failed to initialize model or processor at startup")
51
+ else:
52
+ print("Model and processor loaded at startup")
53
+
54
+
55
+ @rt("/static/{filepath:path}")
56
+ def serve_static(filepath: str):
57
+ return FileResponse(f"./static/{filepath}")
58
+
59
+
60
+ @rt("/")
61
+ def get():
62
+ return Layout(Home())
63
+
64
+
65
+ @rt("/search")
66
+ def get(request):
67
+ # Extract the 'query' parameter from the URL using query_params
68
+ query_value = request.query_params.get("query", "").strip()
69
+
70
+ # Always render the SearchBox first
71
+ if not query_value:
72
+ # Show SearchBox and a message for missing query
73
+ return Layout(
74
+ Div(
75
+ SearchBox(query_value=query_value),
76
+ Div(
77
+ P(
78
+ "No query provided. Please enter a query.",
79
+ cls="text-center text-muted-foreground",
80
+ ),
81
+ cls="p-10",
82
+ ),
83
+ cls="grid",
84
+ )
85
+ )
86
+
87
+ # Show the loading message if a query is provided
88
+ return Layout(Search(request)) # Show SearchBox and Loading message initially
89
+
90
+
91
+ @rt("/fetch_results")
92
+ def get(request, query: str, nn: bool = True):
93
+ # Check if the request came from HTMX; if not, redirect to /search
94
+ if "hx-request" not in request.headers:
95
+ return RedirectResponse("/search")
96
+
97
+ # Extract the 'query' parameter from the URL
98
+
99
+ # Fetch model and processor
100
+ manager = ModelManager.get_instance()
101
+ model = manager.model
102
+ processor = manager.processor
103
+
104
+ # Fetch real search results from Vespa
105
+ result = asyncio.run(
106
+ get_result_from_query(
107
+ vespa_app,
108
+ processor=processor,
109
+ model=model,
110
+ query=query,
111
+ nn=nn,
112
+ gen_sim_map=True,
113
+ )
114
+ )
115
+
116
+ # Extract search results from the result payload
117
+ search_results = (
118
+ result["root"]["children"]
119
+ if "root" in result and "children" in result["root"]
120
+ else []
121
+ )
122
+
123
+ # Directly return the search results without the full page layout
124
+ return SearchResult(search_results)
125
+
126
+
127
+ @rt("/app")
128
+ def get():
129
+ return Layout(Div(P(f"Connected to Vespa at {vespa_app.url}"), cls="p-4"))
130
+
131
+
132
+ @rt("/run_query")
133
+ def get(query: str, nn: bool = False):
134
+ # dummy-function to avoid running the query every time
135
+ # result = get_result_dummy(query, nn)
136
+ # If we want to run real, uncomment the following lines
137
+ model, processor = get_model_and_processor()
138
+ result = asyncio.run(
139
+ get_result_from_query(
140
+ vespa_app, processor=processor, model=model, query=query, nn=nn
141
+ )
142
+ )
143
+ # model, processor = get_model_and_processor()
144
+ # result = asyncio.run(
145
+ # get_result_from_query(vespa_app, processor=processor, model=model, query=query, nn=nn)
146
+ # )
147
+ return Layout(Div(H1("Result"), Pre(Code(json.dumps(result, indent=2))), cls="p-4"))
148
+
149
+
150
+ if __name__ == "__main__":
151
+ # ModelManager.get_instance() # Initialize once at startup
152
+ serve()
output.css ADDED
@@ -0,0 +1,2541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *, ::before, ::after {
2
+ --tw-border-spacing-x: 0;
3
+ --tw-border-spacing-y: 0;
4
+ --tw-translate-x: 0;
5
+ --tw-translate-y: 0;
6
+ --tw-rotate: 0;
7
+ --tw-skew-x: 0;
8
+ --tw-skew-y: 0;
9
+ --tw-scale-x: 1;
10
+ --tw-scale-y: 1;
11
+ --tw-pan-x: ;
12
+ --tw-pan-y: ;
13
+ --tw-pinch-zoom: ;
14
+ --tw-scroll-snap-strictness: proximity;
15
+ --tw-gradient-from-position: ;
16
+ --tw-gradient-via-position: ;
17
+ --tw-gradient-to-position: ;
18
+ --tw-ordinal: ;
19
+ --tw-slashed-zero: ;
20
+ --tw-numeric-figure: ;
21
+ --tw-numeric-spacing: ;
22
+ --tw-numeric-fraction: ;
23
+ --tw-ring-inset: ;
24
+ --tw-ring-offset-width: 0px;
25
+ --tw-ring-offset-color: #fff;
26
+ --tw-ring-color: rgb(59 130 246 / 0.5);
27
+ --tw-ring-offset-shadow: 0 0 #0000;
28
+ --tw-ring-shadow: 0 0 #0000;
29
+ --tw-shadow: 0 0 #0000;
30
+ --tw-shadow-colored: 0 0 #0000;
31
+ --tw-blur: ;
32
+ --tw-brightness: ;
33
+ --tw-contrast: ;
34
+ --tw-grayscale: ;
35
+ --tw-hue-rotate: ;
36
+ --tw-invert: ;
37
+ --tw-saturate: ;
38
+ --tw-sepia: ;
39
+ --tw-drop-shadow: ;
40
+ --tw-backdrop-blur: ;
41
+ --tw-backdrop-brightness: ;
42
+ --tw-backdrop-contrast: ;
43
+ --tw-backdrop-grayscale: ;
44
+ --tw-backdrop-hue-rotate: ;
45
+ --tw-backdrop-invert: ;
46
+ --tw-backdrop-opacity: ;
47
+ --tw-backdrop-saturate: ;
48
+ --tw-backdrop-sepia: ;
49
+ --tw-contain-size: ;
50
+ --tw-contain-layout: ;
51
+ --tw-contain-paint: ;
52
+ --tw-contain-style: ;
53
+ }
54
+
55
+ ::backdrop {
56
+ --tw-border-spacing-x: 0;
57
+ --tw-border-spacing-y: 0;
58
+ --tw-translate-x: 0;
59
+ --tw-translate-y: 0;
60
+ --tw-rotate: 0;
61
+ --tw-skew-x: 0;
62
+ --tw-skew-y: 0;
63
+ --tw-scale-x: 1;
64
+ --tw-scale-y: 1;
65
+ --tw-pan-x: ;
66
+ --tw-pan-y: ;
67
+ --tw-pinch-zoom: ;
68
+ --tw-scroll-snap-strictness: proximity;
69
+ --tw-gradient-from-position: ;
70
+ --tw-gradient-via-position: ;
71
+ --tw-gradient-to-position: ;
72
+ --tw-ordinal: ;
73
+ --tw-slashed-zero: ;
74
+ --tw-numeric-figure: ;
75
+ --tw-numeric-spacing: ;
76
+ --tw-numeric-fraction: ;
77
+ --tw-ring-inset: ;
78
+ --tw-ring-offset-width: 0px;
79
+ --tw-ring-offset-color: #fff;
80
+ --tw-ring-color: rgb(59 130 246 / 0.5);
81
+ --tw-ring-offset-shadow: 0 0 #0000;
82
+ --tw-ring-shadow: 0 0 #0000;
83
+ --tw-shadow: 0 0 #0000;
84
+ --tw-shadow-colored: 0 0 #0000;
85
+ --tw-blur: ;
86
+ --tw-brightness: ;
87
+ --tw-contrast: ;
88
+ --tw-grayscale: ;
89
+ --tw-hue-rotate: ;
90
+ --tw-invert: ;
91
+ --tw-saturate: ;
92
+ --tw-sepia: ;
93
+ --tw-drop-shadow: ;
94
+ --tw-backdrop-blur: ;
95
+ --tw-backdrop-brightness: ;
96
+ --tw-backdrop-contrast: ;
97
+ --tw-backdrop-grayscale: ;
98
+ --tw-backdrop-hue-rotate: ;
99
+ --tw-backdrop-invert: ;
100
+ --tw-backdrop-opacity: ;
101
+ --tw-backdrop-saturate: ;
102
+ --tw-backdrop-sepia: ;
103
+ --tw-contain-size: ;
104
+ --tw-contain-layout: ;
105
+ --tw-contain-paint: ;
106
+ --tw-contain-style: ;
107
+ }
108
+
109
+ /*
110
+ ! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com
111
+ */
112
+
113
+ /*
114
+ 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
115
+ 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
116
+ */
117
+
118
+ *,
119
+ ::before,
120
+ ::after {
121
+ box-sizing: border-box;
122
+ /* 1 */
123
+ border-width: 0;
124
+ /* 2 */
125
+ border-style: solid;
126
+ /* 2 */
127
+ border-color: #e5e7eb;
128
+ /* 2 */
129
+ }
130
+
131
+ ::before,
132
+ ::after {
133
+ --tw-content: '';
134
+ }
135
+
136
+ /*
137
+ 1. Use a consistent sensible line-height in all browsers.
138
+ 2. Prevent adjustments of font size after orientation changes in iOS.
139
+ 3. Use a more readable tab size.
140
+ 4. Use the user's configured `sans` font-family by default.
141
+ 5. Use the user's configured `sans` font-feature-settings by default.
142
+ 6. Use the user's configured `sans` font-variation-settings by default.
143
+ 7. Disable tap highlights on iOS
144
+ */
145
+
146
+ html,
147
+ :host {
148
+ line-height: 1.5;
149
+ /* 1 */
150
+ -webkit-text-size-adjust: 100%;
151
+ /* 2 */
152
+ -moz-tab-size: 4;
153
+ /* 3 */
154
+ -o-tab-size: 4;
155
+ tab-size: 4;
156
+ /* 3 */
157
+ font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
158
+ /* 4 */
159
+ font-feature-settings: normal;
160
+ /* 5 */
161
+ font-variation-settings: normal;
162
+ /* 6 */
163
+ -webkit-tap-highlight-color: transparent;
164
+ /* 7 */
165
+ }
166
+
167
+ /*
168
+ 1. Remove the margin in all browsers.
169
+ 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
170
+ */
171
+
172
+ body {
173
+ margin: 0;
174
+ /* 1 */
175
+ line-height: inherit;
176
+ /* 2 */
177
+ }
178
+
179
+ /*
180
+ 1. Add the correct height in Firefox.
181
+ 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
182
+ 3. Ensure horizontal rules are visible by default.
183
+ */
184
+
185
+ hr {
186
+ height: 0;
187
+ /* 1 */
188
+ color: inherit;
189
+ /* 2 */
190
+ border-top-width: 1px;
191
+ /* 3 */
192
+ }
193
+
194
+ /*
195
+ Add the correct text decoration in Chrome, Edge, and Safari.
196
+ */
197
+
198
+ abbr:where([title]) {
199
+ -webkit-text-decoration: underline dotted;
200
+ text-decoration: underline dotted;
201
+ }
202
+
203
+ /*
204
+ Remove the default font size and weight for headings.
205
+ */
206
+
207
+ h1,
208
+ h2,
209
+ h3,
210
+ h4,
211
+ h5,
212
+ h6 {
213
+ font-size: inherit;
214
+ font-weight: inherit;
215
+ }
216
+
217
+ /*
218
+ Reset links to optimize for opt-in styling instead of opt-out.
219
+ */
220
+
221
+ a {
222
+ color: inherit;
223
+ text-decoration: inherit;
224
+ }
225
+
226
+ /*
227
+ Add the correct font weight in Edge and Safari.
228
+ */
229
+
230
+ b,
231
+ strong {
232
+ font-weight: bolder;
233
+ }
234
+
235
+ /*
236
+ 1. Use the user's configured `mono` font-family by default.
237
+ 2. Use the user's configured `mono` font-feature-settings by default.
238
+ 3. Use the user's configured `mono` font-variation-settings by default.
239
+ 4. Correct the odd `em` font sizing in all browsers.
240
+ */
241
+
242
+ code,
243
+ kbd,
244
+ samp,
245
+ pre {
246
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
247
+ /* 1 */
248
+ font-feature-settings: normal;
249
+ /* 2 */
250
+ font-variation-settings: normal;
251
+ /* 3 */
252
+ font-size: 1em;
253
+ /* 4 */
254
+ }
255
+
256
+ /*
257
+ Add the correct font size in all browsers.
258
+ */
259
+
260
+ small {
261
+ font-size: 80%;
262
+ }
263
+
264
+ /*
265
+ Prevent `sub` and `sup` elements from affecting the line height in all browsers.
266
+ */
267
+
268
+ sub,
269
+ sup {
270
+ font-size: 75%;
271
+ line-height: 0;
272
+ position: relative;
273
+ vertical-align: baseline;
274
+ }
275
+
276
+ sub {
277
+ bottom: -0.25em;
278
+ }
279
+
280
+ sup {
281
+ top: -0.5em;
282
+ }
283
+
284
+ /*
285
+ 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
286
+ 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
287
+ 3. Remove gaps between table borders by default.
288
+ */
289
+
290
+ table {
291
+ text-indent: 0;
292
+ /* 1 */
293
+ border-color: inherit;
294
+ /* 2 */
295
+ border-collapse: collapse;
296
+ /* 3 */
297
+ }
298
+
299
+ /*
300
+ 1. Change the font styles in all browsers.
301
+ 2. Remove the margin in Firefox and Safari.
302
+ 3. Remove default padding in all browsers.
303
+ */
304
+
305
+ button,
306
+ input,
307
+ optgroup,
308
+ select,
309
+ textarea {
310
+ font-family: inherit;
311
+ /* 1 */
312
+ font-feature-settings: inherit;
313
+ /* 1 */
314
+ font-variation-settings: inherit;
315
+ /* 1 */
316
+ font-size: 100%;
317
+ /* 1 */
318
+ font-weight: inherit;
319
+ /* 1 */
320
+ line-height: inherit;
321
+ /* 1 */
322
+ letter-spacing: inherit;
323
+ /* 1 */
324
+ color: inherit;
325
+ /* 1 */
326
+ margin: 0;
327
+ /* 2 */
328
+ padding: 0;
329
+ /* 3 */
330
+ }
331
+
332
+ /*
333
+ Remove the inheritance of text transform in Edge and Firefox.
334
+ */
335
+
336
+ button,
337
+ select {
338
+ text-transform: none;
339
+ }
340
+
341
+ /*
342
+ 1. Correct the inability to style clickable types in iOS and Safari.
343
+ 2. Remove default button styles.
344
+ */
345
+
346
+ button,
347
+ input:where([type='button']),
348
+ input:where([type='reset']),
349
+ input:where([type='submit']) {
350
+ -webkit-appearance: button;
351
+ /* 1 */
352
+ background-color: transparent;
353
+ /* 2 */
354
+ background-image: none;
355
+ /* 2 */
356
+ }
357
+
358
+ /*
359
+ Use the modern Firefox focus style for all focusable elements.
360
+ */
361
+
362
+ :-moz-focusring {
363
+ outline: auto;
364
+ }
365
+
366
+ /*
367
+ Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
368
+ */
369
+
370
+ :-moz-ui-invalid {
371
+ box-shadow: none;
372
+ }
373
+
374
+ /*
375
+ Add the correct vertical alignment in Chrome and Firefox.
376
+ */
377
+
378
+ progress {
379
+ vertical-align: baseline;
380
+ }
381
+
382
+ /*
383
+ Correct the cursor style of increment and decrement buttons in Safari.
384
+ */
385
+
386
+ ::-webkit-inner-spin-button,
387
+ ::-webkit-outer-spin-button {
388
+ height: auto;
389
+ }
390
+
391
+ /*
392
+ 1. Correct the odd appearance in Chrome and Safari.
393
+ 2. Correct the outline style in Safari.
394
+ */
395
+
396
+ [type='search'] {
397
+ -webkit-appearance: textfield;
398
+ /* 1 */
399
+ outline-offset: -2px;
400
+ /* 2 */
401
+ }
402
+
403
+ /*
404
+ Remove the inner padding in Chrome and Safari on macOS.
405
+ */
406
+
407
+ ::-webkit-search-decoration {
408
+ -webkit-appearance: none;
409
+ }
410
+
411
+ /*
412
+ 1. Correct the inability to style clickable types in iOS and Safari.
413
+ 2. Change font properties to `inherit` in Safari.
414
+ */
415
+
416
+ ::-webkit-file-upload-button {
417
+ -webkit-appearance: button;
418
+ /* 1 */
419
+ font: inherit;
420
+ /* 2 */
421
+ }
422
+
423
+ /*
424
+ Add the correct display in Chrome and Safari.
425
+ */
426
+
427
+ summary {
428
+ display: list-item;
429
+ }
430
+
431
+ /*
432
+ Removes the default spacing and border for appropriate elements.
433
+ */
434
+
435
+ blockquote,
436
+ dl,
437
+ dd,
438
+ h1,
439
+ h2,
440
+ h3,
441
+ h4,
442
+ h5,
443
+ h6,
444
+ hr,
445
+ figure,
446
+ p,
447
+ pre {
448
+ margin: 0;
449
+ }
450
+
451
+ fieldset {
452
+ margin: 0;
453
+ padding: 0;
454
+ }
455
+
456
+ legend {
457
+ padding: 0;
458
+ }
459
+
460
+ ol,
461
+ ul,
462
+ menu {
463
+ list-style: none;
464
+ margin: 0;
465
+ padding: 0;
466
+ }
467
+
468
+ /*
469
+ Reset default styling for dialogs.
470
+ */
471
+
472
+ dialog {
473
+ padding: 0;
474
+ }
475
+
476
+ /*
477
+ Prevent resizing textareas horizontally by default.
478
+ */
479
+
480
+ textarea {
481
+ resize: vertical;
482
+ }
483
+
484
+ /*
485
+ 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
486
+ 2. Set the default placeholder color to the user's configured gray 400 color.
487
+ */
488
+
489
+ input::-moz-placeholder, textarea::-moz-placeholder {
490
+ opacity: 1;
491
+ /* 1 */
492
+ color: #9ca3af;
493
+ /* 2 */
494
+ }
495
+
496
+ input::placeholder,
497
+ textarea::placeholder {
498
+ opacity: 1;
499
+ /* 1 */
500
+ color: #9ca3af;
501
+ /* 2 */
502
+ }
503
+
504
+ /*
505
+ Set the default cursor for buttons.
506
+ */
507
+
508
+ button,
509
+ [role="button"] {
510
+ cursor: pointer;
511
+ }
512
+
513
+ /*
514
+ Make sure disabled buttons don't get the pointer cursor.
515
+ */
516
+
517
+ :disabled {
518
+ cursor: default;
519
+ }
520
+
521
+ /*
522
+ 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
523
+ 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
524
+ This can trigger a poorly considered lint error in some tools but is included by design.
525
+ */
526
+
527
+ img,
528
+ svg,
529
+ video,
530
+ canvas,
531
+ audio,
532
+ iframe,
533
+ embed,
534
+ object {
535
+ display: block;
536
+ /* 1 */
537
+ vertical-align: middle;
538
+ /* 2 */
539
+ }
540
+
541
+ /*
542
+ Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
543
+ */
544
+
545
+ img,
546
+ video {
547
+ max-width: 100%;
548
+ height: auto;
549
+ }
550
+
551
+ /* Make elements with the HTML hidden attribute stay hidden by default */
552
+
553
+ [hidden] {
554
+ display: none;
555
+ }
556
+
557
+ :root {
558
+ --background: 0 0% 100%;
559
+ --foreground: 240 10% 3.9%;
560
+ --card: 0 0% 100%;
561
+ --card-foreground: 240 10% 3.9%;
562
+ --popover: 0 0% 100%;
563
+ --popover-foreground: 240 10% 3.9%;
564
+ --primary: 240 5.9% 10%;
565
+ --primary-foreground: 0 0% 98%;
566
+ --secondary: 240 4.8% 95.9%;
567
+ --secondary-foreground: 240 5.9% 10%;
568
+ --muted: 240 4.8% 95.9%;
569
+ --muted-foreground: 240 3.8% 46.1%;
570
+ --accent: 240 4.8% 95.9%;
571
+ --accent-foreground: 240 5.9% 10%;
572
+ --destructive: 0 84.2% 60.2%;
573
+ --destructive-foreground: 0 0% 98%;
574
+ --border: 240 5.9% 90%;
575
+ --input: 240 5.9% 90%;
576
+ --ring: 240 5.9% 10%;
577
+ --radius: 0.5rem;
578
+ --chart-1: 12 76% 61%;
579
+ --chart-2: 173 58% 39%;
580
+ --chart-3: 197 37% 24%;
581
+ --chart-4: 43 74% 66%;
582
+ --chart-5: 27 87% 67%;
583
+ }
584
+
585
+ .dark {
586
+ --background: 240 10% 3.9%;
587
+ --foreground: 0 0% 98%;
588
+ --card: 240 10% 3.9%;
589
+ --card-foreground: 0 0% 98%;
590
+ --popover: 240 10% 3.9%;
591
+ --popover-foreground: 0 0% 98%;
592
+ --primary: 0 0% 98%;
593
+ --primary-foreground: 240 5.9% 10%;
594
+ --secondary: 240 3.7% 15.9%;
595
+ --secondary-foreground: 0 0% 98%;
596
+ --muted: 240 3.7% 15.9%;
597
+ --muted-foreground: 240 5% 64.9%;
598
+ --accent: 240 3.7% 15.9%;
599
+ --accent-foreground: 0 0% 98%;
600
+ --destructive: 0 62.8% 30.6%;
601
+ --destructive-foreground: 0 0% 98%;
602
+ --border: 240 3.7% 15.9%;
603
+ --input: 240 3.7% 15.9%;
604
+ --ring: 240 4.9% 83.9%;
605
+ --chart-1: 220 70% 50%;
606
+ --chart-2: 160 60% 45%;
607
+ --chart-3: 30 80% 55%;
608
+ --chart-4: 280 65% 60%;
609
+ --chart-5: 340 75% 55%;
610
+ }
611
+
612
+ :root:has(.no-bg-scroll) {
613
+ overflow: hidden;
614
+ }
615
+
616
+ * {
617
+ border-color: hsl(var(--border));
618
+ }
619
+
620
+ body {
621
+ min-height: 100vh;
622
+ background-color: hsl(var(--background));
623
+ color: hsl(var(--foreground));
624
+ -webkit-font-smoothing: antialiased;
625
+ -moz-osx-font-smoothing: grayscale;
626
+ font-feature-settings: "rlig" 1, "calt" 1;
627
+ }
628
+
629
+ .container {
630
+ width: 100%;
631
+ margin-right: auto;
632
+ margin-left: auto;
633
+ padding-right: 2rem;
634
+ padding-left: 2rem;
635
+ }
636
+
637
+ @media (min-width: 1400px) {
638
+ .container {
639
+ max-width: 1400px;
640
+ }
641
+ }
642
+
643
+ .sr-only {
644
+ position: absolute;
645
+ width: 1px;
646
+ height: 1px;
647
+ padding: 0;
648
+ margin: -1px;
649
+ overflow: hidden;
650
+ clip: rect(0, 0, 0, 0);
651
+ white-space: nowrap;
652
+ border-width: 0;
653
+ }
654
+
655
+ .pointer-events-none {
656
+ pointer-events: none;
657
+ }
658
+
659
+ .pointer-events-auto {
660
+ pointer-events: auto;
661
+ }
662
+
663
+ .static {
664
+ position: static;
665
+ }
666
+
667
+ .fixed {
668
+ position: fixed;
669
+ }
670
+
671
+ .absolute {
672
+ position: absolute;
673
+ }
674
+
675
+ .relative {
676
+ position: relative;
677
+ }
678
+
679
+ .inset-0 {
680
+ inset: 0px;
681
+ }
682
+
683
+ .inset-x-0 {
684
+ left: 0px;
685
+ right: 0px;
686
+ }
687
+
688
+ .inset-y-0 {
689
+ top: 0px;
690
+ bottom: 0px;
691
+ }
692
+
693
+ .-bottom-12 {
694
+ bottom: -3rem;
695
+ }
696
+
697
+ .-left-12 {
698
+ left: -3rem;
699
+ }
700
+
701
+ .-right-12 {
702
+ right: -3rem;
703
+ }
704
+
705
+ .-top-12 {
706
+ top: -3rem;
707
+ }
708
+
709
+ .bottom-0 {
710
+ bottom: 0px;
711
+ }
712
+
713
+ .left-0 {
714
+ left: 0px;
715
+ }
716
+
717
+ .left-1\/2 {
718
+ left: 50%;
719
+ }
720
+
721
+ .left-2 {
722
+ left: 0.5rem;
723
+ }
724
+
725
+ .left-\[50\%\] {
726
+ left: 50%;
727
+ }
728
+
729
+ .right-0 {
730
+ right: 0px;
731
+ }
732
+
733
+ .right-2 {
734
+ right: 0.5rem;
735
+ }
736
+
737
+ .right-4 {
738
+ right: 1rem;
739
+ }
740
+
741
+ .top-0 {
742
+ top: 0px;
743
+ }
744
+
745
+ .top-1\/2 {
746
+ top: 50%;
747
+ }
748
+
749
+ .top-2 {
750
+ top: 0.5rem;
751
+ }
752
+
753
+ .top-4 {
754
+ top: 1rem;
755
+ }
756
+
757
+ .top-\[50\%\] {
758
+ top: 50%;
759
+ }
760
+
761
+ .z-50 {
762
+ z-index: 50;
763
+ }
764
+
765
+ .z-\[100\] {
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;
776
+ }
777
+
778
+ .mx-auto {
779
+ margin-left: auto;
780
+ margin-right: auto;
781
+ }
782
+
783
+ .my-1 {
784
+ margin-top: 0.25rem;
785
+ margin-bottom: 0.25rem;
786
+ }
787
+
788
+ .-ml-4 {
789
+ margin-left: -1rem;
790
+ }
791
+
792
+ .-mt-4 {
793
+ margin-top: -1rem;
794
+ }
795
+
796
+ .-mt-\[34vh\] {
797
+ margin-top: -34vh;
798
+ }
799
+
800
+ .mb-1 {
801
+ margin-bottom: 0.25rem;
802
+ }
803
+
804
+ .mt-2 {
805
+ margin-top: 0.5rem;
806
+ }
807
+
808
+ .block {
809
+ display: block;
810
+ }
811
+
812
+ .flex {
813
+ display: flex;
814
+ }
815
+
816
+ .inline-flex {
817
+ display: inline-flex;
818
+ }
819
+
820
+ .table {
821
+ display: table;
822
+ }
823
+
824
+ .grid {
825
+ display: grid;
826
+ }
827
+
828
+ .contents {
829
+ display: contents;
830
+ }
831
+
832
+ .hidden {
833
+ display: none;
834
+ }
835
+
836
+ .aspect-square {
837
+ aspect-ratio: 1 / 1;
838
+ }
839
+
840
+ .size-4 {
841
+ width: 1rem;
842
+ height: 1rem;
843
+ }
844
+
845
+ .h-10 {
846
+ height: 2.5rem;
847
+ }
848
+
849
+ .h-11 {
850
+ height: 2.75rem;
851
+ }
852
+
853
+ .h-12 {
854
+ height: 3rem;
855
+ }
856
+
857
+ .h-2 {
858
+ height: 0.5rem;
859
+ }
860
+
861
+ .h-2\.5 {
862
+ height: 0.625rem;
863
+ }
864
+
865
+ .h-3\.5 {
866
+ height: 0.875rem;
867
+ }
868
+
869
+ .h-4 {
870
+ height: 1rem;
871
+ }
872
+
873
+ .h-5 {
874
+ height: 1.25rem;
875
+ }
876
+
877
+ .h-6 {
878
+ height: 1.5rem;
879
+ }
880
+
881
+ .h-8 {
882
+ height: 2rem;
883
+ }
884
+
885
+ .h-9 {
886
+ height: 2.25rem;
887
+ }
888
+
889
+ .h-\[1\.5px\] {
890
+ height: 1.5px;
891
+ }
892
+
893
+ .h-\[27px\] {
894
+ height: 27px;
895
+ }
896
+
897
+ .h-\[55px\] {
898
+ height: 55px;
899
+ }
900
+
901
+ .h-\[var\(--radix-select-trigger-height\)\] {
902
+ height: var(--radix-select-trigger-height);
903
+ }
904
+
905
+ .h-auto {
906
+ height: auto;
907
+ }
908
+
909
+ .h-full {
910
+ height: 100%;
911
+ }
912
+
913
+ .h-px {
914
+ height: 1px;
915
+ }
916
+
917
+ .max-h-96 {
918
+ max-height: 24rem;
919
+ }
920
+
921
+ .max-h-screen {
922
+ max-height: 100vh;
923
+ }
924
+
925
+ .min-h-\[55px\] {
926
+ min-height: 55px;
927
+ }
928
+
929
+ .min-h-\[80px\] {
930
+ min-height: 80px;
931
+ }
932
+
933
+ .min-h-screen {
934
+ min-height: 100vh;
935
+ }
936
+
937
+ .w-10 {
938
+ width: 2.5rem;
939
+ }
940
+
941
+ .w-11 {
942
+ width: 2.75rem;
943
+ }
944
+
945
+ .w-2\.5 {
946
+ width: 0.625rem;
947
+ }
948
+
949
+ .w-3\.5 {
950
+ width: 0.875rem;
951
+ }
952
+
953
+ .w-3\/4 {
954
+ width: 75%;
955
+ }
956
+
957
+ .w-4 {
958
+ width: 1rem;
959
+ }
960
+
961
+ .w-5 {
962
+ width: 1.25rem;
963
+ }
964
+
965
+ .w-8 {
966
+ width: 2rem;
967
+ }
968
+
969
+ .w-\[1\.5px\] {
970
+ width: 1.5px;
971
+ }
972
+
973
+ .w-full {
974
+ width: 100%;
975
+ }
976
+
977
+ .min-w-0 {
978
+ min-width: 0px;
979
+ }
980
+
981
+ .min-w-\[8rem\] {
982
+ min-width: 8rem;
983
+ }
984
+
985
+ .min-w-\[var\(--radix-select-trigger-width\)\] {
986
+ min-width: var(--radix-select-trigger-width);
987
+ }
988
+
989
+ .max-w-full {
990
+ max-width: 100%;
991
+ }
992
+
993
+ .max-w-lg {
994
+ max-width: 32rem;
995
+ }
996
+
997
+ .max-w-screen-md {
998
+ max-width: 768px;
999
+ }
1000
+
1001
+ .flex-1 {
1002
+ flex: 1 1 0%;
1003
+ }
1004
+
1005
+ .shrink-0 {
1006
+ flex-shrink: 0;
1007
+ }
1008
+
1009
+ .grow {
1010
+ flex-grow: 1;
1011
+ }
1012
+
1013
+ .grow-0 {
1014
+ flex-grow: 0;
1015
+ }
1016
+
1017
+ .basis-full {
1018
+ flex-basis: 100%;
1019
+ }
1020
+
1021
+ .caption-bottom {
1022
+ caption-side: bottom;
1023
+ }
1024
+
1025
+ .-translate-x-1\/2 {
1026
+ --tw-translate-x: -50%;
1027
+ 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));
1028
+ }
1029
+
1030
+ .-translate-y-1\/2 {
1031
+ --tw-translate-y: -50%;
1032
+ 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));
1033
+ }
1034
+
1035
+ .translate-x-\[-50\%\] {
1036
+ --tw-translate-x: -50%;
1037
+ 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));
1038
+ }
1039
+
1040
+ .translate-y-\[-50\%\] {
1041
+ --tw-translate-y: -50%;
1042
+ 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));
1043
+ }
1044
+
1045
+ .rotate-90 {
1046
+ --tw-rotate: 90deg;
1047
+ 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));
1048
+ }
1049
+
1050
+ .transform {
1051
+ 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));
1052
+ }
1053
+
1054
+ .cursor-default {
1055
+ cursor: default;
1056
+ }
1057
+
1058
+ .cursor-pointer {
1059
+ cursor: pointer;
1060
+ }
1061
+
1062
+ .touch-none {
1063
+ touch-action: none;
1064
+ }
1065
+
1066
+ .select-none {
1067
+ -webkit-user-select: none;
1068
+ -moz-user-select: none;
1069
+ user-select: none;
1070
+ }
1071
+
1072
+ .resize {
1073
+ resize: both;
1074
+ }
1075
+
1076
+ .grid-cols-2 {
1077
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1078
+ }
1079
+
1080
+ .grid-cols-subgrid {
1081
+ grid-template-columns: subgrid;
1082
+ }
1083
+
1084
+ .flex-col {
1085
+ flex-direction: column;
1086
+ }
1087
+
1088
+ .flex-col-reverse {
1089
+ flex-direction: column-reverse;
1090
+ }
1091
+
1092
+ .items-center {
1093
+ align-items: center;
1094
+ }
1095
+
1096
+ .justify-center {
1097
+ justify-content: center;
1098
+ }
1099
+
1100
+ .justify-between {
1101
+ justify-content: space-between;
1102
+ }
1103
+
1104
+ .justify-items-center {
1105
+ justify-items: center;
1106
+ }
1107
+
1108
+ .gap-2 {
1109
+ gap: 0.5rem;
1110
+ }
1111
+
1112
+ .gap-4 {
1113
+ gap: 1rem;
1114
+ }
1115
+
1116
+ .gap-5 {
1117
+ gap: 1.25rem;
1118
+ }
1119
+
1120
+ .gap-8 {
1121
+ gap: 2rem;
1122
+ }
1123
+
1124
+ .gap-px {
1125
+ gap: 1px;
1126
+ }
1127
+
1128
+ .gap-3 {
1129
+ gap: 0.75rem;
1130
+ }
1131
+
1132
+ .gap-y-4 {
1133
+ row-gap: 1rem;
1134
+ }
1135
+
1136
+ .space-x-3 > :not([hidden]) ~ :not([hidden]) {
1137
+ --tw-space-x-reverse: 0;
1138
+ margin-right: calc(0.75rem * var(--tw-space-x-reverse));
1139
+ margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
1140
+ }
1141
+
1142
+ .space-x-4 > :not([hidden]) ~ :not([hidden]) {
1143
+ --tw-space-x-reverse: 0;
1144
+ margin-right: calc(1rem * var(--tw-space-x-reverse));
1145
+ margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
1146
+ }
1147
+
1148
+ .space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
1149
+ --tw-space-y-reverse: 0;
1150
+ margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
1151
+ margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
1152
+ }
1153
+
1154
+ .space-y-2 > :not([hidden]) ~ :not([hidden]) {
1155
+ --tw-space-y-reverse: 0;
1156
+ margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
1157
+ margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
1158
+ }
1159
+
1160
+ .self-stretch {
1161
+ align-self: stretch;
1162
+ }
1163
+
1164
+ .overflow-auto {
1165
+ overflow: auto;
1166
+ }
1167
+
1168
+ .overflow-hidden {
1169
+ overflow: hidden;
1170
+ }
1171
+
1172
+ .whitespace-nowrap {
1173
+ white-space: nowrap;
1174
+ }
1175
+
1176
+ .\!rounded-full {
1177
+ border-radius: 9999px !important;
1178
+ }
1179
+
1180
+ .rounded-\[inherit\] {
1181
+ border-radius: inherit;
1182
+ }
1183
+
1184
+ .rounded-full {
1185
+ border-radius: 9999px;
1186
+ }
1187
+
1188
+ .rounded-lg {
1189
+ border-radius: var(--radius);
1190
+ }
1191
+
1192
+ .rounded-md {
1193
+ border-radius: calc(var(--radius) - 2px);
1194
+ }
1195
+
1196
+ .rounded-sm {
1197
+ border-radius: calc(var(--radius) - 4px);
1198
+ }
1199
+
1200
+ .border {
1201
+ border-width: 1px;
1202
+ }
1203
+
1204
+ .border-2 {
1205
+ border-width: 2px;
1206
+ }
1207
+
1208
+ .border-b {
1209
+ border-bottom-width: 1px;
1210
+ }
1211
+
1212
+ .border-l {
1213
+ border-left-width: 1px;
1214
+ }
1215
+
1216
+ .border-r {
1217
+ border-right-width: 1px;
1218
+ }
1219
+
1220
+ .border-t {
1221
+ border-top-width: 1px;
1222
+ }
1223
+
1224
+ .border-destructive {
1225
+ border-color: hsl(var(--destructive));
1226
+ }
1227
+
1228
+ .border-destructive\/50 {
1229
+ border-color: hsl(var(--destructive) / 0.5);
1230
+ }
1231
+
1232
+ .border-input {
1233
+ border-color: hsl(var(--input));
1234
+ }
1235
+
1236
+ .border-primary {
1237
+ border-color: hsl(var(--primary));
1238
+ }
1239
+
1240
+ .border-transparent {
1241
+ border-color: transparent;
1242
+ }
1243
+
1244
+ .border-l-transparent {
1245
+ border-left-color: transparent;
1246
+ }
1247
+
1248
+ .border-t-transparent {
1249
+ border-top-color: transparent;
1250
+ }
1251
+
1252
+ .bg-background {
1253
+ background-color: hsl(var(--background));
1254
+ }
1255
+
1256
+ .bg-black\/80 {
1257
+ background-color: rgb(0 0 0 / 0.8);
1258
+ }
1259
+
1260
+ .bg-blue-500 {
1261
+ --tw-bg-opacity: 1;
1262
+ background-color: rgb(59 130 246 / var(--tw-bg-opacity));
1263
+ }
1264
+
1265
+ .bg-border {
1266
+ background-color: hsl(var(--border));
1267
+ }
1268
+
1269
+ .bg-card {
1270
+ background-color: hsl(var(--card));
1271
+ }
1272
+
1273
+ .bg-destructive {
1274
+ background-color: hsl(var(--destructive));
1275
+ }
1276
+
1277
+ .bg-input {
1278
+ background-color: hsl(var(--input));
1279
+ }
1280
+
1281
+ .bg-muted {
1282
+ background-color: hsl(var(--muted));
1283
+ }
1284
+
1285
+ .bg-muted\/50 {
1286
+ background-color: hsl(var(--muted) / 0.5);
1287
+ }
1288
+
1289
+ .bg-muted\/80 {
1290
+ background-color: hsl(var(--muted) / 0.8);
1291
+ }
1292
+
1293
+ .bg-popover {
1294
+ background-color: hsl(var(--popover));
1295
+ }
1296
+
1297
+ .bg-primary {
1298
+ background-color: hsl(var(--primary));
1299
+ }
1300
+
1301
+ .bg-red-300 {
1302
+ --tw-bg-opacity: 1;
1303
+ background-color: rgb(252 165 165 / var(--tw-bg-opacity));
1304
+ }
1305
+
1306
+ .bg-red-500 {
1307
+ --tw-bg-opacity: 1;
1308
+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
1309
+ }
1310
+
1311
+ .bg-secondary {
1312
+ background-color: hsl(var(--secondary));
1313
+ }
1314
+
1315
+ .bg-gradient-to-r {
1316
+ background-image: linear-gradient(to right, var(--tw-gradient-stops));
1317
+ }
1318
+
1319
+ .from-black {
1320
+ --tw-gradient-from: #000 var(--tw-gradient-from-position);
1321
+ --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
1322
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1323
+ }
1324
+
1325
+ .to-gray-700 {
1326
+ --tw-gradient-to: #374151 var(--tw-gradient-to-position);
1327
+ }
1328
+
1329
+ .bg-clip-text {
1330
+ -webkit-background-clip: text;
1331
+ background-clip: text;
1332
+ }
1333
+
1334
+ .fill-current {
1335
+ fill: currentColor;
1336
+ }
1337
+
1338
+ .p-1 {
1339
+ padding: 0.25rem;
1340
+ }
1341
+
1342
+ .p-16 {
1343
+ padding: 4rem;
1344
+ }
1345
+
1346
+ .p-3 {
1347
+ padding: 0.75rem;
1348
+ }
1349
+
1350
+ .p-4 {
1351
+ padding: 1rem;
1352
+ }
1353
+
1354
+ .p-6 {
1355
+ padding: 1.5rem;
1356
+ }
1357
+
1358
+ .p-\[1px\] {
1359
+ padding: 1px;
1360
+ }
1361
+
1362
+ .p-10 {
1363
+ padding: 2.5rem;
1364
+ }
1365
+
1366
+ .px-2\.5 {
1367
+ padding-left: 0.625rem;
1368
+ padding-right: 0.625rem;
1369
+ }
1370
+
1371
+ .px-3 {
1372
+ padding-left: 0.75rem;
1373
+ padding-right: 0.75rem;
1374
+ }
1375
+
1376
+ .px-4 {
1377
+ padding-left: 1rem;
1378
+ padding-right: 1rem;
1379
+ }
1380
+
1381
+ .px-8 {
1382
+ padding-left: 2rem;
1383
+ padding-right: 2rem;
1384
+ }
1385
+
1386
+ .py-0\.5 {
1387
+ padding-top: 0.125rem;
1388
+ padding-bottom: 0.125rem;
1389
+ }
1390
+
1391
+ .py-1 {
1392
+ padding-top: 0.25rem;
1393
+ padding-bottom: 0.25rem;
1394
+ }
1395
+
1396
+ .py-1\.5 {
1397
+ padding-top: 0.375rem;
1398
+ padding-bottom: 0.375rem;
1399
+ }
1400
+
1401
+ .py-2 {
1402
+ padding-top: 0.5rem;
1403
+ padding-bottom: 0.5rem;
1404
+ }
1405
+
1406
+ .py-5 {
1407
+ padding-top: 1.25rem;
1408
+ padding-bottom: 1.25rem;
1409
+ }
1410
+
1411
+ .pl-10 {
1412
+ padding-left: 2.5rem;
1413
+ }
1414
+
1415
+ .pl-4 {
1416
+ padding-left: 1rem;
1417
+ }
1418
+
1419
+ .pl-8 {
1420
+ padding-left: 2rem;
1421
+ }
1422
+
1423
+ .pr-2 {
1424
+ padding-right: 0.5rem;
1425
+ }
1426
+
1427
+ .pr-8 {
1428
+ padding-right: 2rem;
1429
+ }
1430
+
1431
+ .pt-0 {
1432
+ padding-top: 0px;
1433
+ }
1434
+
1435
+ .pt-4 {
1436
+ padding-top: 1rem;
1437
+ }
1438
+
1439
+ .text-left {
1440
+ text-align: left;
1441
+ }
1442
+
1443
+ .text-center {
1444
+ text-align: center;
1445
+ }
1446
+
1447
+ .align-middle {
1448
+ vertical-align: middle;
1449
+ }
1450
+
1451
+ .text-2xl {
1452
+ font-size: 1.5rem;
1453
+ line-height: 2rem;
1454
+ }
1455
+
1456
+ .text-3xl {
1457
+ font-size: 1.875rem;
1458
+ line-height: 2.25rem;
1459
+ }
1460
+
1461
+ .text-5xl {
1462
+ font-size: 3rem;
1463
+ line-height: 1;
1464
+ }
1465
+
1466
+ .text-base {
1467
+ font-size: 1rem;
1468
+ line-height: 1.5rem;
1469
+ }
1470
+
1471
+ .text-lg {
1472
+ font-size: 1.125rem;
1473
+ line-height: 1.75rem;
1474
+ }
1475
+
1476
+ .text-sm {
1477
+ font-size: 0.875rem;
1478
+ line-height: 1.25rem;
1479
+ }
1480
+
1481
+ .text-xl {
1482
+ font-size: 1.25rem;
1483
+ line-height: 1.75rem;
1484
+ }
1485
+
1486
+ .text-xs {
1487
+ font-size: 0.75rem;
1488
+ line-height: 1rem;
1489
+ }
1490
+
1491
+ .font-bold {
1492
+ font-weight: 700;
1493
+ }
1494
+
1495
+ .font-medium {
1496
+ font-weight: 500;
1497
+ }
1498
+
1499
+ .font-semibold {
1500
+ font-weight: 600;
1501
+ }
1502
+
1503
+ .font-normal {
1504
+ font-weight: 400;
1505
+ }
1506
+
1507
+ .leading-none {
1508
+ line-height: 1;
1509
+ }
1510
+
1511
+ .tracking-tight {
1512
+ letter-spacing: -0.025em;
1513
+ }
1514
+
1515
+ .tracking-wide {
1516
+ letter-spacing: 0.025em;
1517
+ }
1518
+
1519
+ .text-card-foreground {
1520
+ color: hsl(var(--card-foreground));
1521
+ }
1522
+
1523
+ .text-current {
1524
+ color: currentColor;
1525
+ }
1526
+
1527
+ .text-destructive {
1528
+ color: hsl(var(--destructive));
1529
+ }
1530
+
1531
+ .text-destructive-foreground {
1532
+ color: hsl(var(--destructive-foreground));
1533
+ }
1534
+
1535
+ .text-foreground {
1536
+ color: hsl(var(--foreground));
1537
+ }
1538
+
1539
+ .text-foreground\/50 {
1540
+ color: hsl(var(--foreground) / 0.5);
1541
+ }
1542
+
1543
+ .text-gray-800 {
1544
+ --tw-text-opacity: 1;
1545
+ color: rgb(31 41 55 / var(--tw-text-opacity));
1546
+ }
1547
+
1548
+ .text-gray-900 {
1549
+ --tw-text-opacity: 1;
1550
+ color: rgb(17 24 39 / var(--tw-text-opacity));
1551
+ }
1552
+
1553
+ .text-muted-foreground {
1554
+ color: hsl(var(--muted-foreground));
1555
+ }
1556
+
1557
+ .text-popover-foreground {
1558
+ color: hsl(var(--popover-foreground));
1559
+ }
1560
+
1561
+ .text-primary {
1562
+ color: hsl(var(--primary));
1563
+ }
1564
+
1565
+ .text-primary-foreground {
1566
+ color: hsl(var(--primary-foreground));
1567
+ }
1568
+
1569
+ .text-secondary-foreground {
1570
+ color: hsl(var(--secondary-foreground));
1571
+ }
1572
+
1573
+ .text-transparent {
1574
+ color: transparent;
1575
+ }
1576
+
1577
+ .underline {
1578
+ text-decoration-line: underline;
1579
+ }
1580
+
1581
+ .no-underline {
1582
+ text-decoration-line: none;
1583
+ }
1584
+
1585
+ .underline-offset-4 {
1586
+ text-underline-offset: 4px;
1587
+ }
1588
+
1589
+ .antialiased {
1590
+ -webkit-font-smoothing: antialiased;
1591
+ -moz-osx-font-smoothing: grayscale;
1592
+ }
1593
+
1594
+ .opacity-0 {
1595
+ opacity: 0;
1596
+ }
1597
+
1598
+ .opacity-50 {
1599
+ opacity: 0.5;
1600
+ }
1601
+
1602
+ .opacity-70 {
1603
+ opacity: 0.7;
1604
+ }
1605
+
1606
+ .opacity-90 {
1607
+ opacity: 0.9;
1608
+ }
1609
+
1610
+ .shadow-lg {
1611
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1612
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1613
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1614
+ }
1615
+
1616
+ .shadow-md {
1617
+ --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
1618
+ --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
1619
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1620
+ }
1621
+
1622
+ .shadow-sm {
1623
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
1624
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
1625
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1626
+ }
1627
+
1628
+ .outline-none {
1629
+ outline: 2px solid transparent;
1630
+ outline-offset: 2px;
1631
+ }
1632
+
1633
+ .outline {
1634
+ outline-style: solid;
1635
+ }
1636
+
1637
+ .ring {
1638
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1639
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1640
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1641
+ }
1642
+
1643
+ .ring-0 {
1644
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1645
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1646
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1647
+ }
1648
+
1649
+ .ring-offset-background {
1650
+ --tw-ring-offset-color: hsl(var(--background));
1651
+ }
1652
+
1653
+ .ring-offset-transparent {
1654
+ --tw-ring-offset-color: transparent;
1655
+ }
1656
+
1657
+ .blur {
1658
+ --tw-blur: blur(8px);
1659
+ 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);
1660
+ }
1661
+
1662
+ .filter {
1663
+ 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);
1664
+ }
1665
+
1666
+ .transition {
1667
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1668
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1669
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1670
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1671
+ transition-duration: 150ms;
1672
+ }
1673
+
1674
+ .transition-all {
1675
+ transition-property: all;
1676
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1677
+ transition-duration: 150ms;
1678
+ }
1679
+
1680
+ .transition-colors {
1681
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
1682
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1683
+ transition-duration: 150ms;
1684
+ }
1685
+
1686
+ .transition-opacity {
1687
+ transition-property: opacity;
1688
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1689
+ transition-duration: 150ms;
1690
+ }
1691
+
1692
+ .transition-transform {
1693
+ transition-property: transform;
1694
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1695
+ transition-duration: 150ms;
1696
+ }
1697
+
1698
+ .duration-200 {
1699
+ transition-duration: 200ms;
1700
+ }
1701
+
1702
+ .duration-300 {
1703
+ transition-duration: 300ms;
1704
+ }
1705
+
1706
+ .ease-in-out {
1707
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1708
+ }
1709
+
1710
+ .ease-out {
1711
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1712
+ }
1713
+
1714
+ @keyframes enter {
1715
+ from {
1716
+ opacity: var(--tw-enter-opacity, 1);
1717
+ transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0));
1718
+ }
1719
+ }
1720
+
1721
+ @keyframes exit {
1722
+ to {
1723
+ opacity: var(--tw-exit-opacity, 1);
1724
+ transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0));
1725
+ }
1726
+ }
1727
+
1728
+ .animate-in {
1729
+ animation-name: enter;
1730
+ animation-duration: 150ms;
1731
+ --tw-enter-opacity: initial;
1732
+ --tw-enter-scale: initial;
1733
+ --tw-enter-rotate: initial;
1734
+ --tw-enter-translate-x: initial;
1735
+ --tw-enter-translate-y: initial;
1736
+ }
1737
+
1738
+ .animate-out {
1739
+ animation-name: exit;
1740
+ animation-duration: 150ms;
1741
+ --tw-exit-opacity: initial;
1742
+ --tw-exit-scale: initial;
1743
+ --tw-exit-rotate: initial;
1744
+ --tw-exit-translate-x: initial;
1745
+ --tw-exit-translate-y: initial;
1746
+ }
1747
+
1748
+ .fade-in {
1749
+ --tw-enter-opacity: 0;
1750
+ }
1751
+
1752
+ .fade-out {
1753
+ --tw-exit-opacity: 0;
1754
+ }
1755
+
1756
+ .zoom-in {
1757
+ --tw-enter-scale: 0;
1758
+ }
1759
+
1760
+ .zoom-out {
1761
+ --tw-exit-scale: 0;
1762
+ }
1763
+
1764
+ .spin-in {
1765
+ --tw-enter-rotate: 30deg;
1766
+ }
1767
+
1768
+ .spin-out {
1769
+ --tw-exit-rotate: 30deg;
1770
+ }
1771
+
1772
+ .slide-in-from-bottom {
1773
+ --tw-enter-translate-y: 100%;
1774
+ }
1775
+
1776
+ .slide-in-from-left {
1777
+ --tw-enter-translate-x: -100%;
1778
+ }
1779
+
1780
+ .slide-in-from-right {
1781
+ --tw-enter-translate-x: 100%;
1782
+ }
1783
+
1784
+ .slide-in-from-top {
1785
+ --tw-enter-translate-y: -100%;
1786
+ }
1787
+
1788
+ .slide-out-to-bottom {
1789
+ --tw-exit-translate-y: 100%;
1790
+ }
1791
+
1792
+ .slide-out-to-left {
1793
+ --tw-exit-translate-x: -100%;
1794
+ }
1795
+
1796
+ .slide-out-to-right {
1797
+ --tw-exit-translate-x: 100%;
1798
+ }
1799
+
1800
+ .slide-out-to-top {
1801
+ --tw-exit-translate-y: -100%;
1802
+ }
1803
+
1804
+ .duration-200 {
1805
+ animation-duration: 200ms;
1806
+ }
1807
+
1808
+ .duration-300 {
1809
+ animation-duration: 300ms;
1810
+ }
1811
+
1812
+ .ease-in-out {
1813
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1814
+ }
1815
+
1816
+ .ease-out {
1817
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
1818
+ }
1819
+
1820
+ .running {
1821
+ animation-play-state: running;
1822
+ }
1823
+
1824
+ .paused {
1825
+ animation-play-state: paused;
1826
+ }
1827
+
1828
+ /* Hide scrollbar for Chrome, Safari and Opera */
1829
+
1830
+ .no-scrollbar::-webkit-scrollbar {
1831
+ display: none;
1832
+ }
1833
+
1834
+ /* Hide scrollbar for IE, Edge and Firefox */
1835
+
1836
+ .no-scrollbar {
1837
+ -webkit-overflow-scrolling: touch;
1838
+ -ms-overflow-style: none;
1839
+ /* IE and Edge */
1840
+ scrollbar-width: none;
1841
+ /* Firefox */
1842
+ }
1843
+
1844
+ @keyframes slideInFromTop {
1845
+ from {
1846
+ transform: translateY(-100%);
1847
+ }
1848
+
1849
+ to {
1850
+ transform: translateY(0);
1851
+ }
1852
+ }
1853
+
1854
+ @keyframes slideInFromBottom {
1855
+ from {
1856
+ transform: translateY(100%);
1857
+ }
1858
+
1859
+ to {
1860
+ transform: translateY(0);
1861
+ }
1862
+ }
1863
+
1864
+ .toast {
1865
+ animation-duration: 0.2s;
1866
+ animation-fill-mode: forwards;
1867
+ }
1868
+
1869
+ @media (max-width: 640px) {
1870
+ .toast {
1871
+ animation-name: slideInFromTop;
1872
+ }
1873
+ }
1874
+
1875
+ @media (min-width: 641px) {
1876
+ .toast {
1877
+ animation-name: slideInFromBottom;
1878
+ }
1879
+ }
1880
+
1881
+ @keyframes fade-in {
1882
+ from {
1883
+ opacity: 0;
1884
+ }
1885
+
1886
+ to {
1887
+ opacity: 1;
1888
+ }
1889
+ }
1890
+
1891
+ @keyframes slide-up {
1892
+ from {
1893
+ transform: translateY(20px);
1894
+ opacity: 0;
1895
+ }
1896
+
1897
+ to {
1898
+ transform: translateY(0);
1899
+ opacity: 1;
1900
+ }
1901
+ }
1902
+
1903
+ .animate-fade-in {
1904
+ animation: fade-in 1s ease-out forwards;
1905
+ }
1906
+
1907
+ .animate-slide-up {
1908
+ animation: slide-up 1s ease-out forwards;
1909
+ }
1910
+
1911
+ :root:has(.data-\[state\=open\]\:no-bg-scroll[data-state="open"]) {
1912
+ overflow: hidden;
1913
+ }
1914
+
1915
+ :root:has(.group[data-state="open"] .group-data-\[state\=open\]\:no-bg-scroll) {
1916
+ overflow: hidden;
1917
+ }
1918
+
1919
+ .file\:border-0::file-selector-button {
1920
+ border-width: 0px;
1921
+ }
1922
+
1923
+ .file\:bg-transparent::file-selector-button {
1924
+ background-color: transparent;
1925
+ }
1926
+
1927
+ .file\:text-sm::file-selector-button {
1928
+ font-size: 0.875rem;
1929
+ line-height: 1.25rem;
1930
+ }
1931
+
1932
+ .file\:font-medium::file-selector-button {
1933
+ font-weight: 500;
1934
+ }
1935
+
1936
+ .placeholder\:text-muted-foreground::-moz-placeholder {
1937
+ color: hsl(var(--muted-foreground));
1938
+ }
1939
+
1940
+ .placeholder\:text-muted-foreground::placeholder {
1941
+ color: hsl(var(--muted-foreground));
1942
+ }
1943
+
1944
+ .focus-within\:border-input:focus-within {
1945
+ border-color: hsl(var(--input));
1946
+ }
1947
+
1948
+ .focus-within\:outline-none:focus-within {
1949
+ outline: 2px solid transparent;
1950
+ outline-offset: 2px;
1951
+ }
1952
+
1953
+ .focus-within\:ring-2:focus-within {
1954
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1955
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1956
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1957
+ }
1958
+
1959
+ .focus-within\:ring-ring:focus-within {
1960
+ --tw-ring-color: hsl(var(--ring));
1961
+ }
1962
+
1963
+ .focus-within\:ring-offset-2:focus-within {
1964
+ --tw-ring-offset-width: 2px;
1965
+ }
1966
+
1967
+ .hover\:border-white:hover {
1968
+ --tw-border-opacity: 1;
1969
+ border-color: rgb(255 255 255 / var(--tw-border-opacity));
1970
+ }
1971
+
1972
+ .hover\:border-\[text-muted-foreground\]:hover {
1973
+ border-color: text-muted-foreground;
1974
+ }
1975
+
1976
+ .hover\:bg-accent:hover {
1977
+ background-color: hsl(var(--accent));
1978
+ }
1979
+
1980
+ .hover\:bg-destructive\/80:hover {
1981
+ background-color: hsl(var(--destructive) / 0.8);
1982
+ }
1983
+
1984
+ .hover\:bg-destructive\/90:hover {
1985
+ background-color: hsl(var(--destructive) / 0.9);
1986
+ }
1987
+
1988
+ .hover\:bg-muted\/50:hover {
1989
+ background-color: hsl(var(--muted) / 0.5);
1990
+ }
1991
+
1992
+ .hover\:bg-primary\/80:hover {
1993
+ background-color: hsl(var(--primary) / 0.8);
1994
+ }
1995
+
1996
+ .hover\:bg-primary\/90:hover {
1997
+ background-color: hsl(var(--primary) / 0.9);
1998
+ }
1999
+
2000
+ .hover\:bg-secondary\/80:hover {
2001
+ background-color: hsl(var(--secondary) / 0.8);
2002
+ }
2003
+
2004
+ .hover\:bg-secondary:hover {
2005
+ background-color: hsl(var(--secondary));
2006
+ }
2007
+
2008
+ .hover\:text-accent-foreground:hover {
2009
+ color: hsl(var(--accent-foreground));
2010
+ }
2011
+
2012
+ .hover\:text-foreground:hover {
2013
+ color: hsl(var(--foreground));
2014
+ }
2015
+
2016
+ .hover\:text-primary-foreground:hover {
2017
+ color: hsl(var(--primary-foreground));
2018
+ }
2019
+
2020
+ .hover\:text-muted-foreground:hover {
2021
+ color: hsl(var(--muted-foreground));
2022
+ }
2023
+
2024
+ .hover\:underline:hover {
2025
+ text-decoration-line: underline;
2026
+ }
2027
+
2028
+ .hover\:opacity-100:hover {
2029
+ opacity: 1;
2030
+ }
2031
+
2032
+ .focus\:bg-accent:focus {
2033
+ background-color: hsl(var(--accent));
2034
+ }
2035
+
2036
+ .focus\:text-accent-foreground:focus {
2037
+ color: hsl(var(--accent-foreground));
2038
+ }
2039
+
2040
+ .focus\:opacity-100:focus {
2041
+ opacity: 1;
2042
+ }
2043
+
2044
+ .focus\:outline-none:focus {
2045
+ outline: 2px solid transparent;
2046
+ outline-offset: 2px;
2047
+ }
2048
+
2049
+ .focus\:ring-2:focus {
2050
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
2051
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
2052
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
2053
+ }
2054
+
2055
+ .focus\:ring-ring:focus {
2056
+ --tw-ring-color: hsl(var(--ring));
2057
+ }
2058
+
2059
+ .focus\:ring-offset-2:focus {
2060
+ --tw-ring-offset-width: 2px;
2061
+ }
2062
+
2063
+ .focus-visible\:outline-none:focus-visible {
2064
+ outline: 2px solid transparent;
2065
+ outline-offset: 2px;
2066
+ }
2067
+
2068
+ .focus-visible\:ring-2:focus-visible {
2069
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
2070
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
2071
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
2072
+ }
2073
+
2074
+ .focus-visible\:ring-ring:focus-visible {
2075
+ --tw-ring-color: hsl(var(--ring));
2076
+ }
2077
+
2078
+ .focus-visible\:ring-transparent:focus-visible {
2079
+ --tw-ring-color: transparent;
2080
+ }
2081
+
2082
+ .focus-visible\:ring-offset-2:focus-visible {
2083
+ --tw-ring-offset-width: 2px;
2084
+ }
2085
+
2086
+ .active\:ring:active {
2087
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
2088
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
2089
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
2090
+ }
2091
+
2092
+ .disabled\:pointer-events-none:disabled {
2093
+ pointer-events: none;
2094
+ }
2095
+
2096
+ .disabled\:cursor-not-allowed:disabled {
2097
+ cursor: not-allowed;
2098
+ }
2099
+
2100
+ .disabled\:opacity-50:disabled {
2101
+ opacity: 0.5;
2102
+ }
2103
+
2104
+ .group:hover .group-hover\:opacity-100 {
2105
+ opacity: 1;
2106
+ }
2107
+
2108
+ .group.destructive .group-\[\.destructive\]\:text-red-300 {
2109
+ --tw-text-opacity: 1;
2110
+ color: rgb(252 165 165 / var(--tw-text-opacity));
2111
+ }
2112
+
2113
+ .group.destructive .group-\[\.destructive\]\:hover\:text-red-50:hover {
2114
+ --tw-text-opacity: 1;
2115
+ color: rgb(254 242 242 / var(--tw-text-opacity));
2116
+ }
2117
+
2118
+ .group.destructive .group-\[\.destructive\]\:focus\:ring-red-400:focus {
2119
+ --tw-ring-opacity: 1;
2120
+ --tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity));
2121
+ }
2122
+
2123
+ .group.destructive .group-\[\.destructive\]\:focus\:ring-offset-red-600:focus {
2124
+ --tw-ring-offset-color: #dc2626;
2125
+ }
2126
+
2127
+ .peer:checked ~ .peer-checked\:bg-primary {
2128
+ background-color: hsl(var(--primary));
2129
+ }
2130
+
2131
+ .peer:checked ~ .peer-checked\:text-primary-foreground {
2132
+ color: hsl(var(--primary-foreground));
2133
+ }
2134
+
2135
+ .peer:disabled ~ .peer-disabled\:cursor-not-allowed {
2136
+ cursor: not-allowed;
2137
+ }
2138
+
2139
+ .peer:disabled ~ .peer-disabled\:opacity-50 {
2140
+ opacity: 0.5;
2141
+ }
2142
+
2143
+ .peer:disabled ~ .peer-disabled\:opacity-70 {
2144
+ opacity: 0.7;
2145
+ }
2146
+
2147
+ .data-\[disabled\]\:pointer-events-none[data-disabled] {
2148
+ pointer-events: none;
2149
+ }
2150
+
2151
+ .data-\[side\=bottom\]\:translate-y-1[data-side="bottom"] {
2152
+ --tw-translate-y: 0.25rem;
2153
+ 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));
2154
+ }
2155
+
2156
+ .data-\[side\=left\]\:-translate-x-1[data-side="left"] {
2157
+ --tw-translate-x: -0.25rem;
2158
+ 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));
2159
+ }
2160
+
2161
+ .data-\[side\=right\]\:translate-x-1[data-side="right"] {
2162
+ --tw-translate-x: 0.25rem;
2163
+ 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));
2164
+ }
2165
+
2166
+ .data-\[side\=top\]\:-translate-y-1[data-side="top"] {
2167
+ --tw-translate-y: -0.25rem;
2168
+ 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));
2169
+ }
2170
+
2171
+ .data-\[state\=active\]\:bg-background[data-state="active"] {
2172
+ background-color: hsl(var(--background));
2173
+ }
2174
+
2175
+ .data-\[state\=selected\]\:bg-muted[data-state="selected"] {
2176
+ background-color: hsl(var(--muted));
2177
+ }
2178
+
2179
+ .data-\[state\=active\]\:text-foreground[data-state="active"] {
2180
+ color: hsl(var(--foreground));
2181
+ }
2182
+
2183
+ .data-\[disabled\]\:opacity-50[data-disabled] {
2184
+ opacity: 0.5;
2185
+ }
2186
+
2187
+ .data-\[state\=active\]\:shadow-sm[data-state="active"] {
2188
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
2189
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
2190
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
2191
+ }
2192
+
2193
+ .data-\[state\=open\]\:animate-in[data-state="open"] {
2194
+ animation-name: enter;
2195
+ animation-duration: 150ms;
2196
+ --tw-enter-opacity: initial;
2197
+ --tw-enter-scale: initial;
2198
+ --tw-enter-rotate: initial;
2199
+ --tw-enter-translate-x: initial;
2200
+ --tw-enter-translate-y: initial;
2201
+ }
2202
+
2203
+ .data-\[state\=closed\]\:animate-out[data-state="closed"] {
2204
+ animation-name: exit;
2205
+ animation-duration: 150ms;
2206
+ --tw-exit-opacity: initial;
2207
+ --tw-exit-scale: initial;
2208
+ --tw-exit-rotate: initial;
2209
+ --tw-exit-translate-x: initial;
2210
+ --tw-exit-translate-y: initial;
2211
+ }
2212
+
2213
+ .data-\[state\=closed\]\:fade-out-0[data-state="closed"] {
2214
+ --tw-exit-opacity: 0;
2215
+ }
2216
+
2217
+ .data-\[state\=open\]\:fade-in-0[data-state="open"] {
2218
+ --tw-enter-opacity: 0;
2219
+ }
2220
+
2221
+ .data-\[state\=closed\]\:zoom-out-95[data-state="closed"] {
2222
+ --tw-exit-scale: .95;
2223
+ }
2224
+
2225
+ .data-\[state\=open\]\:zoom-in-95[data-state="open"] {
2226
+ --tw-enter-scale: .95;
2227
+ }
2228
+
2229
+ .data-\[side\=bottom\]\:slide-in-from-top-2[data-side="bottom"] {
2230
+ --tw-enter-translate-y: -0.5rem;
2231
+ }
2232
+
2233
+ .data-\[side\=left\]\:slide-in-from-right-2[data-side="left"] {
2234
+ --tw-enter-translate-x: 0.5rem;
2235
+ }
2236
+
2237
+ .data-\[side\=right\]\:slide-in-from-left-2[data-side="right"] {
2238
+ --tw-enter-translate-x: -0.5rem;
2239
+ }
2240
+
2241
+ .data-\[side\=top\]\:slide-in-from-bottom-2[data-side="top"] {
2242
+ --tw-enter-translate-y: 0.5rem;
2243
+ }
2244
+
2245
+ .group[data-checked="true"] .group-data-\[checked\=true\]\:flex {
2246
+ display: flex;
2247
+ }
2248
+
2249
+ .group[data-state="checked"] .group-data-\[state\=checked\]\:flex {
2250
+ display: flex;
2251
+ }
2252
+
2253
+ .group[data-state="open"] .group-data-\[state\=open\]\:bg-accent {
2254
+ background-color: hsl(var(--accent));
2255
+ }
2256
+
2257
+ .group[data-state="open"] .group-data-\[state\=open\]\:text-muted-foreground {
2258
+ color: hsl(var(--muted-foreground));
2259
+ }
2260
+
2261
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:duration-300 {
2262
+ transition-duration: 300ms;
2263
+ }
2264
+
2265
+ .group[data-state="open"] .group-data-\[state\=open\]\:duration-500 {
2266
+ transition-duration: 500ms;
2267
+ }
2268
+
2269
+ .group[data-state="open"] .group-data-\[state\=open\]\:animate-in {
2270
+ animation-name: enter;
2271
+ animation-duration: 150ms;
2272
+ --tw-enter-opacity: initial;
2273
+ --tw-enter-scale: initial;
2274
+ --tw-enter-rotate: initial;
2275
+ --tw-enter-translate-x: initial;
2276
+ --tw-enter-translate-y: initial;
2277
+ }
2278
+
2279
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:animate-out {
2280
+ animation-name: exit;
2281
+ animation-duration: 150ms;
2282
+ --tw-exit-opacity: initial;
2283
+ --tw-exit-scale: initial;
2284
+ --tw-exit-rotate: initial;
2285
+ --tw-exit-translate-x: initial;
2286
+ --tw-exit-translate-y: initial;
2287
+ }
2288
+
2289
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:fade-out-0 {
2290
+ --tw-exit-opacity: 0;
2291
+ }
2292
+
2293
+ .group[data-state="open"] .group-data-\[state\=open\]\:fade-in-0 {
2294
+ --tw-enter-opacity: 0;
2295
+ }
2296
+
2297
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:zoom-out-95 {
2298
+ --tw-exit-scale: .95;
2299
+ }
2300
+
2301
+ .group[data-state="open"] .group-data-\[state\=open\]\:zoom-in-95 {
2302
+ --tw-enter-scale: .95;
2303
+ }
2304
+
2305
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-bottom {
2306
+ --tw-exit-translate-y: 100%;
2307
+ }
2308
+
2309
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-left {
2310
+ --tw-exit-translate-x: -100%;
2311
+ }
2312
+
2313
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-left-1\/2 {
2314
+ --tw-exit-translate-x: -50%;
2315
+ }
2316
+
2317
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-right {
2318
+ --tw-exit-translate-x: 100%;
2319
+ }
2320
+
2321
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-top {
2322
+ --tw-exit-translate-y: -100%;
2323
+ }
2324
+
2325
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:slide-out-to-top-\[48\%\] {
2326
+ --tw-exit-translate-y: -48%;
2327
+ }
2328
+
2329
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-bottom {
2330
+ --tw-enter-translate-y: 100%;
2331
+ }
2332
+
2333
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-left {
2334
+ --tw-enter-translate-x: -100%;
2335
+ }
2336
+
2337
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-left-1\/2 {
2338
+ --tw-enter-translate-x: -50%;
2339
+ }
2340
+
2341
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-right {
2342
+ --tw-enter-translate-x: 100%;
2343
+ }
2344
+
2345
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-top {
2346
+ --tw-enter-translate-y: -100%;
2347
+ }
2348
+
2349
+ .group[data-state="open"] .group-data-\[state\=open\]\:slide-in-from-top-\[48\%\] {
2350
+ --tw-enter-translate-y: -48%;
2351
+ }
2352
+
2353
+ .group[data-state="closed"] .group-data-\[state\=closed\]\:duration-300 {
2354
+ animation-duration: 300ms;
2355
+ }
2356
+
2357
+ .group[data-state="open"] .group-data-\[state\=open\]\:duration-500 {
2358
+ animation-duration: 500ms;
2359
+ }
2360
+
2361
+ @media (min-width: 640px) {
2362
+ .sm\:bottom-0 {
2363
+ bottom: 0px;
2364
+ }
2365
+
2366
+ .sm\:right-0 {
2367
+ right: 0px;
2368
+ }
2369
+
2370
+ .sm\:top-auto {
2371
+ top: auto;
2372
+ }
2373
+
2374
+ .sm\:flex {
2375
+ display: flex;
2376
+ }
2377
+
2378
+ .sm\:max-w-sm {
2379
+ max-width: 24rem;
2380
+ }
2381
+
2382
+ .sm\:flex-row {
2383
+ flex-direction: row;
2384
+ }
2385
+
2386
+ .sm\:flex-col {
2387
+ flex-direction: column;
2388
+ }
2389
+
2390
+ .sm\:justify-end {
2391
+ justify-content: flex-end;
2392
+ }
2393
+
2394
+ .sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
2395
+ --tw-space-x-reverse: 0;
2396
+ margin-right: calc(0.5rem * var(--tw-space-x-reverse));
2397
+ margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
2398
+ }
2399
+
2400
+ .sm\:rounded-lg {
2401
+ border-radius: var(--radius);
2402
+ }
2403
+
2404
+ .sm\:text-left {
2405
+ text-align: left;
2406
+ }
2407
+ }
2408
+
2409
+ @media (min-width: 768px) {
2410
+ .md\:max-w-\[420px\] {
2411
+ max-width: 420px;
2412
+ }
2413
+
2414
+ .md\:text-2xl {
2415
+ font-size: 1.5rem;
2416
+ line-height: 2rem;
2417
+ }
2418
+
2419
+ .md\:text-7xl {
2420
+ font-size: 4.5rem;
2421
+ line-height: 1;
2422
+ }
2423
+
2424
+ .md\:tracking-wide {
2425
+ letter-spacing: 0.025em;
2426
+ }
2427
+
2428
+ .md\:tracking-wider {
2429
+ letter-spacing: 0.05em;
2430
+ }
2431
+ }
2432
+
2433
+ .dark\:block:where(.dark, .dark *) {
2434
+ display: block;
2435
+ }
2436
+
2437
+ .dark\:flex:where(.dark, .dark *) {
2438
+ display: flex;
2439
+ }
2440
+
2441
+ .dark\:hidden:where(.dark, .dark *) {
2442
+ display: none;
2443
+ }
2444
+
2445
+ .dark\:border-destructive:where(.dark, .dark *) {
2446
+ border-color: hsl(var(--destructive));
2447
+ }
2448
+
2449
+ .dark\:bg-muted\/40:where(.dark, .dark *) {
2450
+ background-color: hsl(var(--muted) / 0.4);
2451
+ }
2452
+
2453
+ .dark\:from-white:where(.dark, .dark *) {
2454
+ --tw-gradient-from: #fff var(--tw-gradient-from-position);
2455
+ --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
2456
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
2457
+ }
2458
+
2459
+ .dark\:to-gray-300:where(.dark, .dark *) {
2460
+ --tw-gradient-to: #d1d5db var(--tw-gradient-to-position);
2461
+ }
2462
+
2463
+ .dark\:hover\:border-black:hover:where(.dark, .dark *) {
2464
+ --tw-border-opacity: 1;
2465
+ border-color: rgb(0 0 0 / var(--tw-border-opacity));
2466
+ }
2467
+
2468
+ .hover\:dark\:border-black:where(.dark, .dark *):hover {
2469
+ --tw-border-opacity: 1;
2470
+ border-color: rgb(0 0 0 / var(--tw-border-opacity));
2471
+ }
2472
+
2473
+ .\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]) {
2474
+ padding-right: 0px;
2475
+ }
2476
+
2477
+ .\[\&\>span\]\:line-clamp-1>span {
2478
+ overflow: hidden;
2479
+ display: -webkit-box;
2480
+ -webkit-box-orient: vertical;
2481
+ -webkit-line-clamp: 1;
2482
+ }
2483
+
2484
+ .\[\&\>span\]\:translate-x-0>span {
2485
+ --tw-translate-x: 0px;
2486
+ 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));
2487
+ }
2488
+
2489
+ .peer:checked ~ .peer-checked\:\[\&\>span\]\:flex>span {
2490
+ display: flex;
2491
+ }
2492
+
2493
+ .peer:checked ~ .peer-checked\:\[\&\>span\]\:translate-x-5>span {
2494
+ --tw-translate-x: 1.25rem;
2495
+ 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));
2496
+ }
2497
+
2498
+ .\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div {
2499
+ --tw-translate-y: -3px;
2500
+ 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));
2501
+ }
2502
+
2503
+ .\[\&\>svg\]\:absolute>svg {
2504
+ position: absolute;
2505
+ }
2506
+
2507
+ .\[\&\>svg\]\:left-4>svg {
2508
+ left: 1rem;
2509
+ }
2510
+
2511
+ .\[\&\>svg\]\:top-4>svg {
2512
+ top: 1rem;
2513
+ }
2514
+
2515
+ .\[\&\>svg\]\:text-destructive>svg {
2516
+ color: hsl(var(--destructive));
2517
+ }
2518
+
2519
+ .\[\&\>svg\]\:text-foreground>svg {
2520
+ color: hsl(var(--foreground));
2521
+ }
2522
+
2523
+ .\[\&\>svg\~\*\]\:pl-7>svg~* {
2524
+ padding-left: 1.75rem;
2525
+ }
2526
+
2527
+ .\[\&\>tr\]\:last\:border-b-0:last-child>tr {
2528
+ border-bottom-width: 0px;
2529
+ }
2530
+
2531
+ .\[\&_p\]\:leading-relaxed p {
2532
+ line-height: 1.625;
2533
+ }
2534
+
2535
+ .\[\&_tr\:last-child\]\:border-0 tr:last-child {
2536
+ border-width: 0px;
2537
+ }
2538
+
2539
+ .\[\&_tr\]\:border-b tr {
2540
+ border-bottom-width: 1px;
2541
+ }
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "visual-retrieval-colpali"
3
+ version = "0.1.0"
4
+ description = "Visual retrieval with ColPali"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "Apache-2.0" }
8
+ dependencies = [
9
+ "python-fasthtml",
10
+ "huggingface-hub",
11
+ "pyvespa@git+https://github.com/vespa-engine/pyvespa",
12
+ "vespacli",
13
+ "torch",
14
+ "vidore-benchmark[interpretability]>=4.0.0,<5.0.0",
15
+ "colpali-engine",
16
+ "einops",
17
+ "pypdf",
18
+ "setuptools",
19
+ "python-dotenv",
20
+ "shad4fast>=1.2.1",
21
+ ]
22
+
23
+ # dev-dependencies
24
+ [project.optional-dependencies]
25
+ dev = [
26
+ "ruff",
27
+ "python-dotenv",
28
+ "huggingface_hub[cli]"
29
+ ]
query_vespa.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import torch
5
+ from torch.utils.data import DataLoader
6
+ from PIL import Image
7
+ import numpy as np
8
+ from typing import cast
9
+ import asyncio
10
+
11
+ from colpali_engine.models import ColPali, ColPaliProcessor
12
+ from colpali_engine.utils.torch_utils import get_torch_device
13
+ from vespa.application import Vespa
14
+ from vespa.io import VespaQueryResponse
15
+ from dotenv import load_dotenv
16
+ from pathlib import Path
17
+
18
+ MAX_QUERY_TERMS = 64
19
+ SAVEDIR = Path(__file__) / "output" / "images"
20
+ load_dotenv()
21
+
22
+
23
+ def process_queries(processor, queries, image):
24
+ inputs = processor(
25
+ images=[image] * len(queries), text=queries, return_tensors="pt", padding=True
26
+ )
27
+ return inputs
28
+
29
+
30
+ def display_query_results(query, response, hits=5):
31
+ query_time = response.json.get("timing", {}).get("searchtime", -1)
32
+ query_time = round(query_time, 2)
33
+ count = response.json.get("root", {}).get("fields", {}).get("totalCount", 0)
34
+ result_text = f"Query text: '{query}', query time {query_time}s, count={count}, top results:\n"
35
+
36
+ for i, hit in enumerate(response.hits[:hits]):
37
+ title = hit["fields"]["title"]
38
+ url = hit["fields"]["url"]
39
+ page = hit["fields"]["page_number"]
40
+ image = hit["fields"]["image"]
41
+ _id = hit["id"]
42
+ score = hit["relevance"]
43
+
44
+ result_text += f"\nPDF Result {i + 1}\n"
45
+ result_text += f"Title: {title}, page {page+1} with score {score:.2f}\n"
46
+ result_text += f"URL: {url}\n"
47
+ result_text += f"ID: {_id}\n"
48
+ # Optionally, save or display the image
49
+ # img_data = base64.b64decode(image)
50
+ # img_path = SAVEDIR / f"{title}.png"
51
+ # with open(f"{img_path}", "wb") as f:
52
+ # f.write(img_data)
53
+ print(result_text)
54
+
55
+
56
+ async def query_vespa_default(app, queries, qs):
57
+ async with app.asyncio(connections=1, total_timeout=120) as session:
58
+ for idx, query in enumerate(queries):
59
+ query_embedding = {k: v.tolist() for k, v in enumerate(qs[idx])}
60
+ response: VespaQueryResponse = await session.query(
61
+ yql="select documentid,title,url,image,page_number from pdf_page where userInput(@userQuery)",
62
+ ranking="default",
63
+ userQuery=query,
64
+ timeout=120,
65
+ hits=3,
66
+ body={"input.query(qt)": query_embedding, "presentation.timing": True},
67
+ )
68
+ assert response.is_successful()
69
+ display_query_results(query, response)
70
+
71
+
72
+ async def query_vespa_nearest_neighbor(app, queries, qs):
73
+ # Using nearestNeighbor for retrieval
74
+ target_hits_per_query_tensor = (
75
+ 20 # this is a hyper parameter that can be tuned for speed versus accuracy
76
+ )
77
+ async with app.asyncio(connections=1, total_timeout=180) as session:
78
+ for idx, query in enumerate(queries):
79
+ float_query_embedding = {k: v.tolist() for k, v in enumerate(qs[idx])}
80
+ binary_query_embeddings = dict()
81
+ for k, v in float_query_embedding.items():
82
+ binary_vector = (
83
+ np.packbits(np.where(np.array(v) > 0, 1, 0))
84
+ .astype(np.int8)
85
+ .tolist()
86
+ )
87
+ binary_query_embeddings[k] = binary_vector
88
+ if len(binary_query_embeddings) >= MAX_QUERY_TERMS:
89
+ print(
90
+ f"Warning: Query has more than {MAX_QUERY_TERMS} terms. Truncating."
91
+ )
92
+ break
93
+
94
+ # The mixed tensors used in MaxSim calculations
95
+ # We use both binary and float representations
96
+ query_tensors = {
97
+ "input.query(qtb)": binary_query_embeddings,
98
+ "input.query(qt)": float_query_embedding,
99
+ }
100
+ # The query tensors used in the nearest neighbor calculations
101
+ for i in range(0, len(binary_query_embeddings)):
102
+ query_tensors[f"input.query(rq{i})"] = binary_query_embeddings[i]
103
+ nn = []
104
+ for i in range(0, len(binary_query_embeddings)):
105
+ nn.append(
106
+ f"({{targetHits:{target_hits_per_query_tensor}}}nearestNeighbor(embedding,rq{i}))"
107
+ )
108
+ # We use an OR operator to combine the nearest neighbor operator
109
+ nn = " OR ".join(nn)
110
+ response: VespaQueryResponse = await session.query(
111
+ body={
112
+ **query_tensors,
113
+ "presentation.timing": True,
114
+ "yql": f"select documentid, title, url, image, page_number from pdf_page where {nn}",
115
+ "ranking.profile": "retrieval-and-rerank",
116
+ "timeout": 120,
117
+ "hits": 3,
118
+ },
119
+ )
120
+ assert response.is_successful(), response.json
121
+ display_query_results(query, response)
122
+
123
+
124
+ def main():
125
+ vespa_app_url = os.environ.get(
126
+ "VESPA_APP_URL"
127
+ ) # Ensure this is set to your Vespa app URL
128
+ vespa_cloud_secret_token = os.environ.get("VESPA_CLOUD_SECRET_TOKEN")
129
+ if not vespa_app_url or not vespa_cloud_secret_token:
130
+ raise ValueError(
131
+ "Please set the VESPA_APP_URL and VESPA_CLOUD_SECRET_TOKEN environment variables"
132
+ )
133
+ # Instantiate Vespa connection
134
+ app = Vespa(url=vespa_app_url, vespa_cloud_secret_token=vespa_cloud_secret_token)
135
+ status_resp = app.get_application_status()
136
+ if status_resp.status_code != 200:
137
+ print(f"Failed to connect to Vespa at {vespa_app_url}")
138
+ return
139
+ else:
140
+ print(f"Connected to Vespa at {vespa_app_url}")
141
+ # Load the model
142
+ device = get_torch_device("auto")
143
+ print(f"Using device: {device}")
144
+
145
+ model_name = "vidore/colpali-v1.2"
146
+ processor_name = "google/paligemma-3b-mix-448"
147
+
148
+ model = cast(
149
+ ColPali,
150
+ ColPali.from_pretrained(
151
+ model_name,
152
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
153
+ device_map=device,
154
+ ),
155
+ ).eval()
156
+
157
+ processor = cast(ColPaliProcessor, ColPaliProcessor.from_pretrained(processor_name))
158
+
159
+ # Create dummy image
160
+ dummy_image = Image.new("RGB", (448, 448), (255, 255, 255))
161
+
162
+ # Define queries
163
+ queries = [
164
+ "Percentage of non-fresh water as source?",
165
+ "Policies related to nature risk?",
166
+ "How much of produced water is recycled?",
167
+ ]
168
+
169
+ # Obtain query embeddings
170
+ dataloader = DataLoader(
171
+ queries,
172
+ batch_size=1,
173
+ shuffle=False,
174
+ collate_fn=lambda x: process_queries(processor, x, dummy_image),
175
+ )
176
+ qs = []
177
+ for batch_query in dataloader:
178
+ with torch.no_grad():
179
+ batch_query = {k: v.to(model.device) for k, v in batch_query.items()}
180
+ embeddings_query = model(**batch_query)
181
+ qs.extend(list(torch.unbind(embeddings_query.to("cpu"))))
182
+
183
+ # Perform queries using default rank profile
184
+ print("Performing queries using default rank profile:")
185
+ asyncio.run(query_vespa_default(app, queries, qs))
186
+
187
+ # Perform queries using nearestNeighbor
188
+ print("Performing queries using nearestNeighbor:")
189
+ asyncio.run(query_vespa_nearest_neighbor(app, queries, qs))
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
static/assets/ConocoPhillips Sustainability Highlights - Nature (24-0976).png ADDED
static/img/carbon.png ADDED
static/img/energy.png ADDED
static/img/sustainability.png ADDED
static/js/highlightjs-theme.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ function getPreferredTheme() {
3
+ if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
4
+ return 'dark';
5
+ }
6
+ return 'light';
7
+ }
8
+
9
+ function syncHighlightTheme() {
10
+ const link = document.getElementById('highlight-theme');
11
+ const preferredTheme = getPreferredTheme();
12
+ link.href = preferredTheme === 'dark' ?
13
+ 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/github-dark.min.css' :
14
+ 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/github.min.css';
15
+ }
16
+
17
+ // Apply the correct theme immediately
18
+ syncHighlightTheme();
19
+
20
+ // Observe changes in the 'dark' class on the <html> element
21
+ const observer = new MutationObserver(syncHighlightTheme);
22
+ observer.observe(document.documentElement, {attributes: true, attributeFilter: ['class']});
23
+ })();
tailwind.config.js ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function filterDefault(values) {
2
+ return Object.fromEntries(
3
+ Object.entries(values).filter(([key]) => key !== "DEFAULT"),
4
+ );
5
+ }
6
+
7
+ /** @type {import('tailwindcss').Config} */
8
+ export default {
9
+ darkMode: ["selector"],
10
+ content: [
11
+ "./**/*.py",
12
+ "./.venv/lib/python3.12/site-packages/shad4fast/**/*.{py,js}",
13
+ ],
14
+ theme: {
15
+ container: {
16
+ center: true,
17
+ padding: "2rem",
18
+ screens: {
19
+ "2xl": "1400px",
20
+ },
21
+ },
22
+ extend: {
23
+ animation: {
24
+ "accordion-down": "accordion-down 0.2s ease-out",
25
+ "accordion-up": "accordion-up 0.2s ease-out",
26
+ },
27
+ animationDelay: ({theme}) => ({
28
+ ...theme("transitionDelay"),
29
+ }),
30
+ animationDuration: ({theme}) => ({
31
+ 0: "0ms",
32
+ ...theme("transitionDuration"),
33
+ }),
34
+ animationTimingFunction: ({theme}) => ({
35
+ ...theme("transitionTimingFunction"),
36
+ }),
37
+ animationFillMode: {
38
+ none: "none",
39
+ forwards: "forwards",
40
+ backwards: "backwards",
41
+ both: "both",
42
+ },
43
+ animationDirection: {
44
+ normal: "normal",
45
+ reverse: "reverse",
46
+ alternate: "alternate",
47
+ "alternate-reverse": "alternate-reverse",
48
+ },
49
+ animationOpacity: ({theme}) => ({
50
+ DEFAULT: 0,
51
+ ...theme("opacity"),
52
+ }),
53
+ animationTranslate: ({theme}) => ({
54
+ DEFAULT: "100%",
55
+ ...theme("translate"),
56
+ }),
57
+ animationScale: ({theme}) => ({
58
+ DEFAULT: 0,
59
+ ...theme("scale"),
60
+ }),
61
+ animationRotate: ({theme}) => ({
62
+ DEFAULT: "30deg",
63
+ ...theme("rotate"),
64
+ }),
65
+ animationRepeat: {
66
+ 0: "0",
67
+ 1: "1",
68
+ infinite: "infinite",
69
+ },
70
+ keyframes: {
71
+ enter: {
72
+ from: {
73
+ opacity: "var(--tw-enter-opacity, 1)",
74
+ transform:
75
+ "translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))",
76
+ },
77
+ },
78
+ exit: {
79
+ to: {
80
+ opacity: "var(--tw-exit-opacity, 1)",
81
+ transform:
82
+ "translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))",
83
+ },
84
+ },
85
+ },
86
+ colors: {
87
+ border: "hsl(var(--border))",
88
+ input: "hsl(var(--input))",
89
+ ring: "hsl(var(--ring))",
90
+ background: "hsl(var(--background))",
91
+ foreground: "hsl(var(--foreground))",
92
+ primary: {
93
+ DEFAULT: "hsl(var(--primary))",
94
+ foreground: "hsl(var(--primary-foreground))",
95
+ },
96
+ secondary: {
97
+ DEFAULT: "hsl(var(--secondary))",
98
+ foreground: "hsl(var(--secondary-foreground))",
99
+ },
100
+ destructive: {
101
+ DEFAULT: "hsl(var(--destructive))",
102
+ foreground: "hsl(var(--destructive-foreground))",
103
+ },
104
+ muted: {
105
+ DEFAULT: "hsl(var(--muted))",
106
+ foreground: "hsl(var(--muted-foreground))",
107
+ },
108
+ accent: {
109
+ DEFAULT: "hsl(var(--accent))",
110
+ foreground: "hsl(var(--accent-foreground))",
111
+ },
112
+ popover: {
113
+ DEFAULT: "hsl(var(--popover))",
114
+ foreground: "hsl(var(--popover-foreground))",
115
+ },
116
+ card: {
117
+ DEFAULT: "hsl(var(--card))",
118
+ foreground: "hsl(var(--card-foreground))",
119
+ },
120
+ },
121
+ borderRadius: {
122
+ lg: `var(--radius)`,
123
+ md: `calc(var(--radius) - 2px)`,
124
+ sm: "calc(var(--radius) - 4px)",
125
+ },
126
+ },
127
+ },
128
+ plugins: [
129
+ function ({addUtilities, matchUtilities, theme}) {
130
+ addUtilities({
131
+ "@keyframes enter": theme("keyframes.enter"),
132
+ "@keyframes exit": theme("keyframes.exit"),
133
+ ".animate-in": {
134
+ animationName: "enter",
135
+ animationDuration: theme("animationDuration.DEFAULT"),
136
+ "--tw-enter-opacity": "initial",
137
+ "--tw-enter-scale": "initial",
138
+ "--tw-enter-rotate": "initial",
139
+ "--tw-enter-translate-x": "initial",
140
+ "--tw-enter-translate-y": "initial",
141
+ },
142
+ ".animate-out": {
143
+ animationName: "exit",
144
+ animationDuration: theme("animationDuration.DEFAULT"),
145
+ "--tw-exit-opacity": "initial",
146
+ "--tw-exit-scale": "initial",
147
+ "--tw-exit-rotate": "initial",
148
+ "--tw-exit-translate-x": "initial",
149
+ "--tw-exit-translate-y": "initial",
150
+ },
151
+ });
152
+
153
+ matchUtilities(
154
+ {
155
+ "fade-in": (value) => ({"--tw-enter-opacity": value}),
156
+ "fade-out": (value) => ({"--tw-exit-opacity": value}),
157
+ },
158
+ {values: theme("animationOpacity")},
159
+ );
160
+
161
+ matchUtilities(
162
+ {
163
+ "zoom-in": (value) => ({"--tw-enter-scale": value}),
164
+ "zoom-out": (value) => ({"--tw-exit-scale": value}),
165
+ },
166
+ {values: theme("animationScale")},
167
+ );
168
+
169
+ matchUtilities(
170
+ {
171
+ "spin-in": (value) => ({"--tw-enter-rotate": value}),
172
+ "spin-out": (value) => ({"--tw-exit-rotate": value}),
173
+ },
174
+ {values: theme("animationRotate")},
175
+ );
176
+
177
+ matchUtilities(
178
+ {
179
+ "slide-in-from-top": (value) => ({
180
+ "--tw-enter-translate-y": `-${value}`,
181
+ }),
182
+ "slide-in-from-bottom": (value) => ({
183
+ "--tw-enter-translate-y": value,
184
+ }),
185
+ "slide-in-from-left": (value) => ({
186
+ "--tw-enter-translate-x": `-${value}`,
187
+ }),
188
+ "slide-in-from-right": (value) => ({
189
+ "--tw-enter-translate-x": value,
190
+ }),
191
+ "slide-out-to-top": (value) => ({
192
+ "--tw-exit-translate-y": `-${value}`,
193
+ }),
194
+ "slide-out-to-bottom": (value) => ({
195
+ "--tw-exit-translate-y": value,
196
+ }),
197
+ "slide-out-to-left": (value) => ({
198
+ "--tw-exit-translate-x": `-${value}`,
199
+ }),
200
+ "slide-out-to-right": (value) => ({
201
+ "--tw-exit-translate-x": value,
202
+ }),
203
+ },
204
+ {values: theme("animationTranslate")},
205
+ );
206
+
207
+ matchUtilities(
208
+ {duration: (value) => ({animationDuration: value})},
209
+ {values: filterDefault(theme("animationDuration"))},
210
+ );
211
+
212
+ matchUtilities(
213
+ {delay: (value) => ({animationDelay: value})},
214
+ {values: theme("animationDelay")},
215
+ );
216
+
217
+ matchUtilities(
218
+ {ease: (value) => ({animationTimingFunction: value})},
219
+ {values: filterDefault(theme("animationTimingFunction"))},
220
+ );
221
+
222
+ addUtilities({
223
+ ".running": {animationPlayState: "running"},
224
+ ".paused": {animationPlayState: "paused"},
225
+ });
226
+
227
+ matchUtilities(
228
+ {"fill-mode": (value) => ({animationFillMode: value})},
229
+ {values: theme("animationFillMode")},
230
+ );
231
+
232
+ matchUtilities(
233
+ {direction: (value) => ({animationDirection: value})},
234
+ {values: theme("animationDirection")},
235
+ );
236
+
237
+ matchUtilities(
238
+ {repeat: (value) => ({animationIterationCount: value})},
239
+ {values: theme("animationRepeat")},
240
+ );
241
+ },
242
+ ],
243
+ };
tailwindcss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:327703a4646081906e11d116ff4e8e43076466c3d269282bbe612555b9fe0c58
3
+ size 47351504
uv.lock ADDED
The diff for this file is too large to render. See raw diff