umairali64488 commited on
Commit
771382e
·
verified ·
1 Parent(s): 7999da7

Upload index.html

Browse files
Files changed (1) hide show
  1. frontend/index.html +1754 -0
frontend/index.html ADDED
@@ -0,0 +1,1754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RAG Research Assistant</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400;1,600&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* ── Design System ───────────────────────────────────────── */
11
+ :root {
12
+ --bg: #000000;
13
+ --bg2: #0a0a0a;
14
+ --surface: #111111;
15
+ --surface2: #1a1a1a;
16
+ --surface3: #222222;
17
+ --border: #333333;
18
+ --border2: #444444;
19
+ --ink: #ffffff;
20
+ --ink2: #cccccc;
21
+ --ink3: #999999;
22
+ --muted: #666666;
23
+ --muted2: #444444;
24
+ --gold: #ffffff;
25
+ --gold2: #cccccc;
26
+ --gold-lt: #333333;
27
+ --teal: #888888;
28
+ --teal-lt: #1a1a1a;
29
+ --rose: #666666;
30
+ --rose-lt: #1a1a1a;
31
+ --amber: #999999;
32
+ --mono: 'DM Mono', monospace;
33
+ --serif: 'Cormorant Garamond', serif;
34
+ --display: 'Playfair Display', serif;
35
+ --radius: 8px;
36
+ --shadow: 0 1px 3px rgba(0,0,0,0.5), 0 4px 16px rgba(0,0,0,0.3);
37
+ --shadow-lg: 0 2px 8px rgba(0,0,0,0.6), 0 12px 40px rgba(0,0,0,0.4);
38
+ }
39
+
40
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
41
+ html { font-size: 14px; scroll-behavior: smooth; }
42
+
43
+ body {
44
+ background: var(--bg);
45
+ color: var(--ink);
46
+ font-family: var(--display);
47
+ min-height: 100vh;
48
+ overflow-x: hidden;
49
+ position: relative;
50
+ }
51
+
52
+ /* Animated starfield background matching the video */
53
+ body::before {
54
+ content: '';
55
+ position: fixed;
56
+ inset: 0;
57
+ background: #000;
58
+ z-index: -2;
59
+ }
60
+
61
+ /* Starfield canvas - now covers only central area */
62
+ #starfield {
63
+ position: fixed;
64
+ top: 80px;
65
+ left: 50%;
66
+ transform: translateX(-50%);
67
+ width: 600px;
68
+ height: calc(100vh - 200px);
69
+ z-index: -1;
70
+ pointer-events: none;
71
+ opacity: 0.7;
72
+ }
73
+
74
+ /* ── Layout ──────────────────────────────────────────────── */
75
+ .app {
76
+ display: grid;
77
+ grid-template-columns: 280px 1fr 320px;
78
+ min-height: 100vh;
79
+ position: relative;
80
+ z-index: 1;
81
+ }
82
+
83
+ /* ── Left Sidebar (Model Config) ─────────────────────────── */
84
+ .sidebar-left {
85
+ background: rgba(10, 10, 10, 0.95);
86
+ backdrop-filter: blur(10px);
87
+ border-right: 1px solid var(--border);
88
+ display: flex;
89
+ flex-direction: column;
90
+ height: 100vh;
91
+ position: sticky;
92
+ top: 0;
93
+ overflow-y: auto;
94
+ box-shadow: 2px 0 20px rgba(0,0,0,0.5);
95
+ padding: 20px;
96
+ }
97
+
98
+ .sidebar-left::-webkit-scrollbar { width: 3px; }
99
+ .sidebar-left::-webkit-scrollbar-track { background: transparent; }
100
+ .sidebar-left::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
101
+
102
+ /* ── Center Header ───────────────────────────────────────── */
103
+ .center-header {
104
+ position: fixed;
105
+ top: 0;
106
+ left: 50%;
107
+ transform: translateX(-50%);
108
+ z-index: 100;
109
+ padding: 20px 40px;
110
+ text-align: center;
111
+ background: linear-gradient(180deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.8) 70%, transparent 100%);
112
+ }
113
+
114
+ .logo-eyebrow {
115
+ font-family: var(--mono);
116
+ font-size: 11px;
117
+ font-weight: 600;
118
+ letter-spacing: 0.4em;
119
+ text-transform: uppercase;
120
+ color: var(--ink);
121
+ margin-bottom: 8px;
122
+ text-shadow: 0 0 15px rgba(255,255,255,0.4);
123
+ }
124
+
125
+ .logo-title {
126
+ font-family: var(--display);
127
+ font-size: 32px;
128
+ font-weight: 800;
129
+ line-height: 1.1;
130
+ color: var(--ink);
131
+ letter-spacing: 0.02em;
132
+ text-shadow: 0 0 30px rgba(255,255,255,0.3);
133
+ }
134
+
135
+ .logo-title em {
136
+ font-style: italic;
137
+ color: var(--ink2);
138
+ font-weight: 600;
139
+ }
140
+
141
+ .logo-sub {
142
+ font-family: var(--mono);
143
+ font-size: 9px;
144
+ color: var(--muted);
145
+ margin-top: 6px;
146
+ letter-spacing: 0.08em;
147
+ }
148
+
149
+ /* ── Section label ───────────────────────────────────────── */
150
+ .panel-label {
151
+ font-family: var(--mono);
152
+ font-size: 9px;
153
+ letter-spacing: 0.25em;
154
+ text-transform: uppercase;
155
+ color: var(--muted);
156
+ margin-bottom: 16px;
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 10px;
160
+ font-weight: 500;
161
+ }
162
+ .panel-label::before { content: ''; width: 16px; height: 1px; background: var(--ink2); flex-shrink: 0; }
163
+ .panel-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
164
+
165
+ /* Settings panel */
166
+ .settings-panel { margin-bottom: 20px; }
167
+
168
+ .field-group { margin-bottom: 20px; }
169
+
170
+ .field-label {
171
+ font-family: var(--mono);
172
+ font-size: 9.5px;
173
+ color: var(--ink2);
174
+ margin-bottom: 8px;
175
+ display: flex;
176
+ justify-content: space-between;
177
+ align-items: center;
178
+ letter-spacing: 0.06em;
179
+ font-weight: 500;
180
+ }
181
+
182
+ .field-label .val {
183
+ color: var(--ink);
184
+ font-weight: 600;
185
+ background: var(--surface2);
186
+ padding: 2px 8px;
187
+ border-radius: 10px;
188
+ font-size: 9px;
189
+ border: 1px solid var(--border);
190
+ }
191
+
192
+ /* Visible slider track */
193
+ .slider-container {
194
+ position: relative;
195
+ height: 24px;
196
+ display: flex;
197
+ align-items: center;
198
+ }
199
+
200
+ .slider-track {
201
+ position: absolute;
202
+ left: 0;
203
+ right: 0;
204
+ height: 4px;
205
+ background: var(--border);
206
+ border-radius: 2px;
207
+ overflow: hidden;
208
+ }
209
+
210
+ .slider-fill {
211
+ position: absolute;
212
+ left: 0;
213
+ top: 0;
214
+ bottom: 0;
215
+ background: linear-gradient(90deg, var(--muted), var(--ink));
216
+ border-radius: 2px;
217
+ transition: width 0.1s ease;
218
+ }
219
+
220
+ input[type=range] {
221
+ position: relative;
222
+ width: 100%;
223
+ background: none;
224
+ border: none;
225
+ padding: 0;
226
+ height: 24px;
227
+ cursor: pointer;
228
+ outline: none;
229
+ appearance: none;
230
+ -webkit-appearance: none;
231
+ z-index: 2;
232
+ }
233
+
234
+ input[type=range]::-webkit-slider-thumb {
235
+ -webkit-appearance: none;
236
+ width: 18px;
237
+ height: 18px;
238
+ background: var(--ink);
239
+ border: 2px solid var(--surface);
240
+ border-radius: 50%;
241
+ cursor: pointer;
242
+ box-shadow: 0 0 10px rgba(255,255,255,0.5), 0 2px 8px rgba(0,0,0,0.5);
243
+ transition: transform 0.15s, box-shadow 0.15s;
244
+ }
245
+
246
+ input[type=range]::-webkit-slider-thumb:hover {
247
+ transform: scale(1.2);
248
+ box-shadow: 0 0 15px rgba(255,255,255,0.8), 0 2px 8px rgba(0,0,0,0.5);
249
+ }
250
+
251
+ select {
252
+ width: 100%;
253
+ background: var(--surface2);
254
+ border: 1px solid var(--border);
255
+ color: var(--ink2);
256
+ border-radius: var(--radius);
257
+ font-family: var(--mono);
258
+ font-size: 10.5px;
259
+ padding: 9px 11px;
260
+ outline: none;
261
+ transition: border-color 0.2s, box-shadow 0.2s;
262
+ appearance: none;
263
+ cursor: pointer;
264
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666'/%3E%3C/svg%3E");
265
+ background-repeat: no-repeat;
266
+ background-position: right 10px center;
267
+ padding-right: 28px;
268
+ }
269
+
270
+ select:focus {
271
+ border-color: var(--ink2);
272
+ box-shadow: 0 0 0 3px rgba(255,255,255,0.1);
273
+ }
274
+
275
+ /* Strategy buttons */
276
+ .strategy-btns { display: flex; gap: 5px; }
277
+ .strategy-btn {
278
+ flex: 1;
279
+ padding: 8px 4px;
280
+ border: 1px solid var(--border);
281
+ background: var(--surface2);
282
+ color: var(--muted);
283
+ font-family: var(--mono);
284
+ font-size: 8.5px;
285
+ letter-spacing: 0.1em;
286
+ text-transform: uppercase;
287
+ border-radius: 6px;
288
+ cursor: pointer;
289
+ transition: all 0.2s;
290
+ text-align: center;
291
+ font-weight: 500;
292
+ }
293
+
294
+ .strategy-btn:hover { border-color: var(--ink3); color: var(--ink2); }
295
+
296
+ .strategy-btn.active {
297
+ border-color: var(--ink);
298
+ color: var(--ink);
299
+ background: var(--surface3);
300
+ font-weight: 600;
301
+ }
302
+
303
+ /* Top-K */
304
+ .topk-btns { display: flex; gap: 4px; }
305
+ .topk-btn {
306
+ flex: 1;
307
+ padding: 7px;
308
+ border: 1px solid var(--border);
309
+ background: var(--surface2);
310
+ color: var(--muted);
311
+ font-family: var(--mono);
312
+ font-size: 11px;
313
+ border-radius: 6px;
314
+ cursor: pointer;
315
+ transition: all 0.2s;
316
+ text-align: center;
317
+ font-weight: 500;
318
+ }
319
+
320
+ .topk-btn:hover { border-color: var(--ink3); color: var(--ink2); }
321
+
322
+ .topk-btn.active {
323
+ border-color: var(--ink);
324
+ color: var(--ink);
325
+ background: var(--surface3);
326
+ font-weight: 600;
327
+ }
328
+
329
+ /* ── Main Content ────────────────────────────────────────── */
330
+ .main {
331
+ display: flex;
332
+ flex-direction: column;
333
+ height: 100vh;
334
+ padding-top: 120px;
335
+ }
336
+
337
+ .chat-area {
338
+ flex: 1;
339
+ overflow-y: auto;
340
+ padding: 20px 40px 100px;
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 24px;
344
+ }
345
+
346
+ .chat-area::-webkit-scrollbar { width: 4px; }
347
+ .chat-area::-webkit-scrollbar-track { background: transparent; }
348
+ .chat-area::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
349
+
350
+ /* Empty state */
351
+ .empty-state {
352
+ flex: 1;
353
+ display: flex;
354
+ flex-direction: column;
355
+ align-items: center;
356
+ justify-content: center;
357
+ padding: 40px;
358
+ text-align: center;
359
+ min-height: 400px;
360
+ }
361
+
362
+ .empty-glyph {
363
+ width: 80px;
364
+ height: 80px;
365
+ border: 1.5px solid var(--border);
366
+ border-radius: 50%;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ font-size: 28px;
371
+ margin: 0 auto 24px;
372
+ position: relative;
373
+ background: radial-gradient(circle, var(--surface) 40%, var(--surface2) 100%);
374
+ box-shadow: var(--shadow-lg);
375
+ color: var(--ink);
376
+ }
377
+
378
+ .empty-glyph::before {
379
+ content: '';
380
+ position: absolute;
381
+ inset: -8px;
382
+ border: 1px dashed rgba(255,255,255,0.15);
383
+ border-radius: 50%;
384
+ animation: spin 20s linear infinite;
385
+ }
386
+
387
+ .empty-glyph::after {
388
+ content: '';
389
+ position: absolute;
390
+ inset: -14px;
391
+ border: 1px dashed rgba(255,255,255,0.08);
392
+ border-radius: 50%;
393
+ animation: spin 30s linear infinite reverse;
394
+ }
395
+
396
+ @keyframes spin { to { transform: rotate(360deg); } }
397
+
398
+ .empty-title {
399
+ font-family: var(--display);
400
+ font-size: 24px;
401
+ font-weight: 600;
402
+ color: var(--ink);
403
+ margin-bottom: 10px;
404
+ letter-spacing: -0.02em;
405
+ text-shadow: 0 0 20px rgba(255,255,255,0.1);
406
+ }
407
+
408
+ .empty-sub {
409
+ font-family: var(--serif);
410
+ font-style: italic;
411
+ font-size: 14px;
412
+ color: var(--ink2);
413
+ max-width: 400px;
414
+ line-height: 1.75;
415
+ }
416
+
417
+ .empty-divider {
418
+ width: 40px;
419
+ height: 1px;
420
+ background: linear-gradient(90deg, transparent, var(--ink3), transparent);
421
+ margin: 20px auto 16px;
422
+ }
423
+
424
+ .example-queries {
425
+ display: flex;
426
+ flex-wrap: wrap;
427
+ gap: 8px;
428
+ justify-content: center;
429
+ }
430
+
431
+ .example-q {
432
+ padding: 8px 16px;
433
+ border: 1px solid var(--border);
434
+ border-radius: 20px;
435
+ font-family: var(--mono);
436
+ font-size: 9.5px;
437
+ color: var(--ink2);
438
+ cursor: pointer;
439
+ transition: all 0.2s;
440
+ background: var(--surface);
441
+ letter-spacing: 0.04em;
442
+ font-weight: 500;
443
+ }
444
+
445
+ .example-q:hover {
446
+ border-color: var(--ink);
447
+ color: var(--ink);
448
+ background: var(--surface2);
449
+ transform: translateY(-1px);
450
+ box-shadow: 0 2px 8px rgba(255,255,255,0.1);
451
+ }
452
+
453
+ /* Messages */
454
+ .message {
455
+ animation: fadeUp 0.45s cubic-bezier(0.16,1,0.3,1);
456
+ max-width: 700px;
457
+ margin: 0 auto;
458
+ width: 100%;
459
+ }
460
+
461
+ @keyframes fadeUp {
462
+ from { opacity: 0; transform: translateY(18px); }
463
+ to { opacity: 1; transform: translateY(0); }
464
+ }
465
+
466
+ /* User message */
467
+ .msg-q {
468
+ display: flex;
469
+ justify-content: flex-end;
470
+ margin-bottom: 6px;
471
+ }
472
+
473
+ .msg-q-bubble {
474
+ max-width: 80%;
475
+ background: linear-gradient(135deg, rgba(26,26,26,0.95), rgba(17,17,17,0.98));
476
+ border: 1px solid var(--border2);
477
+ border-radius: 14px 14px 2px 14px;
478
+ padding: 12px 16px;
479
+ font-family: var(--display);
480
+ font-size: 13px;
481
+ line-height: 1.55;
482
+ color: var(--ink);
483
+ box-shadow: 0 2px 12px rgba(0,0,0,0.4);
484
+ }
485
+
486
+ /* AI message */
487
+ .msg-a-header {
488
+ display: flex;
489
+ align-items: center;
490
+ gap: 10px;
491
+ margin-bottom: 10px;
492
+ }
493
+
494
+ .ai-badge {
495
+ width: 26px;
496
+ height: 26px;
497
+ background: linear-gradient(135deg, var(--surface2), var(--surface3));
498
+ border-radius: 50%;
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ font-size: 12px;
503
+ flex-shrink: 0;
504
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
505
+ border: 1px solid var(--border);
506
+ color: var(--ink);
507
+ }
508
+
509
+ .ai-label {
510
+ font-family: var(--mono);
511
+ font-size: 9px;
512
+ color: var(--ink2);
513
+ font-weight: 600;
514
+ letter-spacing: 0.08em;
515
+ }
516
+
517
+ .ai-meta {
518
+ font-family: var(--mono);
519
+ font-size: 8px;
520
+ color: var(--muted);
521
+ margin-left: auto;
522
+ letter-spacing: 0.06em;
523
+ }
524
+
525
+ .msg-a-body {
526
+ background: rgba(17, 17, 17, 0.95);
527
+ border: 1px solid var(--border);
528
+ border-radius: 2px 14px 14px 14px;
529
+ padding: 16px 20px;
530
+ box-shadow: var(--shadow);
531
+ backdrop-filter: blur(5px);
532
+ }
533
+
534
+ .answer-text {
535
+ font-family: var(--serif);
536
+ font-size: 14px;
537
+ line-height: 1.8;
538
+ color: var(--ink2);
539
+ white-space: pre-wrap;
540
+ }
541
+
542
+ /* Sources */
543
+ .sources-block {
544
+ margin-top: 14px;
545
+ padding-top: 12px;
546
+ border-top: 1px solid var(--border);
547
+ }
548
+
549
+ .sources-label {
550
+ font-family: var(--mono);
551
+ font-size: 8px;
552
+ letter-spacing: 0.2em;
553
+ text-transform: uppercase;
554
+ color: var(--muted);
555
+ margin-bottom: 8px;
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 8px;
559
+ font-weight: 500;
560
+ }
561
+
562
+ .sources-label::before {
563
+ content: '◆';
564
+ font-size: 7px;
565
+ color: var(--ink2);
566
+ }
567
+
568
+ .source-chips {
569
+ display: flex;
570
+ flex-wrap: wrap;
571
+ gap: 6px;
572
+ }
573
+
574
+ .source-chip {
575
+ padding: 4px 10px;
576
+ background: var(--surface2);
577
+ border: 1px solid var(--border);
578
+ border-radius: 5px;
579
+ font-family: var(--mono);
580
+ font-size: 8.5px;
581
+ color: var(--ink2);
582
+ display: flex;
583
+ align-items: center;
584
+ gap: 6px;
585
+ cursor: pointer;
586
+ transition: all 0.2s;
587
+ position: relative;
588
+ font-weight: 500;
589
+ }
590
+
591
+ .source-chip:hover {
592
+ background: var(--surface3);
593
+ border-color: var(--ink3);
594
+ color: var(--ink);
595
+ box-shadow: 0 2px 8px rgba(255,255,255,0.05);
596
+ transform: translateY(-1px);
597
+ }
598
+
599
+ .source-score {
600
+ background: var(--surface3);
601
+ padding: 1px 5px;
602
+ border-radius: 3px;
603
+ font-size: 7.5px;
604
+ color: var(--ink);
605
+ font-weight: 600;
606
+ border: 1px solid var(--border);
607
+ }
608
+
609
+ /* Source tooltip */
610
+ .source-tooltip {
611
+ position: absolute;
612
+ bottom: calc(100% + 8px);
613
+ left: 0;
614
+ width: 300px;
615
+ background: var(--surface);
616
+ border: 1px solid var(--border2);
617
+ border-radius: var(--radius);
618
+ padding: 12px 14px;
619
+ font-family: var(--mono);
620
+ font-size: 9px;
621
+ color: var(--ink2);
622
+ display: none;
623
+ z-index: 100;
624
+ box-shadow: var(--shadow-lg);
625
+ line-height: 1.65;
626
+ }
627
+
628
+ .source-chip:hover .source-tooltip { display: block; }
629
+
630
+ .source-tooltip-meta {
631
+ color: var(--muted);
632
+ margin-bottom: 6px;
633
+ font-size: 8px;
634
+ padding-bottom: 6px;
635
+ border-bottom: 1px solid var(--border);
636
+ }
637
+
638
+ /* Evaluation block */
639
+ .eval-block {
640
+ margin-top: 12px;
641
+ padding: 12px 14px;
642
+ background: rgba(26, 26, 26, 0.9);
643
+ border: 1px solid var(--border);
644
+ border-radius: var(--radius);
645
+ }
646
+
647
+ .eval-label {
648
+ font-family: var(--mono);
649
+ font-size: 8px;
650
+ letter-spacing: 0.18em;
651
+ text-transform: uppercase;
652
+ color: var(--ink2);
653
+ margin-bottom: 10px;
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 6px;
657
+ font-weight: 600;
658
+ }
659
+
660
+ .eval-metrics {
661
+ display: grid;
662
+ grid-template-columns: repeat(4, 1fr);
663
+ gap: 8px;
664
+ }
665
+
666
+ .eval-metric { text-align: center; }
667
+
668
+ .eval-val {
669
+ font-family: var(--display);
670
+ font-size: 16px;
671
+ font-weight: 700;
672
+ line-height: 1;
673
+ }
674
+
675
+ .eval-key {
676
+ font-family: var(--mono);
677
+ font-size: 7px;
678
+ color: var(--ink3);
679
+ margin-top: 2px;
680
+ text-transform: uppercase;
681
+ letter-spacing: 0.08em;
682
+ font-weight: 500;
683
+ }
684
+
685
+ .eval-bar {
686
+ width: 100%;
687
+ height: 3px;
688
+ background: var(--border);
689
+ border-radius: 2px;
690
+ margin-top: 4px;
691
+ overflow: hidden;
692
+ }
693
+
694
+ .eval-bar-fill {
695
+ height: 100%;
696
+ border-radius: 2px;
697
+ transition: width 1.2s cubic-bezier(0.16,1,0.3,1);
698
+ }
699
+
700
+ /* Loading */
701
+ .msg-loading .dots {
702
+ display: inline-flex;
703
+ gap: 5px;
704
+ align-items: center;
705
+ padding: 4px 0;
706
+ }
707
+
708
+ .dots span {
709
+ width: 6px;
710
+ height: 6px;
711
+ background: var(--ink);
712
+ border-radius: 50%;
713
+ animation: dot-bounce 1.4s ease-in-out infinite;
714
+ opacity: 0.6;
715
+ }
716
+
717
+ .dots span:nth-child(2) { animation-delay: 0.2s; }
718
+ .dots span:nth-child(3) { animation-delay: 0.4s; }
719
+
720
+ @keyframes dot-bounce {
721
+ 0%,80%,100% { transform: scale(0.5) translateY(0); opacity:0.4; }
722
+ 40% { transform: scale(1) translateY(-4px); opacity:1; }
723
+ }
724
+
725
+ /* ── Input Area ──────────────────────────────────────────── */
726
+ .input-area {
727
+ position: fixed;
728
+ bottom: 0;
729
+ left: 280px;
730
+ right: 320px;
731
+ padding: 16px 30px 20px;
732
+ border-top: 1px solid var(--border);
733
+ background: rgba(10, 10, 10, 0.95);
734
+ backdrop-filter: blur(10px);
735
+ }
736
+
737
+ .input-row {
738
+ display: flex;
739
+ gap: 10px;
740
+ align-items: flex-end;
741
+ max-width: 700px;
742
+ margin: 0 auto;
743
+ }
744
+
745
+ .input-box {
746
+ flex: 1;
747
+ background: rgba(17, 17, 17, 0.9);
748
+ border: 1.5px solid var(--border);
749
+ border-radius: 12px;
750
+ padding: 12px 16px;
751
+ transition: border-color 0.2s, box-shadow 0.2s;
752
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
753
+ }
754
+
755
+ .input-box:focus-within {
756
+ border-color: var(--ink2);
757
+ box-shadow: 0 0 0 3px rgba(255,255,255,0.05), inset 0 1px 3px rgba(0,0,0,0.3);
758
+ }
759
+
760
+ textarea {
761
+ width: 100%;
762
+ background: none;
763
+ border: none;
764
+ outline: none;
765
+ color: var(--ink);
766
+ font-family: var(--display);
767
+ font-size: 13px;
768
+ line-height: 1.55;
769
+ resize: none;
770
+ min-height: 44px;
771
+ max-height: 120px;
772
+ overflow-y: auto;
773
+ }
774
+
775
+ textarea::placeholder {
776
+ color: var(--muted);
777
+ font-style: italic;
778
+ font-family: var(--serif);
779
+ font-size: 13px;
780
+ }
781
+
782
+ .input-footer {
783
+ display: flex;
784
+ align-items: center;
785
+ justify-content: space-between;
786
+ margin-top: 6px;
787
+ }
788
+
789
+ .char-count {
790
+ font-family: var(--mono);
791
+ font-size: 8px;
792
+ color: var(--muted);
793
+ font-weight: 500;
794
+ }
795
+
796
+ .eval-toggle {
797
+ display: flex;
798
+ align-items: center;
799
+ gap: 6px;
800
+ cursor: pointer;
801
+ }
802
+ .eval-toggle input[type=checkbox] { display: none; }
803
+
804
+ .toggle-track {
805
+ width: 28px;
806
+ height: 15px;
807
+ background: var(--border);
808
+ border-radius: 8px;
809
+ position: relative;
810
+ transition: background 0.25s;
811
+ border: 1px solid var(--border2);
812
+ }
813
+
814
+ .toggle-track::after {
815
+ content: '';
816
+ position: absolute;
817
+ width: 11px;
818
+ height: 11px;
819
+ top: 1px;
820
+ left: 1px;
821
+ background: var(--surface);
822
+ border-radius: 50%;
823
+ transition: all 0.25s;
824
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
825
+ }
826
+
827
+ .eval-toggle input:checked + .toggle-track {
828
+ background: var(--ink2);
829
+ border-color: var(--ink);
830
+ }
831
+
832
+ .eval-toggle input:checked + .toggle-track::after {
833
+ left: 14px;
834
+ background: var(--ink);
835
+ }
836
+
837
+ .toggle-label {
838
+ font-family: var(--mono);
839
+ font-size: 8px;
840
+ color: var(--ink2);
841
+ letter-spacing: 0.04em;
842
+ font-weight: 500;
843
+ }
844
+
845
+ .send-btn {
846
+ width: 48px;
847
+ height: 48px;
848
+ background: linear-gradient(135deg, var(--surface2), var(--surface3));
849
+ border: 1px solid var(--border2);
850
+ border-radius: 12px;
851
+ cursor: pointer;
852
+ display: flex;
853
+ align-items: center;
854
+ justify-content: center;
855
+ font-size: 16px;
856
+ color: var(--ink);
857
+ transition: all 0.2s;
858
+ flex-shrink: 0;
859
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
860
+ }
861
+
862
+ .send-btn:hover {
863
+ transform: translateY(-2px);
864
+ box-shadow: 0 6px 20px rgba(0,0,0,0.4);
865
+ background: linear-gradient(135deg, var(--surface3), var(--surface));
866
+ border-color: var(--ink3);
867
+ }
868
+
869
+ .send-btn:active { transform: translateY(0); }
870
+ .send-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; box-shadow: none; }
871
+
872
+ /* ── Right Sidebar (Documents) ───────────────────────────── */
873
+ .sidebar-right {
874
+ background: rgba(10, 10, 10, 0.95);
875
+ backdrop-filter: blur(10px);
876
+ border-left: 1px solid var(--border);
877
+ display: flex;
878
+ flex-direction: column;
879
+ height: 100vh;
880
+ position: sticky;
881
+ top: 0;
882
+ overflow-y: auto;
883
+ box-shadow: -2px 0 20px rgba(0,0,0,0.5);
884
+ }
885
+
886
+ .sidebar-right::-webkit-scrollbar { width: 3px; }
887
+ .sidebar-right::-webkit-scrollbar-track { background: transparent; }
888
+ .sidebar-right::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
889
+
890
+ .docs-header {
891
+ padding: 20px;
892
+ border-bottom: 1px solid var(--border);
893
+ }
894
+
895
+ .docs-panel {
896
+ flex: 1;
897
+ padding: 20px;
898
+ overflow-y: auto;
899
+ }
900
+
901
+ .upload-zone {
902
+ border: 1.5px dashed var(--border2);
903
+ border-radius: var(--radius);
904
+ padding: 20px 14px;
905
+ text-align: center;
906
+ cursor: pointer;
907
+ transition: all 0.25s;
908
+ margin-bottom: 16px;
909
+ position: relative;
910
+ overflow: hidden;
911
+ background: linear-gradient(135deg, rgba(17,17,17,0.8) 0%, rgba(10,10,10,0.9) 100%);
912
+ }
913
+
914
+ .upload-zone:hover, .upload-zone.drag-over {
915
+ border-color: var(--ink);
916
+ background: linear-gradient(135deg, rgba(26,26,26,0.9) 0%, rgba(17,17,17,0.95) 100%);
917
+ box-shadow: 0 0 0 4px rgba(255,255,255,0.05);
918
+ }
919
+
920
+ .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
921
+ .upload-icon { font-size: 24px; margin-bottom: 8px; filter: grayscale(1); opacity: 0.8; }
922
+ .upload-text { font-family: var(--mono); font-size: 9.5px; color: var(--ink2); font-weight: 500; }
923
+ .upload-text strong { color: var(--ink); font-weight: 600; }
924
+ .upload-types { font-size: 8.5px; color: var(--muted); margin-top: 3px; letter-spacing: 0.08em; }
925
+
926
+ .doc-list {
927
+ display: flex;
928
+ flex-direction: column;
929
+ gap: 6px;
930
+ }
931
+
932
+ .doc-item {
933
+ padding: 10px 12px;
934
+ background: var(--surface);
935
+ border: 1px solid var(--border);
936
+ border-radius: var(--radius);
937
+ display: flex;
938
+ align-items: center;
939
+ gap: 10px;
940
+ transition: all 0.2s;
941
+ animation: slideIn 0.3s ease;
942
+ box-shadow: var(--shadow);
943
+ }
944
+
945
+ .doc-item:hover {
946
+ border-color: var(--border2);
947
+ box-shadow: 0 2px 8px rgba(255,255,255,0.05);
948
+ transform: translateY(-1px);
949
+ }
950
+
951
+ @keyframes slideIn {
952
+ from { opacity: 0; transform: translateX(12px); }
953
+ to { opacity: 1; transform: translateX(0); }
954
+ }
955
+
956
+ .doc-icon { font-size: 16px; flex-shrink: 0; filter: grayscale(1); }
957
+ .doc-info { flex: 1; min-width: 0; }
958
+
959
+ .doc-name {
960
+ font-family: var(--mono);
961
+ font-size: 9.5px;
962
+ color: var(--ink2);
963
+ white-space: nowrap;
964
+ overflow: hidden;
965
+ text-overflow: ellipsis;
966
+ font-weight: 500;
967
+ }
968
+
969
+ .doc-meta {
970
+ font-size: 8.5px;
971
+ color: var(--muted);
972
+ margin-top: 2px;
973
+ font-family: var(--mono);
974
+ }
975
+
976
+ .doc-status {
977
+ font-size: 7.5px;
978
+ font-family: var(--mono);
979
+ padding: 2px 6px;
980
+ border-radius: 10px;
981
+ text-transform: uppercase;
982
+ letter-spacing: 0.08em;
983
+ flex-shrink: 0;
984
+ font-weight: 600;
985
+ }
986
+
987
+ .doc-status.indexed { background: var(--teal-lt); color: var(--teal); border: 1px solid var(--border); }
988
+ .doc-status.processing { background: #1a1a1a; color: var(--amber); border: 1px solid var(--border); }
989
+ .doc-status.pending { background: var(--surface3); color: var(--muted); border: 1px solid var(--border); }
990
+ .doc-status.failed { background: var(--rose-lt); color: var(--rose); border: 1px solid var(--border); }
991
+
992
+ .doc-del {
993
+ background: none;
994
+ border: none;
995
+ color: var(--muted);
996
+ cursor: pointer;
997
+ font-size: 14px;
998
+ padding: 2px;
999
+ border-radius: 4px;
1000
+ transition: all 0.2s;
1001
+ flex-shrink: 0;
1002
+ }
1003
+ .doc-del:hover { color: var(--ink); transform: scale(1.2); }
1004
+
1005
+ /* Status bar */
1006
+ .status-bar {
1007
+ padding: 16px 20px;
1008
+ border-top: 1px solid var(--border);
1009
+ display: flex;
1010
+ gap: 0;
1011
+ background: linear-gradient(135deg, rgba(17,17,17,0.9) 0%, rgba(10,10,10,0.95) 100%);
1012
+ }
1013
+
1014
+ .stat-item {
1015
+ flex: 1;
1016
+ text-align: center;
1017
+ padding: 4px 0;
1018
+ position: relative;
1019
+ }
1020
+
1021
+ .stat-item + .stat-item::before {
1022
+ content: '';
1023
+ position: absolute;
1024
+ left: 0;
1025
+ top: 25%;
1026
+ bottom: 25%;
1027
+ width: 1px;
1028
+ background: var(--border);
1029
+ }
1030
+
1031
+ .stat-val {
1032
+ font-family: var(--display);
1033
+ font-size: 18px;
1034
+ font-weight: 800;
1035
+ color: var(--ink);
1036
+ line-height: 1;
1037
+ text-shadow: 0 0 10px rgba(255,255,255,0.2);
1038
+ }
1039
+
1040
+ .stat-key {
1041
+ font-family: var(--mono);
1042
+ font-size: 7.5px;
1043
+ color: var(--muted);
1044
+ letter-spacing: 0.12em;
1045
+ text-transform: uppercase;
1046
+ margin-top: 2px;
1047
+ font-weight: 500;
1048
+ }
1049
+
1050
+ /* ── Toasts ──────────────────────────────────────────────── */
1051
+ .toast-container {
1052
+ position: fixed;
1053
+ top: 100px;
1054
+ right: 20px;
1055
+ display: flex;
1056
+ flex-direction: column;
1057
+ gap: 8px;
1058
+ z-index: 1000;
1059
+ }
1060
+
1061
+ .toast {
1062
+ padding: 10px 14px;
1063
+ border-radius: var(--radius);
1064
+ font-family: var(--mono);
1065
+ font-size: 10px;
1066
+ display: flex;
1067
+ align-items: center;
1068
+ gap: 8px;
1069
+ animation: toastIn 0.35s cubic-bezier(0.16,1,0.3,1);
1070
+ max-width: 280px;
1071
+ box-shadow: var(--shadow-lg);
1072
+ font-weight: 500;
1073
+ }
1074
+
1075
+ @keyframes toastIn {
1076
+ from { opacity:0; transform:translateX(20px); }
1077
+ to { opacity:1; transform:translateX(0); }
1078
+ }
1079
+
1080
+ .toast.success { background: var(--teal-lt); border: 1px solid var(--border); color: var(--teal); }
1081
+ .toast.error { background: var(--rose-lt); border: 1px solid var(--border); color: var(--rose); }
1082
+ .toast.info { background: var(--surface2); border: 1px solid var(--border); color: var(--ink2); }
1083
+
1084
+ /* ── Scrollbar ───────────────────────────────────────────── */
1085
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
1086
+ ::-webkit-scrollbar-track { background: transparent; }
1087
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
1088
+
1089
+ /* ── Responsive ──────────────────────────────────────────── */
1090
+ @media (max-width: 1200px) {
1091
+ .app { grid-template-columns: 260px 1fr 280px; }
1092
+ #starfield { width: 500px; }
1093
+ }
1094
+
1095
+ @media (max-width: 900px) {
1096
+ .app { grid-template-columns: 1fr; }
1097
+ .sidebar-left, .sidebar-right {
1098
+ height: auto;
1099
+ position: relative;
1100
+ border: none;
1101
+ border-bottom: 1px solid var(--border);
1102
+ }
1103
+ .input-area {
1104
+ left: 0;
1105
+ right: 0;
1106
+ }
1107
+ #starfield {
1108
+ width: 100%;
1109
+ left: 0;
1110
+ transform: none;
1111
+ }
1112
+ .center-header {
1113
+ position: relative;
1114
+ padding: 15px;
1115
+ }
1116
+ .main {
1117
+ padding-top: 20px;
1118
+ }
1119
+ }
1120
+ </style>
1121
+ </head>
1122
+ <body>
1123
+ <canvas id="starfield"></canvas>
1124
+
1125
+ <!-- Center Header -->
1126
+ <div class="center-header">
1127
+ <div class="logo-eyebrow">Aash-31 · Intelligence</div>
1128
+ <div class="logo-title">Research <em>Assistant</em></div>
1129
+ <div class="logo-sub">v1.0.0 · Hybrid RAG + RAGAS Evaluation</div>
1130
+ </div>
1131
+
1132
+ <div class="app">
1133
+
1134
+ <!-- ══════════════ LEFT SIDEBAR (Model Config) ══════════════ -->
1135
+ <aside class="sidebar-left">
1136
+
1137
+ <div class="settings-panel">
1138
+ <div class="panel-label">Model Configuration</div>
1139
+
1140
+ <div class="field-group">
1141
+ <div class="field-label">LLM Model</div>
1142
+ <select id="modelSelect">
1143
+ <option value="arcee-ai/trinity-large-preview:free">Arcee Trinity Large (Free)</option>
1144
+ <option value="openai/gpt-4o">GPT-4o</option>
1145
+ <option value="openai/gpt-4-turbo">GPT-4 Turbo</option>
1146
+ <option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo</option>
1147
+ <option value="anthropic/claude-3-haiku">Claude 3 Haiku</option>
1148
+ <option value="mistralai/mistral-7b-instruct:free">Mistral 7B (Free)</option>
1149
+ <option value="google/gemma-2-9b-it:free">Gemma 2 9B (Free)</option>
1150
+ </select>
1151
+ </div>
1152
+
1153
+ <div class="field-group">
1154
+ <div class="field-label">
1155
+ Temperature
1156
+ <span class="val" id="tempVal">0.10</span>
1157
+ </div>
1158
+ <div class="slider-container">
1159
+ <div class="slider-track">
1160
+ <div class="slider-fill" id="tempFill" style="width: 5%"></div>
1161
+ </div>
1162
+ <input type="range" id="tempSlider" min="0" max="2" step="0.05" value="0.1"
1163
+ oninput="updateSlider(this, 'tempVal', 'tempFill', 2)" />
1164
+ </div>
1165
+ </div>
1166
+
1167
+ <div class="field-group">
1168
+ <div class="field-label">
1169
+ Top-P
1170
+ <span class="val" id="topPVal">0.90</span>
1171
+ </div>
1172
+ <div class="slider-container">
1173
+ <div class="slider-track">
1174
+ <div class="slider-fill" id="topPFill" style="width: 90%"></div>
1175
+ </div>
1176
+ <input type="range" id="topPSlider" min="0" max="1" step="0.05" value="0.9"
1177
+ oninput="updateSlider(this, 'topPVal', 'topPFill', 1)" />
1178
+ </div>
1179
+ </div>
1180
+
1181
+ <div class="field-group">
1182
+ <div class="field-label">Search Strategy</div>
1183
+ <div class="strategy-btns">
1184
+ <button class="strategy-btn" onclick="setStrategy('semantic')">Semantic</button>
1185
+ <button class="strategy-btn active" onclick="setStrategy('hybrid')">Hybrid</button>
1186
+ <button class="strategy-btn" onclick="setStrategy('bm25')">BM25</button>
1187
+ </div>
1188
+ </div>
1189
+
1190
+ <div class="field-group">
1191
+ <div class="field-label">Top-K Results</div>
1192
+ <div class="topk-btns">
1193
+ <button class="topk-btn" onclick="setTopK(3)">3</button>
1194
+ <button class="topk-btn active" onclick="setTopK(5)">5</button>
1195
+ <button class="topk-btn" onclick="setTopK(8)">8</button>
1196
+ <button class="topk-btn" onclick="setTopK(10)">10</button>
1197
+ </div>
1198
+ </div>
1199
+ </div>
1200
+
1201
+ </aside>
1202
+
1203
+ <!-- ══════════════ MAIN CONTENT ══════════════ -->
1204
+ <main class="main">
1205
+
1206
+ <div class="chat-area" id="chatArea">
1207
+ <div class="empty-state" id="emptyState">
1208
+ <div class="empty-glyph">✦</div>
1209
+ <div class="empty-title">Ready to Research</div>
1210
+ <div class="empty-sub">
1211
+ Upload your documents, then ask questions. The system will retrieve
1212
+ the most relevant passages and generate a grounded answer with citations.
1213
+ </div>
1214
+ <div class="empty-divider"></div>
1215
+ <div class="example-queries">
1216
+ <div class="example-q" onclick="fillQuery(this)">What are the main conclusions?</div>
1217
+ <div class="example-q" onclick="fillQuery(this)">Summarize the key methodology</div>
1218
+ <div class="example-q" onclick="fillQuery(this)">What evidence supports this claim?</div>
1219
+ <div class="example-q" onclick="fillQuery(this)">Compare the approaches discussed</div>
1220
+ </div>
1221
+ </div>
1222
+ </div>
1223
+
1224
+ <div class="input-area">
1225
+ <div class="input-row">
1226
+ <div class="input-box">
1227
+ <textarea id="queryInput" rows="2"
1228
+ placeholder="Ask a question about your documents…"
1229
+ onkeydown="handleKey(event)"
1230
+ oninput="updateCharCount()"></textarea>
1231
+ <div class="input-footer">
1232
+ <span class="char-count" id="charCount">0 / 2000</span>
1233
+ <label class="eval-toggle">
1234
+ <input type="checkbox" id="evalToggle" checked>
1235
+ <div class="toggle-track"></div>
1236
+ <span class="toggle-label">RAG Eval</span>
1237
+ </label>
1238
+ </div>
1239
+ </div>
1240
+ <button class="send-btn" id="sendBtn" onclick="sendQuery()">↑</button>
1241
+ </div>
1242
+ </div>
1243
+ </main>
1244
+
1245
+ <!-- ══════════════ RIGHT SIDEBAR (Documents) ══════════════ -->
1246
+ <aside class="sidebar-right">
1247
+
1248
+ <div class="docs-header">
1249
+ <div class="panel-label" style="margin-bottom: 0;">Documents</div>
1250
+ </div>
1251
+
1252
+ <div class="docs-panel">
1253
+
1254
+ <div class="upload-zone" id="uploadZone"
1255
+ ondrop="handleDrop(event)"
1256
+ ondragover="event.preventDefault(); this.classList.add('drag-over')"
1257
+ ondragleave="this.classList.remove('drag-over')">
1258
+ <input type="file" id="fileInput" multiple accept=".pdf,.txt,.docx,.csv"
1259
+ onchange="handleFileSelect(event)">
1260
+ <div class="upload-icon">📂</div>
1261
+ <div class="upload-text"><strong>Click or drag</strong> to upload</div>
1262
+ <div class="upload-types">PDF · DOCX · TXT · CSV</div>
1263
+ </div>
1264
+
1265
+ <div class="doc-list" id="docList"></div>
1266
+ </div>
1267
+
1268
+ <div class="status-bar" id="statusBar">
1269
+ <div class="stat-item">
1270
+ <div class="stat-val" id="statDocs">0</div>
1271
+ <div class="stat-key">Docs</div>
1272
+ </div>
1273
+ <div class="stat-item">
1274
+ <div class="stat-val" id="statChunks">0</div>
1275
+ <div class="stat-key">Chunks</div>
1276
+ </div>
1277
+ <div class="stat-item">
1278
+ <div class="stat-val" id="statQueries">0</div>
1279
+ <div class="stat-key">Queries</div>
1280
+ </div>
1281
+ </div>
1282
+ </aside>
1283
+
1284
+ </div>
1285
+
1286
+ <div class="toast-container" id="toastContainer"></div>
1287
+
1288
+ <script>
1289
+ // ── Starfield Animation ─────────────────────────────────────
1290
+ const canvas = document.getElementById('starfield');
1291
+ const ctx = canvas.getContext('2d');
1292
+
1293
+ let width, height;
1294
+ let stars = [];
1295
+ const numStars = 600;
1296
+ let animationId;
1297
+ let isActive = true;
1298
+
1299
+ function resize() {
1300
+ const rect = canvas.getBoundingClientRect();
1301
+ width = canvas.width = rect.width;
1302
+ height = canvas.height = rect.height;
1303
+ }
1304
+
1305
+ window.addEventListener('resize', resize);
1306
+
1307
+ // Use ResizeObserver for more reliable sizing
1308
+ const resizeObserver = new ResizeObserver(() => {
1309
+ resize();
1310
+ });
1311
+ resizeObserver.observe(canvas);
1312
+
1313
+ resize();
1314
+
1315
+ class Star {
1316
+ constructor() {
1317
+ this.reset();
1318
+ }
1319
+
1320
+ reset() {
1321
+ this.x = (Math.random() - 0.5) * width;
1322
+ this.y = (Math.random() - 0.5) * height;
1323
+ this.z = Math.random() * width;
1324
+ this.pz = this.z;
1325
+ }
1326
+
1327
+ update() {
1328
+ // Variable speed based on z-depth for parallax effect
1329
+ const speed = 2 + (1 - this.z / width) * 3;
1330
+ this.z -= speed;
1331
+ if (this.z <= 0) {
1332
+ this.reset();
1333
+ this.z = width;
1334
+ this.pz = this.z;
1335
+ }
1336
+ }
1337
+
1338
+ draw() {
1339
+ const sx = (this.x / this.z) * width + width / 2;
1340
+ const sy = (this.y / this.z) * height + height / 2;
1341
+
1342
+ const px = (this.x / this.pz) * width + width / 2;
1343
+ const py = (this.y / this.pz) * height + height / 2;
1344
+
1345
+ this.pz = this.z;
1346
+
1347
+ if (sx < 0 || sx > width || sy < 0 || sy > height) return;
1348
+
1349
+ const size = (1 - this.z / width) * 2.5;
1350
+ const alpha = (1 - this.z / width);
1351
+
1352
+ // Draw streak
1353
+ ctx.beginPath();
1354
+ ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.6})`;
1355
+ ctx.lineWidth = size * 0.3;
1356
+ ctx.moveTo(px, py);
1357
+ ctx.lineTo(sx, sy);
1358
+ ctx.stroke();
1359
+
1360
+ // Draw star head
1361
+ ctx.beginPath();
1362
+ ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
1363
+ ctx.arc(sx, sy, size * 0.4, 0, Math.PI * 2);
1364
+ ctx.fill();
1365
+ }
1366
+ }
1367
+
1368
+ for (let i = 0; i < numStars; i++) {
1369
+ stars.push(new Star());
1370
+ }
1371
+
1372
+ let frameCount = 0;
1373
+ function animate() {
1374
+ if (!isActive) {
1375
+ animationId = requestAnimationFrame(animate);
1376
+ return;
1377
+ }
1378
+
1379
+ // Clear with fade effect for trails
1380
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
1381
+ ctx.fillRect(0, 0, width, height);
1382
+
1383
+ stars.forEach(star => {
1384
+ star.update();
1385
+ star.draw();
1386
+ });
1387
+
1388
+ frameCount++;
1389
+ animationId = requestAnimationFrame(animate);
1390
+ }
1391
+
1392
+ // Start animation
1393
+ animate();
1394
+
1395
+ // Pause when tab is hidden to save resources, but resume when visible
1396
+ document.addEventListener('visibilitychange', () => {
1397
+ isActive = document.visibilityState === 'visible';
1398
+ });
1399
+
1400
+ // ── Configuration ────────────────────────────────────────────
1401
+ const API_BASE = window.location.origin.includes("localhost")
1402
+ ? "http://localhost:8000/api/v1"
1403
+ : window.location.origin + "/api/v1";
1404
+
1405
+ let state = { strategy: 'hybrid', topK: 5, loading: false, messages: [] };
1406
+
1407
+ // ── Init ──────────────────────────────────────────���──────────
1408
+ document.addEventListener('DOMContentLoaded', () => {
1409
+ loadDocuments();
1410
+ fetchStats();
1411
+ setInterval(fetchStats, 30000);
1412
+ setInterval(loadDocuments, 15000);
1413
+ });
1414
+
1415
+ // ── Sliders with visible fill ────────────────────────────────
1416
+ function updateSlider(input, valId, fillId, max) {
1417
+ const val = parseFloat(input.value);
1418
+ document.getElementById(valId).textContent = val.toFixed(2);
1419
+ const percentage = (val / max) * 100;
1420
+ document.getElementById(fillId).style.width = percentage + '%';
1421
+ }
1422
+
1423
+ // Initialize sliders
1424
+ ['tempSlider', 'topPSlider'].forEach(id => {
1425
+ const el = document.getElementById(id);
1426
+ const valId = id === 'tempSlider' ? 'tempVal' : 'topPVal';
1427
+ const fillId = id === 'tempSlider' ? 'tempFill' : 'topPFill';
1428
+ const max = id === 'tempSlider' ? 2 : 1;
1429
+ updateSlider(el, valId, fillId, max);
1430
+ });
1431
+
1432
+ // ── Strategy ─────────────────────────────────────────────────
1433
+ function setStrategy(s) {
1434
+ state.strategy = s;
1435
+ document.querySelectorAll('.strategy-btn').forEach(b =>
1436
+ b.classList.toggle('active', b.textContent.toLowerCase() === s));
1437
+ }
1438
+
1439
+ // ── Top-K ─────────────────────────────────────────────────────
1440
+ function setTopK(k) {
1441
+ state.topK = k;
1442
+ document.querySelectorAll('.topk-btn').forEach(b =>
1443
+ b.classList.toggle('active', b.textContent == k));
1444
+ }
1445
+
1446
+ // ── Upload ────────────────────────────────────────────────────
1447
+ function handleDrop(e) {
1448
+ e.preventDefault();
1449
+ document.getElementById('uploadZone').classList.remove('drag-over');
1450
+ uploadFiles(Array.from(e.dataTransfer.files));
1451
+ }
1452
+
1453
+ function handleFileSelect(e) {
1454
+ uploadFiles(Array.from(e.target.files));
1455
+ e.target.value = '';
1456
+ }
1457
+
1458
+ async function uploadFiles(files) {
1459
+ const allowed = ['.pdf', '.txt', '.docx', '.csv'];
1460
+ for (const file of files) {
1461
+ const ext = '.' + file.name.split('.').pop().toLowerCase();
1462
+ if (!allowed.includes(ext)) { toast(`${file.name}: unsupported type`, 'error'); continue; }
1463
+ if (file.size > 50 * 1024 * 1024) { toast(`${file.name}: exceeds 50MB`, 'error'); continue; }
1464
+ const fd = new FormData();
1465
+ fd.append('file', file);
1466
+ addDocToList({ doc_id: 'uploading-' + Date.now(), filename: file.name, status: 'pending', total_chunks: 0, file_size: file.size });
1467
+ try {
1468
+ const r = await fetch(`${API_BASE}/upload`, { method: 'POST', body: fd });
1469
+ const data = await r.json();
1470
+ if (!r.ok) { toast(data.error || 'Upload failed', 'error'); continue; }
1471
+ toast(`${file.name} queued for indexing`, 'success');
1472
+ setTimeout(() => loadDocuments(), 2000);
1473
+ } catch (e) { toast('Upload failed: ' + e.message, 'error'); }
1474
+ }
1475
+ }
1476
+
1477
+ // ── Doc list ──────────────────────────────────────────────────
1478
+ async function loadDocuments() {
1479
+ try {
1480
+ const r = await fetch(`${API_BASE}/documents`);
1481
+ if (!r.ok) return;
1482
+ const data = await r.json();
1483
+ renderDocList(data.documents || []);
1484
+ } catch (_) {}
1485
+ }
1486
+
1487
+ function renderDocList(docs) {
1488
+ const list = document.getElementById('docList');
1489
+ list.innerHTML = '';
1490
+ if (!docs.length) {
1491
+ list.innerHTML = `<div style="font-family:var(--mono);font-size:10px;color:var(--muted);text-align:center;padding:20px;font-style:italic">No documents indexed yet</div>`;
1492
+ return;
1493
+ }
1494
+ docs.forEach(doc => addDocToList(doc, false));
1495
+ }
1496
+
1497
+ function addDocToList(doc, prepend = true) {
1498
+ const icons = { '.pdf': '📄', '.txt': '📝', '.docx': '📃', '.csv': '📊' };
1499
+ const ext = '.' + (doc.filename || '').split('.').pop();
1500
+ const icon = icons[ext] || '📄';
1501
+ const size = formatBytes(doc.file_size || 0);
1502
+ const chunks = doc.total_chunks ? `${doc.total_chunks} chunks` : 'indexing…';
1503
+ const el = document.createElement('div');
1504
+ el.className = 'doc-item';
1505
+ el.id = 'doc-' + doc.doc_id;
1506
+ el.innerHTML = `
1507
+ <div class="doc-icon">${icon}</div>
1508
+ <div class="doc-info">
1509
+ <div class="doc-name" title="${esc(doc.filename)}">${esc(doc.filename)}</div>
1510
+ <div class="doc-meta">${size} · ${chunks}</div>
1511
+ </div>
1512
+ <span class="doc-status ${doc.status}">${doc.status}</span>
1513
+ <button class="doc-del" onclick="deleteDoc('${doc.doc_id}', this)" title="Remove">×</button>`;
1514
+ const list = document.getElementById('docList');
1515
+ if (prepend) list.prepend(el);
1516
+ else list.appendChild(el);
1517
+ }
1518
+
1519
+ async function deleteDoc(docId, btn) {
1520
+ if (!confirm('Remove this document and all its chunks?')) return;
1521
+ try {
1522
+ const r = await fetch(`${API_BASE}/documents/${docId}`, { method: 'DELETE' });
1523
+ if (r.ok) {
1524
+ document.getElementById('doc-' + docId)?.remove();
1525
+ toast('Document removed', 'info');
1526
+ fetchStats();
1527
+ } else {
1528
+ const d = await r.json();
1529
+ toast(d.error || 'Delete failed', 'error');
1530
+ }
1531
+ } catch (e) { toast('Delete failed', 'error'); }
1532
+ }
1533
+
1534
+ // ── Stats ─────────────────────────────────────────────────────
1535
+ async function fetchStats() {
1536
+ try {
1537
+ const r = await fetch('/stats');
1538
+ if (!r.ok) return;
1539
+ const s = await r.json();
1540
+ document.getElementById('statDocs').textContent = s.total_documents ?? 0;
1541
+ document.getElementById('statChunks').textContent = s.total_chunks ?? 0;
1542
+ document.getElementById('statQueries').textContent = s.total_queries ?? 0;
1543
+ } catch (_) {}
1544
+ }
1545
+
1546
+ // ── Query ─────────────────────────────────────────────────────
1547
+ function handleKey(e) {
1548
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendQuery(); }
1549
+ }
1550
+
1551
+ function updateCharCount() {
1552
+ const ta = document.getElementById('queryInput');
1553
+ document.getElementById('charCount').textContent = `${ta.value.length} / 2000`;
1554
+ ta.style.height = 'auto';
1555
+ ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
1556
+ }
1557
+
1558
+ function fillQuery(el) {
1559
+ document.getElementById('queryInput').value = el.textContent;
1560
+ updateCharCount();
1561
+ }
1562
+
1563
+ async function sendQuery() {
1564
+ if (state.loading) return;
1565
+ const input = document.getElementById('queryInput');
1566
+ const q = input.value.trim();
1567
+ if (!q) return;
1568
+
1569
+ state.loading = true;
1570
+ document.getElementById('sendBtn').disabled = true;
1571
+ document.getElementById('emptyState')?.remove();
1572
+
1573
+ input.value = '';
1574
+ updateCharCount();
1575
+ appendMessage('user', q);
1576
+
1577
+ const loadingId = 'loading-' + Date.now();
1578
+ appendLoading(loadingId);
1579
+
1580
+ const payload = {
1581
+ question: q,
1582
+ model: document.getElementById('modelSelect').value,
1583
+ temperature: parseFloat(document.getElementById('tempSlider').value),
1584
+ top_p: parseFloat(document.getElementById('topPSlider').value),
1585
+ top_k: state.topK,
1586
+ search_strategy: state.strategy,
1587
+ include_eval: document.getElementById('evalToggle').checked,
1588
+ };
1589
+
1590
+ try {
1591
+ const r = await fetch(`${API_BASE}/ask`, {
1592
+ method: 'POST',
1593
+ headers: { 'Content-Type': 'application/json' },
1594
+ body: JSON.stringify(payload),
1595
+ });
1596
+ const data = await r.json();
1597
+ document.getElementById(loadingId)?.remove();
1598
+ if (!r.ok) appendError(data.error || 'Query failed');
1599
+ else { appendAnswer(data); fetchStats(); }
1600
+ } catch (e) {
1601
+ document.getElementById(loadingId)?.remove();
1602
+ appendError('Network error: ' + e.message);
1603
+ } finally {
1604
+ state.loading = false;
1605
+ document.getElementById('sendBtn').disabled = false;
1606
+ }
1607
+ }
1608
+
1609
+ function appendMessage(role, text) {
1610
+ const chatArea = document.getElementById('chatArea');
1611
+ if (role === 'user') {
1612
+ const div = document.createElement('div');
1613
+ div.className = 'message msg-q';
1614
+ div.innerHTML = `<div class="msg-q-bubble">${esc(text)}</div>`;
1615
+ chatArea.appendChild(div);
1616
+ }
1617
+ chatArea.scrollTop = chatArea.scrollHeight;
1618
+ }
1619
+
1620
+ function appendLoading(id) {
1621
+ const chatArea = document.getElementById('chatArea');
1622
+ const div = document.createElement('div');
1623
+ div.id = id;
1624
+ div.className = 'message msg-a msg-loading';
1625
+ div.innerHTML = `
1626
+ <div class="msg-a-header">
1627
+ <div class="ai-badge">✦</div>
1628
+ <div class="ai-label">Researching</div>
1629
+ </div>
1630
+ <div class="msg-a-body" style="padding:14px 18px">
1631
+ <div class="dots"><span></span><span></span><span></span></div>
1632
+ </div>`;
1633
+ chatArea.appendChild(div);
1634
+ chatArea.scrollTop = chatArea.scrollHeight;
1635
+ }
1636
+
1637
+ function appendAnswer(data) {
1638
+ const chatArea = document.getElementById('chatArea');
1639
+ const div = document.createElement('div');
1640
+ div.className = 'message msg-a';
1641
+
1642
+ const latency = data.latency_ms?.toFixed(0) ?? '—';
1643
+ const cached = data.cached ? ' · <span style="color:var(--amber)">cached</span>' : '';
1644
+ const tokens = data.tokens_used ? ` · ${data.tokens_used} tok` : '';
1645
+ const strategy = data.search_strategy ?? 'hybrid';
1646
+
1647
+ // Sources
1648
+ let sourcesHtml = '';
1649
+ if (data.sources?.length) {
1650
+ const chips = data.sources.map(s => `
1651
+ <div class="source-chip">
1652
+ 📄 ${esc(s.filename.slice(0, 20))}${s.filename.length > 20 ? '…' : ''}
1653
+ ${s.page ? `p.${s.page}` : ''}
1654
+ <span class="source-score">${(s.score * 100).toFixed(0)}%</span>
1655
+ <div class="source-tooltip">
1656
+ <div class="source-tooltip-meta">
1657
+ ${esc(s.filename)} · Page ${s.page ?? 'N/A'} · Score: ${(s.score*100).toFixed(1)}%
1658
+ ${s.hybrid_score ? `· Hybrid: ${(s.hybrid_score*100).toFixed(1)}%` : ''}
1659
+ </div>
1660
+ ${esc(s.content.slice(0, 260))}${s.content.length > 260 ? '…' : ''}
1661
+ </div>
1662
+ </div>`).join('');
1663
+ sourcesHtml = `
1664
+ <div class="sources-block">
1665
+ <div class="sources-label">Sources — ${data.sources.length} chunks</div>
1666
+ <div class="source-chips">${chips}</div>
1667
+ </div>`;
1668
+ }
1669
+
1670
+ // Evaluation
1671
+ let evalHtml = '';
1672
+ if (data.evaluation) {
1673
+ const ev = data.evaluation;
1674
+ const metrics = [
1675
+ { key: 'Faith', val: ev.faithfulness },
1676
+ { key: 'Rel', val: ev.answer_relevance },
1677
+ { key: 'Prec', val: ev.context_precision },
1678
+ { key: 'Util', val: ev.context_utilization },
1679
+ ];
1680
+ const metricHtml = metrics.map(m => {
1681
+ const v = m.val != null ? (m.val * 100).toFixed(0) : '—';
1682
+ const pct = m.val != null ? (m.val * 100).toFixed(1) + '%' : '0%';
1683
+ const color = m.val == null ? 'var(--muted)' :
1684
+ m.val >= 0.8 ? 'var(--teal)' : m.val >= 0.5 ? 'var(--amber)' : 'var(--rose)';
1685
+ return `
1686
+ <div class="eval-metric">
1687
+ <div class="eval-val" style="color:${color}">${v}${m.val != null ? '%' : ''}</div>
1688
+ <div class="eval-key">${m.key}</div>
1689
+ <div class="eval-bar"><div class="eval-bar-fill" style="width:${pct};background:${color}"></div></div>
1690
+ </div>`;
1691
+ }).join('');
1692
+ const overall = ev.overall_score != null ? `Overall: ${(ev.overall_score*100).toFixed(0)}%` : '';
1693
+ evalHtml = `
1694
+ <div class="eval-block">
1695
+ <div class="eval-label">◆ RAGAS ${overall ? '· ' + overall : ''}</div>
1696
+ <div class="eval-metrics">${metricHtml}</div>
1697
+ </div>`;
1698
+ }
1699
+
1700
+ div.innerHTML = `
1701
+ <div class="msg-a-header">
1702
+ <div class="ai-badge">✦</div>
1703
+ <div class="ai-label">Assistant</div>
1704
+ <div class="ai-meta">${latency}ms · ${strategy}${tokens}${cached}</div>
1705
+ </div>
1706
+ <div class="msg-a-body">
1707
+ <div class="answer-text">${esc(data.answer)}</div>
1708
+ ${sourcesHtml}
1709
+ ${evalHtml}
1710
+ </div>`;
1711
+
1712
+ chatArea.appendChild(div);
1713
+ chatArea.scrollTop = chatArea.scrollHeight;
1714
+ }
1715
+
1716
+ function appendError(msg) {
1717
+ const chatArea = document.getElementById('chatArea');
1718
+ const div = document.createElement('div');
1719
+ div.className = 'message msg-a';
1720
+ div.innerHTML = `
1721
+ <div class="msg-a-header">
1722
+ <div class="ai-badge" style="background:var(--rose-lt);color:var(--rose);border-color:var(--border)">!</div>
1723
+ <div class="ai-label" style="color:var(--rose)">Error</div>
1724
+ </div>
1725
+ <div class="msg-a-body" style="border-color:var(--border);background:var(--rose-lt)">
1726
+ <div class="answer-text" style="color:var(--rose)">${esc(msg)}</div>
1727
+ </div>`;
1728
+ chatArea.appendChild(div);
1729
+ chatArea.scrollTop = chatArea.scrollHeight;
1730
+ }
1731
+
1732
+ // ── Utilities ─────────────────────────────────────────────────
1733
+ function esc(str) {
1734
+ return String(str ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1735
+ }
1736
+
1737
+ function formatBytes(bytes) {
1738
+ if (bytes < 1024) return bytes + 'B';
1739
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
1740
+ return (bytes / 1024 / 1024).toFixed(1) + 'MB';
1741
+ }
1742
+
1743
+ function toast(msg, type = 'info') {
1744
+ const container = document.getElementById('toastContainer');
1745
+ const el = document.createElement('div');
1746
+ el.className = `toast ${type}`;
1747
+ const icons = { success: '✓', error: '✕', info: '◆' };
1748
+ el.innerHTML = `<span>${icons[type]}</span> ${esc(msg)}`;
1749
+ container.appendChild(el);
1750
+ setTimeout(() => el.remove(), 4000);
1751
+ }
1752
+ </script>
1753
+ </body>
1754
+ </html>