nilotpaldhar2004 commited on
Commit
500f8a9
Β·
unverified Β·
1 Parent(s): 70293d1

Add files via upload

Browse files
Files changed (1) hide show
  1. index.html +874 -0
index.html ADDED
@@ -0,0 +1,874 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>QueryMind β€” Natural Language to SQL</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet" />
10
+ <style>
11
+ /* ── Reset & Tokens ─────────────────────────────────────── */
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+
14
+ :root {
15
+ --bg: #0a0c10;
16
+ --surface: #111318;
17
+ --card: #161920;
18
+ --border: #23272f;
19
+ --border2: #2e3340;
20
+ --accent: #00e5a0;
21
+ --accent2: #00b87a;
22
+ --accent-dim: rgba(0,229,160,0.10);
23
+ --accent-glow: rgba(0,229,160,0.22);
24
+ --danger: #ff4d6d;
25
+ --warn: #f59e0b;
26
+ --text: #e8eaf0;
27
+ --muted: #7a8294;
28
+ --mono: 'Space Mono', monospace;
29
+ --sans: 'DM Sans', sans-serif;
30
+ --radius: 10px;
31
+ --radius-lg:16px;
32
+ }
33
+
34
+ html, body {
35
+ height: 100%;
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ font-family: var(--sans);
39
+ font-size: 14px;
40
+ line-height: 1.6;
41
+ overflow-x: hidden;
42
+ }
43
+
44
+ /* ── Noise texture overlay ──────────────────────────────── */
45
+ body::before {
46
+ content: '';
47
+ position: fixed; inset: 0;
48
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
49
+ pointer-events: none;
50
+ z-index: 9999;
51
+ opacity: 0.5;
52
+ }
53
+
54
+ /* ── Layout ─────────────────────────────────────────────── */
55
+ #app {
56
+ display: grid;
57
+ grid-template-rows: 56px 1fr;
58
+ grid-template-columns: 300px 1fr;
59
+ height: 100vh;
60
+ }
61
+
62
+ /* ── Header ─────────────────────────────────────────────── */
63
+ header {
64
+ grid-column: 1 / -1;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 14px;
68
+ padding: 0 24px;
69
+ background: var(--surface);
70
+ border-bottom: 1px solid var(--border);
71
+ position: relative;
72
+ z-index: 10;
73
+ }
74
+
75
+ .logo-icon {
76
+ width: 28px; height: 28px;
77
+ background: var(--accent);
78
+ border-radius: 6px;
79
+ display: flex; align-items: center; justify-content: center;
80
+ font-size: 14px;
81
+ flex-shrink: 0;
82
+ }
83
+
84
+ .logo-text {
85
+ font-family: var(--mono);
86
+ font-weight: 700;
87
+ font-size: 15px;
88
+ letter-spacing: -0.3px;
89
+ color: var(--text);
90
+ }
91
+
92
+ .logo-text span { color: var(--accent); }
93
+
94
+ .header-badge {
95
+ margin-left: auto;
96
+ display: flex; align-items: center; gap: 8px;
97
+ }
98
+
99
+ .badge {
100
+ font-family: var(--mono);
101
+ font-size: 10px;
102
+ padding: 3px 8px;
103
+ border-radius: 4px;
104
+ border: 1px solid var(--border2);
105
+ color: var(--muted);
106
+ letter-spacing: 0.5px;
107
+ }
108
+
109
+ .badge.active {
110
+ border-color: var(--accent2);
111
+ color: var(--accent);
112
+ background: var(--accent-dim);
113
+ }
114
+
115
+ .status-dot {
116
+ width: 7px; height: 7px;
117
+ border-radius: 50%;
118
+ background: var(--muted);
119
+ animation: pulse-idle 3s ease-in-out infinite;
120
+ }
121
+ .status-dot.ready { background: var(--accent); animation: pulse-green 2s ease-in-out infinite; }
122
+
123
+ @keyframes pulse-idle {
124
+ 0%,100% { opacity: 0.5; } 50% { opacity: 1; }
125
+ }
126
+ @keyframes pulse-green {
127
+ 0%,100% { box-shadow: 0 0 0 0 var(--accent-glow); }
128
+ 50% { box-shadow: 0 0 0 5px transparent; }
129
+ }
130
+
131
+ /* ── Sidebar ─────────────────────────────────────────────── */
132
+ aside {
133
+ background: var(--surface);
134
+ border-right: 1px solid var(--border);
135
+ display: flex;
136
+ flex-direction: column;
137
+ overflow: hidden;
138
+ }
139
+
140
+ .aside-section {
141
+ padding: 18px 16px 14px;
142
+ border-bottom: 1px solid var(--border);
143
+ }
144
+
145
+ .section-label {
146
+ font-family: var(--mono);
147
+ font-size: 9px;
148
+ letter-spacing: 1.5px;
149
+ color: var(--muted);
150
+ text-transform: uppercase;
151
+ margin-bottom: 10px;
152
+ }
153
+
154
+ /* ── Upload Zone ─────────────────────────────────────────── */
155
+ .upload-zone {
156
+ border: 1.5px dashed var(--border2);
157
+ border-radius: var(--radius);
158
+ padding: 20px 16px;
159
+ text-align: center;
160
+ cursor: pointer;
161
+ transition: all 0.2s ease;
162
+ position: relative;
163
+ background: var(--card);
164
+ }
165
+ .upload-zone:hover, .upload-zone.dragover {
166
+ border-color: var(--accent);
167
+ background: var(--accent-dim);
168
+ }
169
+ .upload-zone input[type="file"] {
170
+ position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%;
171
+ }
172
+ .upload-icon {
173
+ font-size: 22px; margin-bottom: 6px;
174
+ display: block;
175
+ }
176
+ .upload-zone p {
177
+ font-size: 12px; color: var(--muted); line-height: 1.5;
178
+ }
179
+ .upload-zone p strong { color: var(--text); font-weight: 500; }
180
+
181
+ /* ── File Info Card ──────────────────────────────────────── */
182
+ #file-info {
183
+ display: none;
184
+ background: var(--card);
185
+ border: 1px solid var(--border2);
186
+ border-radius: var(--radius);
187
+ padding: 12px;
188
+ font-size: 12px;
189
+ }
190
+ #file-info.show { display: block; }
191
+
192
+ .file-info-row {
193
+ display: flex; justify-content: space-between;
194
+ color: var(--muted);
195
+ margin-bottom: 4px;
196
+ }
197
+ .file-info-row span:last-child { color: var(--text); }
198
+
199
+ .file-name {
200
+ font-family: var(--mono);
201
+ font-size: 11px;
202
+ color: var(--accent);
203
+ word-break: break-all;
204
+ margin-bottom: 8px;
205
+ font-weight: 700;
206
+ }
207
+
208
+ /* ── Schema Box ──────────────────────────────────────────── */
209
+ #schema-box {
210
+ display: none;
211
+ font-family: var(--mono);
212
+ font-size: 10px;
213
+ color: var(--muted);
214
+ background: var(--bg);
215
+ border: 1px solid var(--border);
216
+ border-radius: 6px;
217
+ padding: 10px;
218
+ max-height: 120px;
219
+ overflow-y: auto;
220
+ white-space: pre-wrap;
221
+ word-break: break-all;
222
+ margin-top: 10px;
223
+ line-height: 1.7;
224
+ }
225
+ #schema-box.show { display: block; }
226
+
227
+ .schema-label {
228
+ font-family: var(--mono);
229
+ font-size: 9px;
230
+ letter-spacing: 1px;
231
+ color: var(--muted);
232
+ text-transform: uppercase;
233
+ margin-top: 10px;
234
+ margin-bottom: 4px;
235
+ }
236
+
237
+ /* ── Suggestions ─────────────────────────────────────────── */
238
+ .aside-section.suggestions { flex: 1; overflow-y: auto; }
239
+ #suggestions-list { display: flex; flex-direction: column; gap: 6px; }
240
+ .suggestion-chip {
241
+ padding: 8px 10px;
242
+ border-radius: 6px;
243
+ border: 1px solid var(--border);
244
+ background: var(--card);
245
+ font-size: 11px;
246
+ color: var(--muted);
247
+ cursor: pointer;
248
+ transition: all 0.15s;
249
+ text-align: left;
250
+ font-family: var(--sans);
251
+ }
252
+ .suggestion-chip:hover {
253
+ border-color: var(--accent2);
254
+ color: var(--text);
255
+ background: var(--accent-dim);
256
+ }
257
+
258
+ /* ── Main Panel ──────────────────────────────────────────── */
259
+ main {
260
+ display: flex;
261
+ flex-direction: column;
262
+ overflow: hidden;
263
+ }
264
+
265
+ /* ── Chat Area ───────────────────────────────────────────── */
266
+ #chat {
267
+ flex: 1;
268
+ overflow-y: auto;
269
+ padding: 20px 24px;
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 18px;
273
+ scroll-behavior: smooth;
274
+ }
275
+
276
+ /* ── Empty state ─────────────────────────────────────────── */
277
+ #empty-state {
278
+ flex: 1;
279
+ display: flex;
280
+ flex-direction: column;
281
+ align-items: center;
282
+ justify-content: center;
283
+ gap: 12px;
284
+ color: var(--muted);
285
+ }
286
+ .empty-icon {
287
+ font-size: 40px;
288
+ filter: grayscale(0.5);
289
+ animation: float 4s ease-in-out infinite;
290
+ }
291
+ @keyframes float {
292
+ 0%,100% { transform: translateY(0); } 50% { transform: translateY(-8px); }
293
+ }
294
+ #empty-state h2 {
295
+ font-size: 16px;
296
+ color: var(--text);
297
+ font-weight: 500;
298
+ }
299
+ #empty-state p { font-size: 13px; text-align: center; max-width: 360px; }
300
+
301
+ /* ── Message Bubbles ─────────────────────────────────────── */
302
+ .msg { display: flex; flex-direction: column; gap: 4px; max-width: 800px; }
303
+ .msg.user { align-self: flex-end; align-items: flex-end; }
304
+ .msg.assistant { align-self: flex-start; align-items: flex-start; }
305
+
306
+ .msg-meta {
307
+ font-size: 10px;
308
+ color: var(--muted);
309
+ font-family: var(--mono);
310
+ padding: 0 4px;
311
+ }
312
+
313
+ .bubble {
314
+ padding: 12px 16px;
315
+ border-radius: var(--radius-lg);
316
+ line-height: 1.6;
317
+ font-size: 13.5px;
318
+ }
319
+ .msg.user .bubble {
320
+ background: var(--accent);
321
+ color: #000;
322
+ border-bottom-right-radius: 4px;
323
+ font-weight: 500;
324
+ }
325
+ .msg.assistant .bubble {
326
+ background: var(--card);
327
+ border: 1px solid var(--border);
328
+ border-bottom-left-radius: 4px;
329
+ color: var(--text);
330
+ }
331
+
332
+ /* ── SQL Block ───────────────────────────────────────────── */
333
+ .sql-block {
334
+ background: var(--bg);
335
+ border: 1px solid var(--border2);
336
+ border-radius: var(--radius);
337
+ margin-top: 10px;
338
+ overflow: hidden;
339
+ }
340
+ .sql-block-header {
341
+ display: flex; align-items: center; justify-content: space-between;
342
+ padding: 8px 12px;
343
+ background: #0d0f14;
344
+ border-bottom: 1px solid var(--border);
345
+ }
346
+ .sql-block-header span {
347
+ font-family: var(--mono);
348
+ font-size: 9px;
349
+ letter-spacing: 1.5px;
350
+ text-transform: uppercase;
351
+ color: var(--accent);
352
+ }
353
+ .copy-btn {
354
+ background: none;
355
+ border: 1px solid var(--border2);
356
+ color: var(--muted);
357
+ font-size: 10px;
358
+ padding: 3px 8px;
359
+ border-radius: 4px;
360
+ cursor: pointer;
361
+ font-family: var(--mono);
362
+ transition: all 0.15s;
363
+ }
364
+ .copy-btn:hover { border-color: var(--accent); color: var(--accent); }
365
+ .sql-code {
366
+ padding: 14px 16px;
367
+ font-family: var(--mono);
368
+ font-size: 12px;
369
+ color: #a8c0ff;
370
+ white-space: pre-wrap;
371
+ word-break: break-word;
372
+ line-height: 1.8;
373
+ }
374
+
375
+ /* ── Result Table ────────────────────────────────────────── */
376
+ .result-table-wrap {
377
+ margin-top: 10px;
378
+ border: 1px solid var(--border);
379
+ border-radius: var(--radius);
380
+ overflow: auto;
381
+ max-height: 320px;
382
+ }
383
+ table {
384
+ width: 100%; border-collapse: collapse;
385
+ font-size: 12px;
386
+ }
387
+ thead th {
388
+ position: sticky; top: 0;
389
+ background: #0d0f14;
390
+ padding: 8px 14px;
391
+ text-align: left;
392
+ font-family: var(--mono);
393
+ font-size: 10px;
394
+ color: var(--accent);
395
+ letter-spacing: 0.8px;
396
+ border-bottom: 1px solid var(--border2);
397
+ white-space: nowrap;
398
+ }
399
+ tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }
400
+ tbody tr:last-child { border-bottom: none; }
401
+ tbody tr:hover { background: var(--accent-dim); }
402
+ td { padding: 8px 14px; color: var(--text); white-space: nowrap; }
403
+
404
+ .result-count {
405
+ font-family: var(--mono);
406
+ font-size: 10px;
407
+ color: var(--muted);
408
+ margin-top: 6px;
409
+ padding-left: 2px;
410
+ }
411
+
412
+ /* ── Error bubble ────────────────────────────────────────── */
413
+ .error-bubble {
414
+ background: rgba(255, 77, 109, 0.08);
415
+ border: 1px solid rgba(255, 77, 109, 0.3);
416
+ border-radius: var(--radius);
417
+ padding: 10px 14px;
418
+ font-size: 12px;
419
+ color: #ff8fa3;
420
+ margin-top: 8px;
421
+ font-family: var(--mono);
422
+ }
423
+
424
+ /* ── Thinking animation ──────────────────────────────────── */
425
+ .thinking {
426
+ display: flex; gap: 5px; align-items: center;
427
+ padding: 12px 16px;
428
+ background: var(--card);
429
+ border: 1px solid var(--border);
430
+ border-radius: var(--radius-lg);
431
+ border-bottom-left-radius: 4px;
432
+ width: fit-content;
433
+ }
434
+ .thinking span {
435
+ width: 6px; height: 6px;
436
+ border-radius: 50%;
437
+ background: var(--accent);
438
+ animation: think 1.2s ease-in-out infinite;
439
+ }
440
+ .thinking span:nth-child(2) { animation-delay: 0.2s; }
441
+ .thinking span:nth-child(3) { animation-delay: 0.4s; }
442
+ @keyframes think {
443
+ 0%,60%,100% { transform: translateY(0); opacity: 0.4; }
444
+ 30% { transform: translateY(-6px); opacity: 1; }
445
+ }
446
+
447
+ /* ── Input Bar ───────────────────────────────────────────── */
448
+ .input-bar {
449
+ padding: 14px 24px 16px;
450
+ background: var(--surface);
451
+ border-top: 1px solid var(--border);
452
+ }
453
+ .input-row {
454
+ display: flex; gap: 10px; align-items: flex-end;
455
+ }
456
+ #question-input {
457
+ flex: 1;
458
+ background: var(--card);
459
+ border: 1.5px solid var(--border2);
460
+ border-radius: var(--radius);
461
+ padding: 10px 14px;
462
+ font-size: 14px;
463
+ font-family: var(--sans);
464
+ color: var(--text);
465
+ resize: none;
466
+ min-height: 44px;
467
+ max-height: 120px;
468
+ outline: none;
469
+ transition: border-color 0.2s;
470
+ line-height: 1.5;
471
+ }
472
+ #question-input:focus { border-color: var(--accent); }
473
+ #question-input::placeholder { color: var(--muted); }
474
+ #question-input:disabled { opacity: 0.5; cursor: not-allowed; }
475
+
476
+ #send-btn {
477
+ background: var(--accent);
478
+ color: #000;
479
+ border: none;
480
+ border-radius: var(--radius);
481
+ width: 44px; height: 44px;
482
+ cursor: pointer;
483
+ font-size: 18px;
484
+ display: flex; align-items: center; justify-content: center;
485
+ transition: all 0.2s;
486
+ flex-shrink: 0;
487
+ font-weight: 700;
488
+ }
489
+ #send-btn:hover { background: var(--accent2); transform: scale(1.05); }
490
+ #send-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
491
+
492
+ .input-hint {
493
+ font-size: 11px;
494
+ color: var(--muted);
495
+ margin-top: 6px;
496
+ padding-left: 2px;
497
+ }
498
+
499
+ /* ── Scrollbar ───────────────────────────────────────────── */
500
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
501
+ ::-webkit-scrollbar-track { background: transparent; }
502
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 10px; }
503
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
504
+
505
+ /* ── Toast ───────────────────────────────────────────────── */
506
+ #toast {
507
+ position: fixed; bottom: 24px; right: 24px;
508
+ background: var(--card);
509
+ border: 1px solid var(--border2);
510
+ border-radius: var(--radius);
511
+ padding: 10px 16px;
512
+ font-size: 12px;
513
+ font-family: var(--mono);
514
+ color: var(--text);
515
+ z-index: 10000;
516
+ transform: translateY(60px);
517
+ opacity: 0;
518
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
519
+ pointer-events: none;
520
+ }
521
+ #toast.show { transform: translateY(0); opacity: 1; }
522
+ #toast.success { border-color: var(--accent2); color: var(--accent); }
523
+ #toast.error { border-color: var(--danger); color: #ff8fa3; }
524
+
525
+ /* ── Loading bar ─────────────────────────────────────────── */
526
+ #loading-bar {
527
+ position: fixed; top: 0; left: 0; right: 0;
528
+ height: 2px;
529
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
530
+ z-index: 10001;
531
+ transform: scaleX(0);
532
+ transform-origin: left;
533
+ transition: transform 0.3s ease;
534
+ }
535
+ #loading-bar.active { transform: scaleX(0.8); }
536
+ #loading-bar.done { transform: scaleX(1); transition: transform 0.1s; opacity: 0; transition: opacity 0.4s 0.2s; }
537
+ </style>
538
+ </head>
539
+ <body>
540
+
541
+ <div id="loading-bar"></div>
542
+ <div id="toast"></div>
543
+
544
+ <div id="app">
545
+ <!-- ── Header ── -->
546
+ <header>
547
+ <div class="logo-icon">⚑</div>
548
+ <div class="logo-text">Query<span>Mind</span></div>
549
+ <div class="header-badge">
550
+ <span id="status-dot" class="status-dot"></span>
551
+ <span id="status-label" class="badge">INITIALIZING</span>
552
+ </div>
553
+ </header>
554
+
555
+ <!-- ── Sidebar ── -->
556
+ <aside>
557
+ <div class="aside-section">
558
+ <div class="section-label">Data Source</div>
559
+ <div class="upload-zone" id="upload-zone">
560
+ <input type="file" id="csv-input" accept=".csv" />
561
+ <span class="upload-icon">πŸ“‚</span>
562
+ <p><strong>Drop CSV here</strong><br />or click to browse</p>
563
+ </div>
564
+ <div id="file-info">
565
+ <div class="file-name" id="file-name-display"></div>
566
+ <div class="file-info-row"><span>Rows</span><span id="row-count">β€”</span></div>
567
+ <div class="file-info-row"><span>Columns</span><span id="col-count">β€”</span></div>
568
+ <div class="schema-label">Schema</div>
569
+ <div id="schema-box"></div>
570
+ </div>
571
+ </div>
572
+
573
+ <div class="aside-section suggestions">
574
+ <div class="section-label">Example Queries</div>
575
+ <div id="suggestions-list">
576
+ <div class="suggestion-chip" style="color:var(--muted);font-style:italic;">Upload a CSV to see suggestions</div>
577
+ </div>
578
+ </div>
579
+ </aside>
580
+
581
+ <!-- ── Main ── -->
582
+ <main>
583
+ <div id="chat">
584
+ <div id="empty-state">
585
+ <div class="empty-icon">πŸ’¬</div>
586
+ <h2>Ask anything about your data</h2>
587
+ <p>Upload a CSV file from the sidebar, then type a question in plain English. QueryMind will convert it to SQL and show you the results.</p>
588
+ </div>
589
+ </div>
590
+
591
+ <div class="input-bar">
592
+ <div class="input-row">
593
+ <textarea
594
+ id="question-input"
595
+ placeholder="Ask a question about your data… e.g. 'Show top 10 rows by sales'"
596
+ rows="1"
597
+ disabled
598
+ ></textarea>
599
+ <button id="send-btn" disabled title="Send">↑</button>
600
+ </div>
601
+ <div class="input-hint">Enter to send &nbsp;Β·&nbsp; Shift+Enter for new line</div>
602
+ </div>
603
+ </main>
604
+ </div>
605
+
606
+ <script>
607
+ // ── State ─────────���────────────────────────────────────────
608
+ let sessionId = null;
609
+ let isLoading = false;
610
+ let columns = [];
611
+
612
+ const chat = document.getElementById('chat');
613
+ const emptyState = document.getElementById('empty-state');
614
+ const input = document.getElementById('question-input');
615
+ const sendBtn = document.getElementById('send-btn');
616
+ const csvInput = document.getElementById('csv-input');
617
+ const uploadZone = document.getElementById('upload-zone');
618
+ const fileInfo = document.getElementById('file-info');
619
+ const fileNameDisp = document.getElementById('file-name-display');
620
+ const rowCountEl = document.getElementById('row-count');
621
+ const colCountEl = document.getElementById('col-count');
622
+ const schemaBox = document.getElementById('schema-box');
623
+ const suggList = document.getElementById('suggestions-list');
624
+ const statusDot = document.getElementById('status-dot');
625
+ const statusLabel = document.getElementById('status-label');
626
+ const loadingBar = document.getElementById('loading-bar');
627
+ const toast = document.getElementById('toast');
628
+
629
+ // ── Health check ────────────────────────────────────────────
630
+ async function checkHealth() {
631
+ try {
632
+ const r = await fetch('/health');
633
+ const d = await r.json();
634
+ if (d.status === 'ok') {
635
+ statusDot.classList.add('ready');
636
+ statusLabel.textContent = d.model.split('/').pop().toUpperCase();
637
+ statusLabel.classList.add('active');
638
+ }
639
+ } catch {
640
+ statusLabel.textContent = 'OFFLINE';
641
+ }
642
+ }
643
+ checkHealth();
644
+
645
+ // ── Toast ────────────────────────────────────────────────────
646
+ let toastTimer;
647
+ function showToast(msg, type='success') {
648
+ toast.textContent = msg;
649
+ toast.className = `show ${type}`;
650
+ clearTimeout(toastTimer);
651
+ toastTimer = setTimeout(() => toast.className = '', 3000);
652
+ }
653
+
654
+ // ── Loading bar ──────────────────────────────────────────────
655
+ function startLoading() {
656
+ loadingBar.className = 'active';
657
+ isLoading = true;
658
+ sendBtn.disabled = true;
659
+ input.disabled = true;
660
+ }
661
+ function stopLoading() {
662
+ loadingBar.className = 'done';
663
+ isLoading = false;
664
+ if (sessionId) {
665
+ sendBtn.disabled = false;
666
+ input.disabled = false;
667
+ }
668
+ setTimeout(() => { loadingBar.className = ''; }, 600);
669
+ }
670
+
671
+ // ── Drag & Drop ──────────────────────────────────────────────
672
+ uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
673
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
674
+ uploadZone.addEventListener('drop', e => {
675
+ e.preventDefault();
676
+ uploadZone.classList.remove('dragover');
677
+ const file = e.dataTransfer.files[0];
678
+ if (file) handleUpload(file);
679
+ });
680
+
681
+ csvInput.addEventListener('change', e => {
682
+ if (e.target.files[0]) handleUpload(e.target.files[0]);
683
+ });
684
+
685
+ // ── Upload ───────────────────────────────────────────────────
686
+ async function handleUpload(file) {
687
+ if (!file.name.endsWith('.csv')) {
688
+ showToast('Only .csv files are supported', 'error');
689
+ return;
690
+ }
691
+ startLoading();
692
+ const fd = new FormData();
693
+ fd.append('file', file);
694
+ try {
695
+ const r = await fetch('/upload', { method: 'POST', body: fd });
696
+ const d = await r.json();
697
+ if (!r.ok) throw new Error(d.detail || 'Upload failed');
698
+
699
+ sessionId = d.session_id;
700
+ columns = d.columns;
701
+
702
+ fileNameDisp.textContent = file.name;
703
+ rowCountEl.textContent = d.row_count.toLocaleString();
704
+ colCountEl.textContent = d.columns.length;
705
+ schemaBox.textContent = d.schema;
706
+ schemaBox.classList.add('show');
707
+ fileInfo.classList.add('show');
708
+
709
+ buildSuggestions(d.columns, d.table_name);
710
+ showToast(`βœ“ Loaded ${d.row_count.toLocaleString()} rows`, 'success');
711
+ emptyState.style.display = 'none';
712
+
713
+ // Welcome message
714
+ addMsg('assistant', `
715
+ <strong>File loaded:</strong> <code>${file.name}</code><br/>
716
+ Table <code>${d.table_name}</code> with ${d.row_count.toLocaleString()} rows and ${d.columns.length} columns.<br/><br/>
717
+ Columns: <code>${d.columns.join(', ')}</code><br/><br/>
718
+ Ask me anything about this dataset in plain English!
719
+ `);
720
+ } catch(e) {
721
+ showToast(e.message, 'error');
722
+ }
723
+ stopLoading();
724
+ }
725
+
726
+ // ── Suggestions ───────��──────────────────────────────────────
727
+ function buildSuggestions(cols, table) {
728
+ const qs = [
729
+ `Show the first 10 rows`,
730
+ `Count total number of records`,
731
+ `How many unique values in ${cols[0]}?`,
732
+ `What is the average of ${cols.find(c => /num|price|val|amt|count|qty|sal/i.test(c)) || cols[1] || cols[0]}?`,
733
+ `Show rows where ${cols[0]} is not null`,
734
+ `Group by ${cols[0]} and count records`,
735
+ ];
736
+ suggList.innerHTML = '';
737
+ qs.forEach(q => {
738
+ const chip = document.createElement('button');
739
+ chip.className = 'suggestion-chip';
740
+ chip.textContent = q;
741
+ chip.onclick = () => { input.value = q; input.focus(); sendQuery(); };
742
+ suggList.appendChild(chip);
743
+ });
744
+ }
745
+
746
+ // ── Auto-resize textarea ─────────────────────────────────────
747
+ input.addEventListener('input', () => {
748
+ input.style.height = 'auto';
749
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
750
+ });
751
+
752
+ // ── Keyboard ─────────────────────────────────────────────────
753
+ input.addEventListener('keydown', e => {
754
+ if (e.key === 'Enter' && !e.shiftKey) {
755
+ e.preventDefault();
756
+ if (!isLoading && sessionId && input.value.trim()) sendQuery();
757
+ }
758
+ });
759
+
760
+ sendBtn.addEventListener('click', () => {
761
+ if (!isLoading && sessionId && input.value.trim()) sendQuery();
762
+ });
763
+
764
+ // ── Send Query ───────────────────────────────────────────────
765
+ async function sendQuery() {
766
+ const question = input.value.trim();
767
+ if (!question || !sessionId) return;
768
+
769
+ // user bubble
770
+ addMsg('user', escapeHtml(question));
771
+ input.value = '';
772
+ input.style.height = 'auto';
773
+ startLoading();
774
+
775
+ // thinking indicator
776
+ const thinkId = addThinking();
777
+
778
+ try {
779
+ const r = await fetch('/query', {
780
+ method: 'POST',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify({ session_id: sessionId, question })
783
+ });
784
+ const d = await r.json();
785
+ removeThinking(thinkId);
786
+ if (!r.ok) throw new Error(d.detail || 'Query failed');
787
+
788
+ addMsg('assistant', buildResultHtml(d.sql, d.results));
789
+ } catch(e) {
790
+ removeThinking(thinkId);
791
+ addMsg('assistant', `<div class="error-bubble">⚠ ${escapeHtml(e.message)}</div>`);
792
+ showToast(e.message, 'error');
793
+ }
794
+ stopLoading();
795
+ }
796
+
797
+ // ── Build result HTML ─────────────────────────────────────────
798
+ function buildResultHtml(sql, results) {
799
+ let html = `
800
+ <div class="sql-block">
801
+ <div class="sql-block-header">
802
+ <span>Generated SQL</span>
803
+ <button class="copy-btn" onclick="copySql(this)">Copy</button>
804
+ </div>
805
+ <div class="sql-code">${escapeHtml(sql)}</div>
806
+ </div>`;
807
+
808
+ if (!results || results.length === 0) {
809
+ html += `<div style="margin-top:10px;font-size:12px;color:var(--muted);">No rows returned.</div>`;
810
+ return html;
811
+ }
812
+
813
+ const cols = Object.keys(results[0]);
814
+ let table = `<div class="result-table-wrap"><table><thead><tr>`;
815
+ cols.forEach(c => { table += `<th>${escapeHtml(c)}</th>`; });
816
+ table += `</tr></thead><tbody>`;
817
+ results.forEach(row => {
818
+ table += '<tr>';
819
+ cols.forEach(c => { table += `<td>${row[c] === null ? '<span style="color:var(--muted)">null</span>' : escapeHtml(String(row[c]))}</td>`; });
820
+ table += '</tr>';
821
+ });
822
+ table += `</tbody></table></div>
823
+ <div class="result-count">${results.length.toLocaleString()} row${results.length !== 1 ? 's' : ''} returned</div>`;
824
+
825
+ return html + table;
826
+ }
827
+
828
+ // ── Copy SQL ──────────────────────────────────────────────────
829
+ window.copySql = function(btn) {
830
+ const code = btn.closest('.sql-block').querySelector('.sql-code').textContent;
831
+ navigator.clipboard.writeText(code).then(() => {
832
+ btn.textContent = 'Copied!';
833
+ setTimeout(() => btn.textContent = 'Copy', 1500);
834
+ });
835
+ };
836
+
837
+ // ── Add message ───────────────────────────────────────────────
838
+ function addMsg(role, html) {
839
+ const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
840
+ const div = document.createElement('div');
841
+ div.className = `msg ${role}`;
842
+ div.innerHTML = `
843
+ <div class="msg-meta">${role === 'user' ? 'You' : 'QueryMind'} Β· ${now}</div>
844
+ <div class="bubble">${html}</div>`;
845
+ chat.appendChild(div);
846
+ chat.scrollTop = chat.scrollHeight;
847
+ return div;
848
+ }
849
+
850
+ let thinkCounter = 0;
851
+ function addThinking() {
852
+ const id = 'think-' + (++thinkCounter);
853
+ const div = document.createElement('div');
854
+ div.id = id;
855
+ div.className = 'msg assistant';
856
+ div.innerHTML = `
857
+ <div class="msg-meta">QueryMind</div>
858
+ <div class="thinking"><span></span><span></span><span></span></div>`;
859
+ chat.appendChild(div);
860
+ chat.scrollTop = chat.scrollHeight;
861
+ return id;
862
+ }
863
+ function removeThinking(id) {
864
+ const el = document.getElementById(id);
865
+ if (el) el.remove();
866
+ }
867
+
868
+ // ── Escape HTML ───────────────────────────────────────────────
869
+ function escapeHtml(str) {
870
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
871
+ }
872
+ </script>
873
+ </body>
874
+ </html>