Spaces:
Running
on
T4
Running
on
T4
thomasht86
commited on
Commit
β’
8ce4d25
1
Parent(s):
947aa12
Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .env.example +3 -0
- .gitattributes +1 -0
- .gitignore +8 -0
- README.md +103 -8
- backend/__init__.py +0 -0
- backend/colpali.py +521 -0
- backend/vespa_app.py +23 -0
- colpalidemo/schemas/pdf_page.sd +198 -0
- colpalidemo/search/query-profiles/default.xml +2 -0
- colpalidemo/search/query-profiles/types/root.xml +2 -0
- colpalidemo/services.xml +28 -0
- deploy_vespa_app.py +204 -0
- feed_vespa.py +207 -0
- frontend/__init__.py +0 -0
- frontend/app.py +193 -0
- frontend/layout.py +60 -0
- globals.css +157 -0
- hello.py +17 -0
- icons.py +1 -0
- main.py +152 -0
- output.css +2541 -0
- pyproject.toml +29 -0
- query_vespa.py +193 -0
- static/assets/ConocoPhillips Sustainability Highlights - Nature (24-0976).png +0 -0
- static/img/carbon.png +0 -0
- static/img/energy.png +0 -0
- static/img/sustainability.png +0 -0
- static/js/highlightjs-theme.js +23 -0
- tailwind.config.js +243 -0
- tailwindcss +3 -0
- uv.lock +0 -0
.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:
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 4.44.
|
8 |
-
app_file:
|
9 |
pinned: false
|
10 |
license: apache-2.0
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|