RobertoBarrosoLuque commited on
Commit
03263ac
·
1 Parent(s): f9c74bd

Frontend V1

Browse files
Files changed (4) hide show
  1. assets/fireworks_logo.png +0 -0
  2. requirements.txt +5 -8
  3. src/app.py +426 -0
  4. src/config.py +194 -0
assets/fireworks_logo.png ADDED
requirements.txt CHANGED
@@ -1,11 +1,8 @@
1
- huggingface_hub
2
- openai
3
  gradio==5.42.0
 
4
  python-dotenv==1.0.0
5
- ipython
6
- scikit-learn
7
- jupyter
8
- altair
9
- matplotlib
10
- pandas
11
  numpy
 
 
 
 
 
 
 
1
  gradio==5.42.0
2
+ openai
3
  python-dotenv==1.0.0
 
 
 
 
 
 
4
  numpy
5
+ pandas
6
+ scikit-learn
7
+ rank-bm25
8
+ faiss-cpu
src/app.py CHANGED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import time
3
+ from typing import List, Dict, Tuple
4
+ from pathlib import Path
5
+ import os
6
+ from config import GRADIO_THEME, CUSTOM_CSS, EXAMPLE_QUERIES
7
+
8
+ _FILE_PATH = Path(__file__).parents[1]
9
+
10
+
11
+ # Placeholder data for demo
12
+ SAMPLE_PRODUCTS = [
13
+ {
14
+ "id": 1,
15
+ "title": "Wireless Bluetooth Headphones",
16
+ "description": "High-quality wireless headphones with 30-hour battery life and noise cancellation.",
17
+ "category": "Electronics",
18
+ },
19
+ {
20
+ "id": 2,
21
+ "title": "Science Kit for Kids",
22
+ "description": "Educational science experiments kit perfect for children ages 5-10.",
23
+ "category": "Toys",
24
+ },
25
+ {
26
+ "id": 3,
27
+ "title": "Running Shoes - Men's",
28
+ "description": "Lightweight running shoes with cushioned soles and breathable mesh.",
29
+ "category": "Sports",
30
+ },
31
+ {
32
+ "id": 4,
33
+ "title": "Portable Bluetooth Speaker",
34
+ "description": "Waterproof speaker with 12-hour battery life and deep bass.",
35
+ "category": "Electronics",
36
+ },
37
+ {
38
+ "id": 5,
39
+ "title": "Ergonomic Office Chair",
40
+ "description": "Adjustable office chair with lumbar support and breathable fabric.",
41
+ "category": "Furniture",
42
+ },
43
+ ]
44
+
45
+
46
+ def format_results(results: List[Dict], stage_name: str, metrics: Dict) -> str:
47
+ """Format search results as HTML."""
48
+ html_parts = [f"### {stage_name} Results\n\n"]
49
+
50
+ for idx, result in enumerate(results, 1):
51
+ html_parts.append(
52
+ f"""
53
+ <div class="result-card">
54
+ <strong>{idx}. {result['title']}</strong><br/>
55
+ <span style="color: #64748B; font-size: 0.9em;">{result['description']}</span><br/>
56
+ <span style="color: #94A3B8; font-size: 0.85em;">Category: {result['category']}</span><br/>
57
+ <span style="color: #6720FF; font-weight: 600;">Score: {result['score']:.3f}</span>
58
+ </div>
59
+ """
60
+ )
61
+
62
+ html_parts.append("\n### Metrics\n\n")
63
+ html_parts.append(
64
+ f"""
65
+ <div class="metric-box">
66
+ " <strong>Semantic Match:</strong> {metrics['semantic_match']:.3f}<br/>
67
+ " <strong>Diversity:</strong> {metrics['diversity']:.3f}<br/>
68
+ " <strong>Latency:</strong> {metrics['latency_ms']}ms
69
+ </div>
70
+ """
71
+ )
72
+
73
+ return "".join(html_parts)
74
+
75
+
76
+ def search_stage_1(query: str) -> Tuple[str, Dict]:
77
+ """Stage 1: Baseline BM25 keyword search."""
78
+ start_time = time.time()
79
+
80
+ # Placeholder: Simple keyword matching
81
+ results = []
82
+ for product in SAMPLE_PRODUCTS[:3]:
83
+ results.append({**product, "score": 0.65 + (len(results) * 0.05)})
84
+
85
+ latency = int((time.time() - start_time) * 1000)
86
+
87
+ metrics = {
88
+ "semantic_match": 0.58,
89
+ "diversity": 0.60,
90
+ "latency_ms": max(50, latency),
91
+ }
92
+
93
+ return format_results(results, "Stage 1: BM25 Baseline", metrics), metrics
94
+
95
+
96
+ def search_stage_2(query: str) -> Tuple[str, Dict]:
97
+ """Stage 2: BM25 + Vector Embeddings."""
98
+ start_time = time.time()
99
+
100
+ # Placeholder: Simulated embedding search
101
+ results = []
102
+ for product in SAMPLE_PRODUCTS[:4]:
103
+ results.append({**product, "score": 0.72 + (len(results) * 0.04)})
104
+
105
+ latency = int((time.time() - start_time) * 1000)
106
+
107
+ metrics = {
108
+ "semantic_match": 0.72,
109
+ "diversity": 0.70,
110
+ "latency_ms": max(100, latency),
111
+ }
112
+
113
+ return format_results(results, "Stage 2: + Vector Embeddings", metrics), metrics
114
+
115
+
116
+ def search_stage_3(query: str) -> Tuple[str, Dict]:
117
+ """Stage 3: BM25 + Embeddings + Query Expansion."""
118
+ start_time = time.time()
119
+
120
+ # Placeholder: Simulated query expansion
121
+ results = []
122
+ for product in SAMPLE_PRODUCTS[:5]:
123
+ results.append({**product, "score": 0.78 + (len(results) * 0.03)})
124
+
125
+ latency = int((time.time() - start_time) * 1000)
126
+
127
+ metrics = {
128
+ "semantic_match": 0.81,
129
+ "diversity": 0.75,
130
+ "latency_ms": max(150, latency),
131
+ }
132
+
133
+ return format_results(results, "Stage 3: + Query Expansion", metrics), metrics
134
+
135
+
136
+ def search_stage_4(query: str) -> Tuple[str, Dict]:
137
+ """Stage 4: BM25 + Embeddings + Query Expansion + LLM Reranking."""
138
+ start_time = time.time()
139
+
140
+ # Placeholder: Simulated reranking
141
+ results = []
142
+ for product in SAMPLE_PRODUCTS[:5]:
143
+ results.append({**product, "score": 0.85 + (len(results) * 0.025)})
144
+
145
+ latency = int((time.time() - start_time) * 1000)
146
+
147
+ metrics = {
148
+ "semantic_match": 0.88,
149
+ "diversity": 0.80,
150
+ "latency_ms": max(200, latency),
151
+ }
152
+
153
+ return format_results(results, "Stage 4: + LLM Reranking", metrics), metrics
154
+
155
+
156
+ def search_all_stages(query: str) -> Tuple[str, str, str, str, str]:
157
+ """Run search across all stages and return comparison."""
158
+ if not query.strip():
159
+ empty_msg = "Please enter a search query."
160
+ return empty_msg, empty_msg, empty_msg, empty_msg, empty_msg
161
+
162
+ results_1, metrics_1 = search_stage_1(query)
163
+ results_2, metrics_2 = search_stage_2(query)
164
+ results_3, metrics_3 = search_stage_3(query)
165
+ results_4, metrics_4 = search_stage_4(query)
166
+
167
+ comparison = generate_comparison_table([metrics_1, metrics_2, metrics_3, metrics_4])
168
+
169
+ return results_1, results_2, results_3, results_4, comparison
170
+
171
+
172
+ def generate_comparison_table(all_metrics: List[Dict]) -> str:
173
+ """Generate comparison table for all stages."""
174
+ stage_names = [
175
+ "Stage 1: BM25",
176
+ "Stage 2: + Embeddings",
177
+ "Stage 3: + Query Expansion",
178
+ "Stage 4: + Reranking",
179
+ ]
180
+
181
+ html = """
182
+ ### Comparison Across All Stages
183
+
184
+ <table class="comparison-table">
185
+ <tr>
186
+ <th>Stage</th>
187
+ <th>Semantic Match</th>
188
+ <th>Diversity</th>
189
+ <th>Latency (ms)</th>
190
+ </tr>
191
+ """
192
+
193
+ for idx, (name, metrics) in enumerate(zip(stage_names, all_metrics)):
194
+ html += f"""
195
+ <tr>
196
+ <td><strong>{name}</strong></td>
197
+ <td>{metrics['semantic_match']:.3f}</td>
198
+ <td>{metrics['diversity']:.3f}</td>
199
+ <td>{metrics['latency_ms']}ms</td>
200
+ </tr>
201
+ """
202
+
203
+ html += "</table>"
204
+
205
+ html += """
206
+ ### Key Insights
207
+
208
+ <div class="metric-box">
209
+ " <strong>Semantic Match improves by 52%</strong> from Stage 1 to Stage 4<br/>
210
+ " <strong>Diversity increases by 33%</strong> showing more varied results<br/>
211
+ " <strong>Latency stays under 200ms</strong> maintaining fast performance<br/>
212
+ " Each stage adds incremental value to search quality
213
+ </div>
214
+ """
215
+
216
+ return html
217
+
218
+
219
+ def set_example(example: str) -> str:
220
+ """Set an example query."""
221
+ return example
222
+
223
+
224
+ # Code snippets for each stage
225
+ CODE_STAGE_1 = """
226
+ ```python
227
+ from rank_bm25 import BM25Okapi
228
+
229
+ # Tokenize documents
230
+ tokenized_docs = [doc.split() for doc in documents]
231
+
232
+ # Create BM25 index
233
+ bm25 = BM25Okapi(tokenized_docs)
234
+
235
+ # Search
236
+ query_tokens = query.split()
237
+ scores = bm25.get_scores(query_tokens)
238
+
239
+ # Get top results
240
+ top_indices = scores.argsort()[-5:][::-1]
241
+ results = [documents[i] for i in top_indices]
242
+ ```
243
+ """
244
+
245
+ CODE_STAGE_2 = """
246
+ ```python
247
+ from openai import OpenAI
248
+ import faiss
249
+ import numpy as np
250
+
251
+ client = OpenAI(
252
+ base_url="https://api.fireworks.ai/inference/v1"
253
+ )
254
+
255
+ # Generate embeddings
256
+ response = client.embeddings.create(
257
+ model="accounts/fireworks/models/qwen3-embedding-8b",
258
+ input=[query] + documents
259
+ )
260
+
261
+ # Extract embeddings
262
+ query_emb = np.array(response.data[0].embedding)
263
+ doc_embs = np.array([d.embedding for d in response.data[1:]])
264
+
265
+ # FAISS search
266
+ index = faiss.IndexFlatIP(doc_embs.shape[1])
267
+ index.add(doc_embs)
268
+ scores, indices = index.search(query_emb.reshape(1, -1), k=5)
269
+ ```
270
+ """
271
+
272
+ CODE_STAGE_3 = """
273
+ ```python
274
+ # Query expansion with LLM
275
+ response = client.chat.completions.create(
276
+ model="accounts/fireworks/models/llama-v3p1-8b-instruct",
277
+ messages=[{
278
+ "role": "user",
279
+ "content": f"Extract 2-3 key search concepts from: {query}"
280
+ }]
281
+ )
282
+
283
+ expanded_query = response.choices[0].message.content
284
+
285
+ # Search with expanded query
286
+ response = client.embeddings.create(
287
+ model="accounts/fireworks/models/qwen3-embedding-8b",
288
+ input=[expanded_query] + documents
289
+ )
290
+
291
+ # Continue with embedding search...
292
+ ```
293
+ """
294
+
295
+ CODE_STAGE_4 = """
296
+ ```python
297
+ # First get top 20 candidates from Stage 3
298
+ top_20_results = get_stage_3_results(query, k=20)
299
+
300
+ # Rerank with Fireworks reranker
301
+ rerank_response = client.post(
302
+ "https://api.fireworks.ai/inference/v1/rerank",
303
+ json={
304
+ "model": "fireworks/qwen3-reranker-8b",
305
+ "query": query,
306
+ "documents": [r["text"] for r in top_20_results],
307
+ "top_n": 5
308
+ }
309
+ )
310
+
311
+ # Get final ranked results
312
+ final_results = [
313
+ top_20_results[r["index"]]
314
+ for r in rerank_response.json()["results"]
315
+ ]
316
+ ```
317
+ """
318
+
319
+
320
+ # Build Gradio Interface
321
+ with gr.Blocks(
322
+ css=CUSTOM_CSS, theme=GRADIO_THEME, title="Search Alchemy - Fireworks AI"
323
+ ) as demo:
324
+
325
+ # Header
326
+ with gr.Row():
327
+ with gr.Column(scale=3):
328
+ gr.Markdown(
329
+ """
330
+ <h1 class="header-title" style="font-size: 2.5em; text-align: left;">Search Alchemy</h1>
331
+ <p style="color: #64748B; font-size: 1.1em; margin-top: 0; text-align: left;">Building Production Search Pipelines with Fireworks AI</p>
332
+ """
333
+ )
334
+ with gr.Row(elem_classes="compact-header"):
335
+ with gr.Column(scale=1, min_width=150):
336
+ gr.Markdown(
337
+ "<p style='margin: 0; padding: 0; font-size: 0.85em; color: #64748B;'>Powered by</p>"
338
+ )
339
+ gr.Image(
340
+ value=str(_FILE_PATH / "assets" / "fireworks_logo.png"),
341
+ height=35,
342
+ width=140,
343
+ show_label=False,
344
+ show_download_button=False,
345
+ container=False,
346
+ show_fullscreen_button=False,
347
+ show_share_button=False,
348
+ )
349
+
350
+ with gr.Row():
351
+ with gr.Column(scale=4):
352
+ query_input = gr.Textbox(
353
+ label="Search Query",
354
+ placeholder="Enter your search query...",
355
+ scale=3,
356
+ elem_classes="search-box",
357
+ )
358
+ with gr.Column(scale=1):
359
+ val = os.getenv("FIREWORKS_API_KEY", "") # pragma: allowlist secret
360
+ api_key_value = gr.Textbox( # pragma: allowlist secret
361
+ label="API Key",
362
+ type="password",
363
+ placeholder="Enter your Fireworks AI API key",
364
+ value=val,
365
+ container=True,
366
+ elem_classes="compact-input",
367
+ )
368
+ with gr.Row():
369
+ search_btn = gr.Button("Search", variant="primary", scale=1)
370
+
371
+ # Example queries
372
+ with gr.Row():
373
+ gr.Markdown("**Quick Examples:**")
374
+ with gr.Row():
375
+ example_buttons = []
376
+ for example in EXAMPLE_QUERIES:
377
+ btn = gr.Button(example, size="sm", variant="secondary")
378
+ example_buttons.append(btn)
379
+ btn.click(fn=set_example, inputs=[gr.State(example)], outputs=[query_input])
380
+
381
+ # Tabs for each stage
382
+ with gr.Tabs() as tabs:
383
+
384
+ # Stage 1 Tab
385
+ with gr.Tab("Stage 1: BM25 Baseline"):
386
+ stage1_output = gr.Markdown(label="Results")
387
+ with gr.Accordion("Show Code", open=False):
388
+ gr.Markdown(CODE_STAGE_1)
389
+
390
+ # Stage 2 Tab
391
+ with gr.Tab("Stage 2: + Vector Embeddings"):
392
+ stage2_output = gr.Markdown(label="Results")
393
+ with gr.Accordion("Show Code", open=False):
394
+ gr.Markdown(CODE_STAGE_2)
395
+
396
+ # Stage 3 Tab
397
+ with gr.Tab("Stage 3: + Query Expansion"):
398
+ stage3_output = gr.Markdown(label="Results")
399
+ with gr.Accordion("Show Code", open=False):
400
+ gr.Markdown(CODE_STAGE_3)
401
+
402
+ # Stage 4 Tab
403
+ with gr.Tab("Stage 4: + LLM Reranking"):
404
+ stage4_output = gr.Markdown(label="Results")
405
+ with gr.Accordion("Show Code", open=False):
406
+ gr.Markdown(CODE_STAGE_4)
407
+
408
+ # Comparison Tab
409
+ with gr.Tab("Compare All Stages"):
410
+ comparison_output = gr.Markdown(label="Comparison")
411
+
412
+ # Search button click handler
413
+ search_btn.click(
414
+ fn=search_all_stages,
415
+ inputs=[query_input],
416
+ outputs=[
417
+ stage1_output,
418
+ stage2_output,
419
+ stage3_output,
420
+ stage4_output,
421
+ comparison_output,
422
+ ],
423
+ )
424
+
425
+ if __name__ == "__main__":
426
+ demo.launch()
src/config.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ # Fireworks AI Model Configuration
4
+ EMBEDDING_MODEL = "accounts/fireworks/models/qwen3-embedding-8b"
5
+ LLM_MODEL = "accounts/fireworks/models/llama-v3p1-8b-instruct"
6
+ RERANKER_MODEL = "fireworks/qwen3-reranker-8b"
7
+
8
+ # Gradio Theme Configuration
9
+ GRADIO_THEME = gr.themes.Base(
10
+ primary_hue=gr.themes.colors.purple,
11
+ secondary_hue=gr.themes.colors.violet,
12
+ neutral_hue=gr.themes.colors.slate,
13
+ spacing_size=gr.themes.sizes.spacing_lg,
14
+ radius_size=gr.themes.sizes.radius_md,
15
+ text_size=gr.themes.sizes.text_md,
16
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
17
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
18
+ ).set(
19
+ button_primary_background_fill="#6720FF",
20
+ button_primary_background_fill_hover="#7B2FFF",
21
+ button_primary_text_color="#FFFFFF",
22
+ button_secondary_background_fill="#F3F0FF",
23
+ button_secondary_background_fill_hover="#EDE9FE",
24
+ button_secondary_text_color="#6720FF",
25
+ slider_color="#6720FF",
26
+ link_text_color="#6720FF",
27
+ link_text_color_hover="#7B2FFF",
28
+ link_text_color_visited="#8B5CF6",
29
+ body_background_fill="#FAFBFC",
30
+ block_background_fill="#FFFFFF",
31
+ input_background_fill="#FFFFFF",
32
+ border_color_primary="#E6EAF4",
33
+ )
34
+
35
+ # Custom CSS
36
+ CUSTOM_CSS = """
37
+ .gradio-container {
38
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
39
+ background: linear-gradient(135deg, #FAFBFC 0%, #F3F0FF 100%);
40
+ }
41
+
42
+ .header-title {
43
+ background: linear-gradient(135deg, #6720FF 0%, #8B5CF6 100%);
44
+ -webkit-background-clip: text;
45
+ -webkit-text-fill-color: transparent;
46
+ background-clip: text;
47
+ font-weight: 700;
48
+ text-align: center;
49
+ margin-bottom: 0.5em;
50
+ }
51
+
52
+ .subtitle {
53
+ color: #64748B;
54
+ text-align: center;
55
+ font-size: 1.1em;
56
+ margin-top: 0;
57
+ }
58
+
59
+ .search-box {
60
+ border: 2px solid #E6EAF4;
61
+ border-radius: 10px;
62
+ transition: all 0.2s ease;
63
+ }
64
+
65
+ .search-box:focus {
66
+ border-color: #6720FF;
67
+ box-shadow: 0 0 0 3px rgba(103, 32, 255, 0.1);
68
+ }
69
+
70
+ .result-card {
71
+ background: white;
72
+ border-radius: 12px;
73
+ padding: 16px;
74
+ margin: 8px 0;
75
+ box-shadow: 0 2px 4px rgba(103, 32, 255, 0.08);
76
+ border: 1px solid #E6EAF4;
77
+ transition: all 0.2s ease;
78
+ }
79
+
80
+ .result-card:hover {
81
+ box-shadow: 0 4px 12px rgba(103, 32, 255, 0.12);
82
+ border-color: #C4B5FD;
83
+ }
84
+
85
+ .metric-box {
86
+ background: linear-gradient(to right, #F3F0FF, #FFFFFF);
87
+ border-left: 3px solid #6720FF;
88
+ padding: 12px;
89
+ margin: 8px 0;
90
+ border-radius: 8px;
91
+ font-size: 0.9em;
92
+ }
93
+
94
+ .code-section {
95
+ background: linear-gradient(to right, #F3F0FF, #FFFFFF);
96
+ border-left: 3px solid #6720FF;
97
+ padding: 16px;
98
+ margin: 12px 0;
99
+ border-radius: 8px;
100
+ font-family: 'JetBrains Mono', monospace;
101
+ font-size: 0.9em;
102
+ }
103
+
104
+ .comparison-table {
105
+ width: 100%;
106
+ border-collapse: collapse;
107
+ margin: 20px 0;
108
+ }
109
+
110
+ .comparison-table th {
111
+ background: #6720FF;
112
+ color: white;
113
+ padding: 12px;
114
+ text-align: left;
115
+ font-weight: 600;
116
+ }
117
+
118
+ .comparison-table td {
119
+ padding: 12px;
120
+ border-bottom: 1px solid #E6EAF4;
121
+ }
122
+
123
+ .comparison-table tr:hover {
124
+ background: #F3F0FF;
125
+ }
126
+
127
+ ::-webkit-scrollbar {
128
+ width: 8px;
129
+ height: 8px;
130
+ }
131
+
132
+ ::-webkit-scrollbar-track {
133
+ background: #F3F0FF;
134
+ border-radius: 4px;
135
+ }
136
+
137
+ ::-webkit-scrollbar-thumb {
138
+ background: #C4B5FD;
139
+ border-radius: 4px;
140
+ }
141
+
142
+ ::-webkit-scrollbar-thumb:hover {
143
+ background: #A78BFA;
144
+ }
145
+
146
+ details {
147
+ border: 1px solid #E6EAF4;
148
+ border-radius: 10px;
149
+ padding: 12px;
150
+ margin: 10px 0;
151
+ background: white;
152
+ }
153
+
154
+ details[open] {
155
+ border-color: #6720FF;
156
+ box-shadow: 0 4px 12px rgba(103, 32, 255, 0.15);
157
+ }
158
+
159
+ summary {
160
+ font-weight: 600;
161
+ color: #6720FF;
162
+ cursor: pointer;
163
+ padding: 4px;
164
+ }
165
+
166
+ summary:hover {
167
+ color: #7B2FFF;
168
+ }
169
+
170
+ .logo-image {
171
+ display: flex;
172
+ justify-content: flex-end;
173
+ align-items: center;
174
+ }
175
+
176
+ .api-config-accordion {
177
+ margin: 10px 0;
178
+ padding: 0;
179
+ }
180
+
181
+ .api-config-accordion > .label-wrap {
182
+ font-size: 0.85em;
183
+ padding: 8px 12px;
184
+ }
185
+ """
186
+
187
+ # Example queries
188
+ EXAMPLE_QUERIES = [
189
+ "gift for 5 year old who likes science",
190
+ "cheap wireless headphones good battery",
191
+ "running shoes",
192
+ "waterproof bluetooth speaker",
193
+ "ergonomic office chair under 200",
194
+ ]