Spaces:
Sleeping
Sleeping
Update index.html
Browse files- static/index.html +296 -775
static/index.html
CHANGED
|
@@ -6,561 +6,225 @@
|
|
| 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=
|
| 10 |
<style>
|
| 11 |
-
/* ── Reset & Tokens ─────────────────────────────────────── */
|
| 12 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 13 |
|
| 14 |
:root {
|
| 15 |
-
--bg:
|
| 16 |
-
--surface:
|
| 17 |
-
--card:
|
| 18 |
-
--
|
| 19 |
-
--
|
| 20 |
-
--
|
| 21 |
-
--
|
| 22 |
-
--
|
| 23 |
-
--accent-
|
| 24 |
-
--
|
| 25 |
-
--
|
| 26 |
-
--
|
| 27 |
-
--
|
| 28 |
-
--
|
| 29 |
-
--
|
| 30 |
-
--
|
| 31 |
-
--
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
body::
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
.
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
.
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
|
| 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 |
-
|
| 194 |
-
|
| 195 |
-
|
| 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:
|
| 240 |
-
.suggestion-chip {
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 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 |
-
.
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
.
|
| 314 |
-
|
| 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 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
.
|
| 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:
|
| 402 |
-
td { padding:
|
| 403 |
-
|
| 404 |
-
.
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
}
|
| 411 |
-
|
| 412 |
-
/*
|
| 413 |
-
.
|
| 414 |
-
|
| 415 |
-
|
| 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 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 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 |
-
/*
|
| 500 |
::-webkit-scrollbar { width: 5px; height: 5px; }
|
| 501 |
::-webkit-scrollbar-track { background: transparent; }
|
| 502 |
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 10px; }
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
#
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
padding:
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
|
|
|
| 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 |
-
<
|
| 548 |
-
<div class="logo-
|
| 549 |
-
|
| 550 |
-
<
|
| 551 |
-
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
</div>
|
| 554 |
</header>
|
| 555 |
|
| 556 |
-
<
|
| 557 |
-
<aside>
|
| 558 |
<div class="aside-section">
|
| 559 |
<div class="section-label">Data Source</div>
|
| 560 |
<div class="upload-zone" id="upload-zone">
|
| 561 |
<input type="file" id="csv-input" accept=".csv" />
|
| 562 |
<span class="upload-icon">📂</span>
|
| 563 |
-
<p><strong>Drop CSV here</strong><br
|
| 564 |
</div>
|
| 565 |
<div id="file-info">
|
| 566 |
<div class="file-name" id="file-name-display"></div>
|
|
@@ -570,314 +234,171 @@
|
|
| 570 |
<div id="schema-box"></div>
|
| 571 |
</div>
|
| 572 |
</div>
|
| 573 |
-
|
| 574 |
<div class="aside-section suggestions">
|
| 575 |
<div class="section-label">Example Queries</div>
|
| 576 |
<div id="suggestions-list">
|
| 577 |
-
<div class="suggestion-chip" style="color:var(--muted);font-style:italic;">Upload
|
| 578 |
</div>
|
| 579 |
</div>
|
| 580 |
</aside>
|
| 581 |
|
| 582 |
-
<!-- ── Main ── -->
|
| 583 |
<main>
|
| 584 |
<div id="chat">
|
| 585 |
<div id="empty-state">
|
| 586 |
-
<div class="empty-icon">
|
| 587 |
<h2>Ask anything about your data</h2>
|
| 588 |
-
<p>Upload a CSV
|
| 589 |
</div>
|
| 590 |
</div>
|
| 591 |
-
|
| 592 |
<div class="input-bar">
|
| 593 |
<div class="input-row">
|
| 594 |
-
<textarea
|
| 595 |
-
|
| 596 |
-
placeholder="Ask a question about your data… e.g. 'Show top 10 rows by sales'"
|
| 597 |
-
rows="1"
|
| 598 |
-
disabled
|
| 599 |
-
></textarea>
|
| 600 |
-
<button id="send-btn" disabled title="Send">↑</button>
|
| 601 |
</div>
|
| 602 |
-
<div class="input-hint">Enter to send · Shift+Enter
|
| 603 |
</div>
|
| 604 |
</main>
|
| 605 |
</div>
|
| 606 |
|
| 607 |
<script>
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
const toast = document.getElementById('toast');
|
| 629 |
-
|
| 630 |
-
// ── Health check ────────────────────────────────────────────
|
| 631 |
-
async function checkHealth() {
|
| 632 |
-
try {
|
| 633 |
-
const r = await fetch('/health');
|
| 634 |
-
const d = await r.json();
|
| 635 |
-
if (d.status === 'ok') {
|
| 636 |
-
statusDot.classList.add('ready');
|
| 637 |
-
statusLabel.textContent = d.model.split('/').pop().toUpperCase();
|
| 638 |
-
statusLabel.classList.add('active');
|
| 639 |
-
}
|
| 640 |
-
} catch {
|
| 641 |
-
statusLabel.textContent = 'OFFLINE';
|
| 642 |
-
}
|
| 643 |
}
|
| 644 |
checkHealth();
|
| 645 |
|
| 646 |
-
// ── Toast ────────────────────────────────────────────────────
|
| 647 |
let toastTimer;
|
| 648 |
-
function showToast(msg,
|
| 649 |
-
toast.textContent = msg;
|
| 650 |
-
toast.className = `show ${type}`;
|
| 651 |
-
clearTimeout(toastTimer);
|
| 652 |
-
toastTimer = setTimeout(() => toast.className = '', 3000);
|
| 653 |
-
}
|
| 654 |
|
| 655 |
-
|
| 656 |
-
function
|
| 657 |
-
loadingBar.className = 'active';
|
| 658 |
-
isLoading = true;
|
| 659 |
-
sendBtn.disabled = true;
|
| 660 |
-
input.disabled = true;
|
| 661 |
-
}
|
| 662 |
-
function stopLoading() {
|
| 663 |
-
loadingBar.className = 'done';
|
| 664 |
-
isLoading = false;
|
| 665 |
-
if (sessionId) {
|
| 666 |
-
sendBtn.disabled = false;
|
| 667 |
-
input.disabled = false;
|
| 668 |
-
}
|
| 669 |
-
setTimeout(() => { loadingBar.className = ''; }, 600);
|
| 670 |
-
}
|
| 671 |
|
| 672 |
-
|
| 673 |
-
uploadZone.addEventListener('
|
| 674 |
-
uploadZone.addEventListener('
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
if (file) handleUpload(file);
|
| 680 |
-
});
|
| 681 |
-
|
| 682 |
-
csvInput.addEventListener('change', e => {
|
| 683 |
-
if (e.target.files[0]) handleUpload(e.target.files[0]);
|
| 684 |
-
});
|
| 685 |
-
|
| 686 |
-
// ── Upload ───────────────────────────────────────────────────
|
| 687 |
-
async function handleUpload(file) {
|
| 688 |
-
if (!file.name.endsWith('.csv')) {
|
| 689 |
-
showToast('Only .csv files are supported', 'error');
|
| 690 |
-
return;
|
| 691 |
-
}
|
| 692 |
startLoading();
|
| 693 |
-
const fd
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
const
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
buildSuggestions(d.columns, d.table_name);
|
| 711 |
-
showToast(`✓ Loaded ${d.row_count.toLocaleString()} rows`, 'success');
|
| 712 |
-
emptyState.style.display = 'none';
|
| 713 |
-
|
| 714 |
-
// Welcome message
|
| 715 |
-
addMsg('assistant', `
|
| 716 |
-
<strong>File loaded:</strong> <code>${file.name}</code><br/>
|
| 717 |
-
Table <code>${d.table_name}</code> with ${d.row_count.toLocaleString()} rows and ${d.columns.length} columns.<br/><br/>
|
| 718 |
-
Columns: <code>${d.columns.join(', ')}</code><br/><br/>
|
| 719 |
-
Ask me anything about this dataset in plain English!
|
| 720 |
-
`);
|
| 721 |
-
} catch(e) {
|
| 722 |
-
showToast(e.message, 'error');
|
| 723 |
-
}
|
| 724 |
stopLoading();
|
| 725 |
}
|
| 726 |
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
const qs
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
`Group by ${cols[0]} and count records`,
|
| 736 |
-
];
|
| 737 |
-
suggList.innerHTML = '';
|
| 738 |
-
qs.forEach(q => {
|
| 739 |
-
const chip = document.createElement('button');
|
| 740 |
-
chip.className = 'suggestion-chip';
|
| 741 |
-
chip.textContent = q;
|
| 742 |
-
chip.onclick = () => { input.value = q; input.focus(); sendQuery(); };
|
| 743 |
-
suggList.appendChild(chip);
|
| 744 |
});
|
| 745 |
}
|
| 746 |
|
| 747 |
-
|
| 748 |
-
input.addEventListener('
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
if (!isLoading && sessionId && input.value.trim()) sendQuery();
|
| 763 |
-
});
|
| 764 |
-
|
| 765 |
-
// ── Send Query ───────────────��───────────────────────────────
|
| 766 |
-
async function sendQuery() {
|
| 767 |
-
const question = input.value.trim();
|
| 768 |
-
if (!question || !sessionId) return;
|
| 769 |
-
|
| 770 |
-
// user bubble
|
| 771 |
-
addMsg('user', escapeHtml(question));
|
| 772 |
-
input.value = '';
|
| 773 |
-
input.style.height = 'auto';
|
| 774 |
-
startLoading();
|
| 775 |
-
|
| 776 |
-
// thinking indicator
|
| 777 |
-
const thinkId = addThinking();
|
| 778 |
-
|
| 779 |
-
try {
|
| 780 |
-
const r = await fetch('/query', {
|
| 781 |
-
method: 'POST',
|
| 782 |
-
headers: { 'Content-Type': 'application/json' },
|
| 783 |
-
body: JSON.stringify({ session_id: sessionId, question })
|
| 784 |
-
});
|
| 785 |
-
const d = await r.json();
|
| 786 |
-
removeThinking(thinkId);
|
| 787 |
-
if (!r.ok) throw new Error(d.detail || 'Query failed');
|
| 788 |
-
|
| 789 |
-
addMsg('assistant', buildResultHtml(d.sql, d.results));
|
| 790 |
-
} catch(e) {
|
| 791 |
removeThinking(thinkId);
|
| 792 |
-
addMsg('assistant',
|
| 793 |
-
showToast(e.message,
|
| 794 |
}
|
| 795 |
stopLoading();
|
| 796 |
}
|
| 797 |
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
</
|
| 808 |
-
|
| 809 |
-
if (!results || results.length === 0) {
|
| 810 |
-
html += `<div style="margin-top:10px;font-size:12px;color:var(--muted);">No rows returned.</div>`;
|
| 811 |
-
return html;
|
| 812 |
-
}
|
| 813 |
-
|
| 814 |
-
const cols = Object.keys(results[0]);
|
| 815 |
-
let table = `<div class="result-table-wrap"><table><thead><tr>`;
|
| 816 |
-
cols.forEach(c => { table += `<th>${escapeHtml(c)}</th>`; });
|
| 817 |
-
table += `</tr></thead><tbody>`;
|
| 818 |
-
results.forEach(row => {
|
| 819 |
-
table += '<tr>';
|
| 820 |
-
cols.forEach(c => { table += `<td>${row[c] === null ? '<span style="color:var(--muted)">null</span>' : escapeHtml(String(row[c]))}</td>`; });
|
| 821 |
-
table += '</tr>';
|
| 822 |
-
});
|
| 823 |
-
table += `</tbody></table></div>
|
| 824 |
-
<div class="result-count">${results.length.toLocaleString()} row${results.length !== 1 ? 's' : ''} returned</div>`;
|
| 825 |
|
| 826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
}
|
| 828 |
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
navigator.clipboard.writeText(code).then(() => {
|
| 833 |
-
btn.textContent = 'Copied!';
|
| 834 |
-
setTimeout(() => btn.textContent = 'Copy', 1500);
|
| 835 |
-
});
|
| 836 |
};
|
| 837 |
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
const
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
div.innerHTML = `
|
| 844 |
-
<div class="msg-meta">${role === 'user' ? 'You' : 'QueryMind'} · ${now}</div>
|
| 845 |
-
<div class="bubble">${html}</div>`;
|
| 846 |
-
chat.appendChild(div);
|
| 847 |
-
chat.scrollTop = chat.scrollHeight;
|
| 848 |
-
return div;
|
| 849 |
}
|
| 850 |
|
| 851 |
-
let
|
| 852 |
-
function addThinking()
|
| 853 |
-
const id
|
| 854 |
-
|
| 855 |
-
div.
|
| 856 |
-
|
| 857 |
-
div.innerHTML = `
|
| 858 |
-
<div class="msg-meta">QueryMind</div>
|
| 859 |
-
<div class="thinking"><span></span><span></span><span></span></div>`;
|
| 860 |
-
chat.appendChild(div);
|
| 861 |
-
chat.scrollTop = chat.scrollHeight;
|
| 862 |
-
return id;
|
| 863 |
-
}
|
| 864 |
-
function removeThinking(id) {
|
| 865 |
-
const el = document.getElementById(id);
|
| 866 |
-
if (el) el.remove();
|
| 867 |
}
|
|
|
|
| 868 |
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
chat.
|
| 872 |
-
emptyState.style.display = '';
|
| 873 |
-
chat.appendChild(emptyState);
|
| 874 |
-
showToast('Chat cleared', 'success');
|
| 875 |
}
|
| 876 |
|
| 877 |
-
|
| 878 |
-
function escapeHtml(str) {
|
| 879 |
-
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
| 880 |
-
}
|
| 881 |
</script>
|
| 882 |
</body>
|
| 883 |
</html>
|
|
|
|
| 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=JetBrains+Mono:wght@400;600&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
| 10 |
<style>
|
|
|
|
| 11 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 12 |
|
| 13 |
:root {
|
| 14 |
+
--bg: #1a1814;
|
| 15 |
+
--surface: #211f1b;
|
| 16 |
+
--card: #272420;
|
| 17 |
+
--card2: #2e2b27;
|
| 18 |
+
--border: #373330;
|
| 19 |
+
--border2: #443f3a;
|
| 20 |
+
--accent: #e8a455;
|
| 21 |
+
--accent2: #c8843a;
|
| 22 |
+
--accent-dim: rgba(232,164,85,0.10);
|
| 23 |
+
--accent-glow: rgba(232,164,85,0.20);
|
| 24 |
+
--sql-keyword: #e8a455;
|
| 25 |
+
--sql-func: #8ecfa8;
|
| 26 |
+
--sql-string: #c8a97e;
|
| 27 |
+
--sql-num: #a8c4e0;
|
| 28 |
+
--sql-comment: #5c5650;
|
| 29 |
+
--sql-op: #d4a574;
|
| 30 |
+
--sql-default: #d4cfc9;
|
| 31 |
+
--danger: #c96b5a;
|
| 32 |
+
--success: #7ab897;
|
| 33 |
+
--text: #e8e0d5;
|
| 34 |
+
--text2: #b8afa4;
|
| 35 |
+
--muted: #7a7268;
|
| 36 |
+
--mono: 'JetBrains Mono', monospace;
|
| 37 |
+
--sans: 'Inter', sans-serif;
|
| 38 |
+
--radius: 8px;
|
| 39 |
+
--radius-lg: 14px;
|
| 40 |
+
--sidebar-w: 280px;
|
| 41 |
+
--header-h: 54px;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; line-height: 1.6; overflow-x: hidden; }
|
| 45 |
+
|
| 46 |
+
#app { display: grid; grid-template-rows: var(--header-h) 1fr; grid-template-columns: var(--sidebar-w) 1fr; height: 100vh; }
|
| 47 |
+
|
| 48 |
+
/* Header */
|
| 49 |
+
header { grid-column: 1/-1; display: flex; align-items: center; gap: 12px; padding: 0 20px; background: var(--surface); border-bottom: 1px solid var(--border); z-index: 100; }
|
| 50 |
+
.logo-wrap { display: flex; align-items: center; gap: 10px; }
|
| 51 |
+
.logo-icon { width: 30px; height: 30px; background: linear-gradient(135deg,var(--accent),var(--accent2)); border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
|
| 52 |
+
.logo-text { font-family: var(--mono); font-weight: 600; font-size: 14px; color: var(--text); letter-spacing: -0.2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
.logo-text span { color: var(--accent); }
|
| 54 |
+
#menu-btn { display: none; background: none; border: none; cursor: pointer; color: var(--text2); font-size: 20px; padding: 4px; margin-right: 4px; }
|
| 55 |
+
.header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
| 56 |
+
.clear-btn { background: none; border: 1px solid var(--border2); color: var(--muted); font-size: 11px; padding: 4px 10px; border-radius: 5px; cursor: pointer; font-family: var(--mono); letter-spacing: 0.5px; transition: all 0.15s; }
|
| 57 |
+
.clear-btn:hover { border-color: var(--danger); color: #e07a6a; }
|
| 58 |
+
.status-wrap { display: flex; align-items: center; gap: 6px; }
|
| 59 |
+
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); animation: blink 3s ease-in-out infinite; }
|
| 60 |
+
.status-dot.ready { background: var(--success); animation: pulse-ok 2s ease-in-out infinite; }
|
| 61 |
+
@keyframes blink { 0%,100%{opacity:.4} 50%{opacity:1} }
|
| 62 |
+
@keyframes pulse-ok { 0%,100%{box-shadow:0 0 0 0 var(--accent-glow)} 50%{box-shadow:0 0 0 4px transparent} }
|
| 63 |
+
.badge { font-family: var(--mono); font-size: 10px; padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border2); color: var(--muted); letter-spacing: 0.5px; }
|
| 64 |
+
.badge.active { border-color: var(--accent2); color: var(--accent); background: var(--accent-dim); }
|
| 65 |
+
|
| 66 |
+
/* Sidebar */
|
| 67 |
+
aside { background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; transition: transform 0.25s ease; }
|
| 68 |
+
.aside-section { padding: 16px 14px 12px; border-bottom: 1px solid var(--border); }
|
| 69 |
+
.section-label { font-family: var(--mono); font-size: 9px; letter-spacing: 1.5px; color: var(--muted); text-transform: uppercase; margin-bottom: 10px; }
|
| 70 |
+
.upload-zone { border: 1.5px dashed var(--border2); border-radius: var(--radius); padding: 18px 12px; text-align: center; cursor: pointer; transition: all 0.2s; position: relative; background: var(--card); }
|
| 71 |
+
.upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-dim); }
|
| 72 |
+
.upload-zone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
|
| 73 |
+
.upload-icon { font-size: 20px; margin-bottom: 6px; display: block; }
|
| 74 |
+
.upload-zone p { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
| 75 |
+
.upload-zone p strong { color: var(--text2); font-weight: 500; }
|
| 76 |
+
#file-info { display: none; background: var(--card); border: 1px solid var(--border2); border-radius: var(--radius); padding: 11px; font-size: 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
#file-info.show { display: block; }
|
| 78 |
+
.file-name { font-family: var(--mono); font-size: 10px; color: var(--accent); word-break: break-all; margin-bottom: 7px; font-weight: 600; }
|
| 79 |
+
.file-info-row { display: flex; justify-content: space-between; color: var(--muted); margin-bottom: 3px; }
|
| 80 |
+
.file-info-row span:last-child { color: var(--text2); }
|
| 81 |
+
.schema-label { font-family: var(--mono); font-size: 9px; letter-spacing: 1px; color: var(--muted); text-transform: uppercase; margin-top: 9px; margin-bottom: 4px; }
|
| 82 |
+
#schema-box { display: none; font-family: var(--mono); font-size: 10px; color: var(--muted); background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 8px; max-height: 100px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; line-height: 1.7; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
#schema-box.show { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
.aside-section.suggestions { flex: 1; overflow-y: auto; }
|
| 85 |
+
#suggestions-list { display: flex; flex-direction: column; gap: 5px; }
|
| 86 |
+
.suggestion-chip { padding: 7px 10px; border-radius: 6px; border: 1px solid var(--border); background: var(--card); font-size: 11px; color: var(--muted); cursor: pointer; transition: all 0.15s; text-align: left; font-family: var(--sans); }
|
| 87 |
+
.suggestion-chip:hover { border-color: var(--accent2); color: var(--text); background: var(--accent-dim); }
|
| 88 |
+
|
| 89 |
+
/* Main */
|
| 90 |
+
main { display: flex; flex-direction: column; overflow: hidden; }
|
| 91 |
+
#chat { flex: 1; overflow-y: auto; padding: 18px 20px; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; }
|
| 92 |
+
#empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--muted); text-align: center; }
|
| 93 |
+
.empty-icon { font-size: 36px; animation: float 4s ease-in-out infinite; }
|
| 94 |
+
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} }
|
| 95 |
+
#empty-state h2 { font-size: 15px; color: var(--text2); font-weight: 500; }
|
| 96 |
+
#empty-state p { font-size: 12px; max-width: 320px; line-height: 1.7; }
|
| 97 |
+
|
| 98 |
+
/* Messages */
|
| 99 |
+
.msg { display: flex; flex-direction: column; gap: 3px; max-width: 780px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
.msg.user { align-self: flex-end; align-items: flex-end; }
|
| 101 |
.msg.assistant { align-self: flex-start; align-items: flex-start; }
|
| 102 |
+
.msg-meta { font-size: 10px; color: var(--muted); font-family: var(--mono); padding: 0 4px; }
|
| 103 |
+
.bubble { padding: 11px 15px; border-radius: var(--radius-lg); font-size: 13.5px; line-height: 1.65; }
|
| 104 |
+
.msg.user .bubble { background: linear-gradient(135deg,var(--accent),var(--accent2)); color: #1a1410; border-bottom-right-radius: 4px; font-weight: 500; }
|
| 105 |
+
.msg.assistant .bubble { background: var(--card); border: 1px solid var(--border); border-bottom-left-radius: 4px; color: var(--text); }
|
| 106 |
+
|
| 107 |
+
/* SQL block */
|
| 108 |
+
.sql-block { background: var(--bg); border: 1px solid var(--border2); border-radius: var(--radius); margin-top: 10px; overflow: hidden; }
|
| 109 |
+
.sql-block-header { display: flex; align-items: center; justify-content: space-between; padding: 7px 12px; background: var(--card2); border-bottom: 1px solid var(--border); }
|
| 110 |
+
.sql-block-header span { font-family: var(--mono); font-size: 9px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--accent); }
|
| 111 |
+
.copy-btn { background: none; border: 1px solid var(--border2); color: var(--muted); font-size: 10px; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-family: var(--mono); transition: all 0.15s; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
|
| 113 |
+
.sql-code { padding: 14px 16px; font-family: var(--mono); font-size: 12.5px; line-height: 2; color: var(--sql-default); white-space: pre-wrap; word-break: break-word; }
|
| 114 |
+
.sql-code .kw { color: var(--sql-keyword); font-weight: 600; }
|
| 115 |
+
.sql-code .fn { color: var(--sql-func); }
|
| 116 |
+
.sql-code .st { color: var(--sql-string); }
|
| 117 |
+
.sql-code .nm { color: var(--sql-num); }
|
| 118 |
+
.sql-code .cm { color: var(--sql-comment); font-style: italic; }
|
| 119 |
+
.sql-code .op { color: var(--sql-op); }
|
| 120 |
+
|
| 121 |
+
/* Table */
|
| 122 |
+
.result-table-wrap { margin-top: 10px; border: 1px solid var(--border); border-radius: var(--radius); overflow: auto; max-height: 300px; }
|
| 123 |
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
| 124 |
+
thead th { position: sticky; top: 0; background: var(--card2); padding: 8px 13px; text-align: left; font-family: var(--mono); font-size: 10px; color: var(--accent); letter-spacing: 0.8px; border-bottom: 1px solid var(--border2); white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }
|
| 126 |
tbody tr:last-child { border-bottom: none; }
|
| 127 |
+
tbody tr:hover { background: rgba(232,164,85,0.05); }
|
| 128 |
+
td { padding: 7px 13px; color: var(--text2); white-space: nowrap; }
|
| 129 |
+
.result-count { font-family: var(--mono); font-size: 10px; color: var(--muted); margin-top: 5px; padding-left: 2px; }
|
| 130 |
+
.error-bubble { background: rgba(201,107,90,0.08); border: 1px solid rgba(201,107,90,0.3); border-radius: var(--radius); padding: 10px 14px; font-size: 12px; color: #e07a6a; margin-top: 8px; font-family: var(--mono); }
|
| 131 |
+
|
| 132 |
+
/* Thinking */
|
| 133 |
+
.thinking { display: flex; gap: 5px; align-items: center; padding: 12px 16px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); border-bottom-left-radius: 4px; }
|
| 134 |
+
.thinking span { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: think 1.2s ease-in-out infinite; }
|
| 135 |
+
.thinking span:nth-child(2){animation-delay:.2s} .thinking span:nth-child(3){animation-delay:.4s}
|
| 136 |
+
@keyframes think { 0%,60%,100%{transform:translateY(0);opacity:.35} 30%{transform:translateY(-6px);opacity:1} }
|
| 137 |
+
|
| 138 |
+
/* Input */
|
| 139 |
+
.input-bar { padding: 12px 20px 14px; background: var(--surface); border-top: 1px solid var(--border); }
|
| 140 |
+
.input-row { display: flex; gap: 9px; align-items: flex-end; }
|
| 141 |
+
#question-input { flex: 1; background: var(--card); border: 1.5px solid var(--border2); border-radius: var(--radius); padding: 10px 13px; font-size: 14px; font-family: var(--sans); color: var(--text); resize: none; min-height: 44px; max-height: 120px; outline: none; transition: border-color 0.2s; line-height: 1.5; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
#question-input:focus { border-color: var(--accent); }
|
| 143 |
#question-input::placeholder { color: var(--muted); }
|
| 144 |
#question-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 145 |
+
#send-btn { background: linear-gradient(135deg,var(--accent),var(--accent2)); color: #1a1410; border: none; border-radius: var(--radius); width: 44px; height: 44px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; flex-shrink: 0; font-weight: 700; }
|
| 146 |
+
#send-btn:hover { opacity: 0.88; transform: scale(1.04); }
|
| 147 |
+
#send-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
|
| 148 |
+
.input-hint { font-size: 11px; color: var(--muted); margin-top: 5px; padding-left: 2px; }
|
| 149 |
|
| 150 |
+
/* Toast */
|
| 151 |
+
#toast { position: fixed; bottom: 20px; right: 20px; background: var(--card2); border: 1px solid var(--border2); border-radius: var(--radius); padding: 9px 15px; font-size: 12px; font-family: var(--mono); color: var(--text); z-index: 9999; transform: translateY(50px); opacity: 0; transition: all 0.3s cubic-bezier(.34,1.56,.64,1); pointer-events: none; }
|
| 152 |
+
#toast.show { transform: translateY(0); opacity: 1; }
|
| 153 |
+
#toast.success { border-color: var(--success); color: #8ecfa8; }
|
| 154 |
+
#toast.error { border-color: var(--danger); color: #e07a6a; }
|
| 155 |
+
|
| 156 |
+
/* Loading bar */
|
| 157 |
+
#loading-bar { position: fixed; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg,var(--accent),var(--accent2)); z-index: 10000; transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; }
|
| 158 |
+
#loading-bar.active { transform: scaleX(0.8); }
|
| 159 |
+
#loading-bar.done { transform: scaleX(1); opacity: 0; transition: opacity 0.4s 0.1s; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
+
/* Scrollbar */
|
| 162 |
::-webkit-scrollbar { width: 5px; height: 5px; }
|
| 163 |
::-webkit-scrollbar-track { background: transparent; }
|
| 164 |
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 10px; }
|
| 165 |
+
|
| 166 |
+
/* Overlay */
|
| 167 |
+
#overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 90; }
|
| 168 |
+
#overlay.show { display: block; }
|
| 169 |
+
|
| 170 |
+
/* ── RESPONSIVE ── */
|
| 171 |
+
@media (max-width: 900px) {
|
| 172 |
+
:root { --sidebar-w: 240px; }
|
| 173 |
+
#chat { padding: 14px 16px; }
|
| 174 |
+
.input-bar { padding: 10px 16px 12px; }
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
@media (max-width: 640px) {
|
| 178 |
+
:root { --header-h: 50px; }
|
| 179 |
+
#app { grid-template-columns: 1fr; }
|
| 180 |
+
#menu-btn { display: flex; }
|
| 181 |
+
aside { position: fixed; top: var(--header-h); left: 0; width: 280px; height: calc(100vh - var(--header-h)); z-index: 95; transform: translateX(-100%); box-shadow: 4px 0 20px rgba(0,0,0,0.4); }
|
| 182 |
+
aside.open { transform: translateX(0); }
|
| 183 |
+
main { grid-column: 1; }
|
| 184 |
+
.msg { max-width: 95%; }
|
| 185 |
+
#chat { padding: 12px; gap: 12px; }
|
| 186 |
+
.bubble { padding: 10px 13px; font-size: 13px; }
|
| 187 |
+
.sql-code { font-size: 11.5px; padding: 12px; }
|
| 188 |
+
td, thead th { padding: 7px 10px; }
|
| 189 |
+
.input-bar { padding: 8px 12px 10px; }
|
| 190 |
+
.input-hint { display: none; }
|
| 191 |
+
.status-wrap .badge { display: none; }
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
@media (max-width: 380px) {
|
| 195 |
+
.logo-text { font-size: 13px; }
|
| 196 |
+
.clear-btn { padding: 3px 7px; font-size: 10px; }
|
| 197 |
}
|
|
|
|
|
|
|
| 198 |
</style>
|
| 199 |
</head>
|
| 200 |
<body>
|
|
|
|
| 201 |
<div id="loading-bar"></div>
|
| 202 |
<div id="toast"></div>
|
| 203 |
+
<div id="overlay" onclick="closeSidebar()"></div>
|
| 204 |
|
| 205 |
<div id="app">
|
|
|
|
| 206 |
<header>
|
| 207 |
+
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
| 208 |
+
<div class="logo-wrap">
|
| 209 |
+
<div class="logo-icon">⚡</div>
|
| 210 |
+
<div class="logo-text">Query<span>Mind</span></div>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="header-right">
|
| 213 |
+
<button class="clear-btn" onclick="clearChat()">CLEAR</button>
|
| 214 |
+
<div class="status-wrap">
|
| 215 |
+
<span id="status-dot" class="status-dot"></span>
|
| 216 |
+
<span id="status-label" class="badge">LOADING</span>
|
| 217 |
+
</div>
|
| 218 |
</div>
|
| 219 |
</header>
|
| 220 |
|
| 221 |
+
<aside id="sidebar">
|
|
|
|
| 222 |
<div class="aside-section">
|
| 223 |
<div class="section-label">Data Source</div>
|
| 224 |
<div class="upload-zone" id="upload-zone">
|
| 225 |
<input type="file" id="csv-input" accept=".csv" />
|
| 226 |
<span class="upload-icon">📂</span>
|
| 227 |
+
<p><strong>Drop CSV here</strong><br/>or click to browse</p>
|
| 228 |
</div>
|
| 229 |
<div id="file-info">
|
| 230 |
<div class="file-name" id="file-name-display"></div>
|
|
|
|
| 234 |
<div id="schema-box"></div>
|
| 235 |
</div>
|
| 236 |
</div>
|
|
|
|
| 237 |
<div class="aside-section suggestions">
|
| 238 |
<div class="section-label">Example Queries</div>
|
| 239 |
<div id="suggestions-list">
|
| 240 |
+
<div class="suggestion-chip" style="color:var(--muted);font-style:italic;cursor:default;">Upload CSV to see examples</div>
|
| 241 |
</div>
|
| 242 |
</div>
|
| 243 |
</aside>
|
| 244 |
|
|
|
|
| 245 |
<main>
|
| 246 |
<div id="chat">
|
| 247 |
<div id="empty-state">
|
| 248 |
+
<div class="empty-icon">🗂️</div>
|
| 249 |
<h2>Ask anything about your data</h2>
|
| 250 |
+
<p>Upload a CSV from the sidebar, then ask a question in plain English. QueryMind converts it to SQL and shows results.</p>
|
| 251 |
</div>
|
| 252 |
</div>
|
|
|
|
| 253 |
<div class="input-bar">
|
| 254 |
<div class="input-row">
|
| 255 |
+
<textarea id="question-input" placeholder="e.g. Show top 10 rows by sales…" rows="1" disabled></textarea>
|
| 256 |
+
<button id="send-btn" disabled>↑</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
</div>
|
| 258 |
+
<div class="input-hint">Enter to send · Shift+Enter new line</div>
|
| 259 |
</div>
|
| 260 |
</main>
|
| 261 |
</div>
|
| 262 |
|
| 263 |
<script>
|
| 264 |
+
let sessionId=null, isLoading=false, columns=[];
|
| 265 |
+
|
| 266 |
+
const chat=document.getElementById('chat'), emptyState=document.getElementById('empty-state'),
|
| 267 |
+
input=document.getElementById('question-input'), sendBtn=document.getElementById('send-btn'),
|
| 268 |
+
csvInput=document.getElementById('csv-input'), uploadZone=document.getElementById('upload-zone'),
|
| 269 |
+
fileInfo=document.getElementById('file-info'), fileNameDisp=document.getElementById('file-name-display'),
|
| 270 |
+
rowCountEl=document.getElementById('row-count'), colCountEl=document.getElementById('col-count'),
|
| 271 |
+
schemaBox=document.getElementById('schema-box'), suggList=document.getElementById('suggestions-list'),
|
| 272 |
+
statusDot=document.getElementById('status-dot'), statusLabel=document.getElementById('status-label'),
|
| 273 |
+
loadingBar=document.getElementById('loading-bar'), toast=document.getElementById('toast'),
|
| 274 |
+
sidebar=document.getElementById('sidebar'), overlay=document.getElementById('overlay');
|
| 275 |
+
|
| 276 |
+
function toggleSidebar(){ sidebar.classList.toggle('open'); overlay.classList.toggle('show'); }
|
| 277 |
+
function closeSidebar(){ sidebar.classList.remove('open'); overlay.classList.remove('show'); }
|
| 278 |
+
|
| 279 |
+
async function checkHealth(){
|
| 280 |
+
try{
|
| 281 |
+
const d=await(await fetch('/health')).json();
|
| 282 |
+
if(d.status==='ok'){ statusDot.classList.add('ready'); statusLabel.textContent=d.model.split('/').pop().toUpperCase(); statusLabel.classList.add('active'); }
|
| 283 |
+
}catch{ statusLabel.textContent='OFFLINE'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
checkHealth();
|
| 286 |
|
|
|
|
| 287 |
let toastTimer;
|
| 288 |
+
function showToast(msg,type='success'){ toast.textContent=msg; toast.className=`show ${type}`; clearTimeout(toastTimer); toastTimer=setTimeout(()=>toast.className='',3000); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
+
function startLoading(){ loadingBar.className='active'; isLoading=true; sendBtn.disabled=true; input.disabled=true; }
|
| 291 |
+
function stopLoading(){ loadingBar.className='done'; isLoading=false; if(sessionId){sendBtn.disabled=false;input.disabled=false;} setTimeout(()=>loadingBar.className='',600); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
+
uploadZone.addEventListener('dragover',e=>{e.preventDefault();uploadZone.classList.add('dragover');});
|
| 294 |
+
uploadZone.addEventListener('dragleave',()=>uploadZone.classList.remove('dragover'));
|
| 295 |
+
uploadZone.addEventListener('drop',e=>{e.preventDefault();uploadZone.classList.remove('dragover');const f=e.dataTransfer.files[0];if(f)handleUpload(f);});
|
| 296 |
+
csvInput.addEventListener('change',e=>{if(e.target.files[0])handleUpload(e.target.files[0]);});
|
| 297 |
+
|
| 298 |
+
async function handleUpload(file){
|
| 299 |
+
if(!file.name.endsWith('.csv')){showToast('Only .csv files accepted','error');return;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
startLoading();
|
| 301 |
+
const fd=new FormData(); fd.append('file',file);
|
| 302 |
+
try{
|
| 303 |
+
const r=await fetch('/upload',{method:'POST',body:fd});
|
| 304 |
+
const d=await r.json();
|
| 305 |
+
if(!r.ok)throw new Error(d.detail||'Upload failed');
|
| 306 |
+
sessionId=d.session_id; columns=d.columns;
|
| 307 |
+
fileNameDisp.textContent=file.name;
|
| 308 |
+
rowCountEl.textContent=d.row_count.toLocaleString();
|
| 309 |
+
colCountEl.textContent=d.columns.length;
|
| 310 |
+
schemaBox.textContent=d.schema;
|
| 311 |
+
schemaBox.classList.add('show'); fileInfo.classList.add('show');
|
| 312 |
+
buildSuggestions(d.columns,d.table_name);
|
| 313 |
+
showToast(`✓ Loaded ${d.row_count.toLocaleString()} rows`,'success');
|
| 314 |
+
emptyState.style.display='none'; closeSidebar();
|
| 315 |
+
addMsg('assistant',`<strong>File loaded:</strong> <code>${file.name}</code><br/>Table <code>${d.table_name}</code> · ${d.row_count.toLocaleString()} rows · ${d.columns.length} columns.<br/><br/>Columns: <code>${d.columns.join(', ')}</code><br/><br/>Ask me anything about this data in plain English.`);
|
| 316 |
+
}catch(e){showToast(e.message,'error');}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
stopLoading();
|
| 318 |
}
|
| 319 |
|
| 320 |
+
function buildSuggestions(cols,table){
|
| 321 |
+
const numCol=cols.find(c=>/num|price|val|amt|count|qty|sal|rev|cost/i.test(c))||cols[1]||cols[0];
|
| 322 |
+
const qs=[`Show the first 10 rows`,`Count total number of records`,`How many unique values in ${cols[0]}?`,`What is the average of ${numCol}?`,`Show rows where ${cols[0]} is not null`,`Group by ${cols[0]} and count records`];
|
| 323 |
+
suggList.innerHTML='';
|
| 324 |
+
qs.forEach(q=>{
|
| 325 |
+
const btn=document.createElement('button'); btn.className='suggestion-chip'; btn.textContent=q;
|
| 326 |
+
btn.onclick=()=>{input.value=q;input.focus();closeSidebar();sendQuery();};
|
| 327 |
+
suggList.appendChild(btn);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
});
|
| 329 |
}
|
| 330 |
|
| 331 |
+
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,120)+'px';});
|
| 332 |
+
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();if(!isLoading&&sessionId&&input.value.trim())sendQuery();}});
|
| 333 |
+
sendBtn.addEventListener('click',()=>{if(!isLoading&&sessionId&&input.value.trim())sendQuery();});
|
| 334 |
+
|
| 335 |
+
async function sendQuery(){
|
| 336 |
+
const question=input.value.trim(); if(!question||!sessionId)return;
|
| 337 |
+
addMsg('user',escapeHtml(question));
|
| 338 |
+
input.value=''; input.style.height='auto';
|
| 339 |
+
startLoading(); const thinkId=addThinking();
|
| 340 |
+
try{
|
| 341 |
+
const r=await fetch('/query',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sessionId,question})});
|
| 342 |
+
const d=await r.json(); removeThinking(thinkId);
|
| 343 |
+
if(!r.ok)throw new Error(d.detail||'Query failed');
|
| 344 |
+
addMsg('assistant',buildResultHtml(d.sql,d.results));
|
| 345 |
+
}catch(e){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
removeThinking(thinkId);
|
| 347 |
+
addMsg('assistant',`<div class="error-bubble">⚠ ${escapeHtml(e.message)}</div>`);
|
| 348 |
+
showToast(e.message,'error');
|
| 349 |
}
|
| 350 |
stopLoading();
|
| 351 |
}
|
| 352 |
|
| 353 |
+
function highlightSQL(raw){
|
| 354 |
+
const kw=/\b(SELECT|FROM|WHERE|AND|OR|NOT|IN|IS|NULL|LIKE|BETWEEN|ORDER BY|GROUP BY|HAVING|LIMIT|OFFSET|DISTINCT|AS|JOIN|LEFT|RIGHT|INNER|OUTER|ON|UNION|ALL|INSERT|UPDATE|DELETE|CREATE|TABLE|DROP|ALTER|WITH|CASE|WHEN|THEN|ELSE|END|EXISTS|BY|ASC|DESC|INTO|VALUES|SET)\b/gi;
|
| 355 |
+
const fn=/\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|IFNULL|ROUND|FLOOR|CEIL|ABS|LENGTH|UPPER|LOWER|TRIM|SUBSTR|REPLACE|CAST|DATE|DATETIME|NOW|RANDOM|IIF|GROUP_CONCAT)\b/gi;
|
| 356 |
+
return escapeHtml(raw)
|
| 357 |
+
.replace(/(--[^\n]*)/g,m=>`<span class="cm">${m}</span>`)
|
| 358 |
+
.replace(/('([^'\\]|\\.)*')/g,m=>`<span class="st">${m}</span>`)
|
| 359 |
+
.replace(/\b(\d+(\.\d+)?)\b/g,m=>`<span class="nm">${m}</span>`)
|
| 360 |
+
.replace(kw,m=>`<span class="kw">${m}</span>`)
|
| 361 |
+
.replace(fn,m=>`<span class="fn">${m}</span>`)
|
| 362 |
+
.replace(/([=<>!]+|[+\-*\/])/g,m=>`<span class="op">${m}</span>`);
|
| 363 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
+
function buildResultHtml(sql,results){
|
| 366 |
+
let html=`<div class="sql-block"><div class="sql-block-header"><span>Generated SQL</span><button class="copy-btn" onclick="copySql(this)">Copy</button></div><div class="sql-code">${highlightSQL(sql)}</div></div>`;
|
| 367 |
+
if(!results||results.length===0) return html+`<div style="margin-top:9px;font-size:12px;color:var(--muted);">No rows returned.</div>`;
|
| 368 |
+
const cols=Object.keys(results[0]);
|
| 369 |
+
let tbl=`<div class="result-table-wrap"><table><thead><tr>${cols.map(c=>`<th>${escapeHtml(c)}</th>`).join('')}</tr></thead><tbody>`;
|
| 370 |
+
results.forEach(row=>{tbl+=`<tr>${cols.map(c=>`<td>${row[c]===null?`<span style="color:var(--muted)">null</span>`:escapeHtml(String(row[c]))}</td>`).join('')}</tr>`;});
|
| 371 |
+
tbl+=`</tbody></table></div><div class="result-count">${results.length.toLocaleString()} row${results.length!==1?'s':''} returned</div>`;
|
| 372 |
+
return html+tbl;
|
| 373 |
}
|
| 374 |
|
| 375 |
+
window.copySql=function(btn){
|
| 376 |
+
const code=btn.closest('.sql-block').querySelector('.sql-code').textContent;
|
| 377 |
+
navigator.clipboard.writeText(code).then(()=>{btn.textContent='Copied!';setTimeout(()=>btn.textContent='Copy',1600);});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
};
|
| 379 |
|
| 380 |
+
function addMsg(role,html){
|
| 381 |
+
const now=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
|
| 382 |
+
const div=document.createElement('div'); div.className=`msg ${role}`;
|
| 383 |
+
div.innerHTML=`<div class="msg-meta">${role==='user'?'You':'QueryMind'} · ${now}</div><div class="bubble">${html}</div>`;
|
| 384 |
+
chat.appendChild(div); chat.scrollTop=chat.scrollHeight; return div;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
|
| 387 |
+
let tc=0;
|
| 388 |
+
function addThinking(){
|
| 389 |
+
const id='think-'+(++tc), div=document.createElement('div');
|
| 390 |
+
div.id=id; div.className='msg assistant';
|
| 391 |
+
div.innerHTML=`<div class="msg-meta">QueryMind</div><div class="thinking"><span></span><span></span><span></span></div>`;
|
| 392 |
+
chat.appendChild(div); chat.scrollTop=chat.scrollHeight; return id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
}
|
| 394 |
+
function removeThinking(id){const el=document.getElementById(id);if(el)el.remove();}
|
| 395 |
|
| 396 |
+
function clearChat(){
|
| 397 |
+
chat.innerHTML=''; emptyState.style.display='';
|
| 398 |
+
chat.appendChild(emptyState); showToast('Chat cleared','success');
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
+
function escapeHtml(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
|
|
|
|
|
|
|
|
| 402 |
</script>
|
| 403 |
</body>
|
| 404 |
</html>
|