text2sql-chatbot / static /index.html
nilotpaldhar2004's picture
Update static/index.html
1acc133 verified
Raw
History Blame Contribute Delete
36.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QueryMind — Natural Language to SQL</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* ── Deep cosmic dark base ── */
--bg: #06060f;
--bg2: #0a0a1a;
--surface: #0e0e20;
--card: #13132a;
--card2: #181830;
--border: rgba(120,100,255,0.18);
--border2: rgba(120,100,255,0.32);
/* ── Neon aurora accents ── */
--violet: #7c5cfc;
--violet2: #9b7eff;
--cyan: #00e5ff;
--cyan2: #00bcd4;
--pink: #ff4ecd;
--pink2: #e040fb;
--green: #00ffa3;
--orange: #ff9500;
/* ── Glow layers ── */
--glow-v: rgba(124,92,252,0.25);
--glow-c: rgba(0,229,255,0.18);
--glow-p: rgba(255,78,205,0.18);
--dim-v: rgba(124,92,252,0.10);
/* ── SQL syntax palette ── */
--sql-keyword: #ff6b9d;
--sql-func: #c084fc;
--sql-string: #67e8f9;
--sql-num: #fbbf24;
--sql-comment: #4b5563;
--sql-op: #fb923c;
--sql-table: #4ade80;
--sql-default: #e2e8f0;
/* ── Semantic ── */
--danger: #ff5252;
--success: #00ffa3;
--warn: #ffca28;
--text: #f0f0ff;
--text2: #9090c0;
--muted: #484870;
--mono: 'Space Mono', monospace;
--sans: 'Outfit', sans-serif;
--radius: 10px;
--radius-lg: 16px;
--sidebar-w: 290px;
--header-h: 58px;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: var(--sans);
font-size: 14px;
line-height: 1.6;
overflow-x: hidden;
}
/* ── Ambient background mesh ── */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(124,92,252,0.12) 0%, transparent 70%),
radial-gradient(ellipse 50% 35% at 80% 90%, rgba(0,229,255,0.08) 0%, transparent 65%),
radial-gradient(ellipse 40% 30% at 60% 50%, rgba(255,78,205,0.06) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
#app {
display: grid;
grid-template-rows: var(--header-h) 1fr;
grid-template-columns: var(--sidebar-w) 1fr;
height: 100vh;
position: relative;
z-index: 1;
}
/* ── Header ── */
header {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 14px;
padding: 0 22px;
background: rgba(14,14,32,0.85);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
z-index: 100;
position: relative;
}
header::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--violet), var(--cyan), var(--pink), transparent);
opacity: 0.6;
}
.logo-wrap { display: flex; align-items: center; gap: 12px; }
.logo-icon {
width: 34px; height: 34px;
background: linear-gradient(135deg, var(--violet), var(--cyan));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
box-shadow: 0 0 20px var(--glow-v), 0 0 40px rgba(0,229,255,0.12);
flex-shrink: 0;
position: relative;
}
.logo-icon::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 12px;
background: linear-gradient(135deg, var(--violet), var(--cyan));
z-index: -1;
opacity: 0.4;
filter: blur(6px);
}
.logo-text {
font-family: var(--mono);
font-weight: 700;
font-size: 15px;
letter-spacing: -0.5px;
color: var(--text);
}
.logo-text span {
background: linear-gradient(90deg, var(--violet2), var(--cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#menu-btn {
display: none;
background: none; border: none; cursor: pointer;
color: var(--text2); font-size: 20px; padding: 4px;
margin-right: 2px; line-height: 1;
}
.header-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.clear-btn {
background: none;
border: 1px solid var(--border2);
color: var(--text2);
font-size: 10px; padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-family: var(--mono);
letter-spacing: 1px;
transition: all 0.2s;
text-transform: uppercase;
}
.clear-btn:hover {
border-color: var(--danger);
color: var(--danger);
background: rgba(255,82,82,0.08);
box-shadow: 0 0 12px rgba(255,82,82,0.15);
}
.status-wrap { display: flex; align-items: center; gap: 7px; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--muted);
}
.status-dot.ready {
background: var(--success);
box-shadow: 0 0 8px var(--success), 0 0 16px rgba(0,255,163,0.3);
animation: pulse-ok 2.5s ease-in-out infinite;
}
@keyframes pulse-ok {
0%,100% { box-shadow: 0 0 8px var(--success), 0 0 16px rgba(0,255,163,0.3); }
50% { box-shadow: 0 0 16px var(--success), 0 0 32px rgba(0,255,163,0.5); }
}
.badge {
font-family: var(--mono);
font-size: 9px; padding: 3px 9px;
border-radius: 5px;
border: 1px solid var(--border2);
color: var(--text2);
letter-spacing: 0.8px;
text-transform: uppercase;
}
.badge.active {
border-color: rgba(124,92,252,0.5);
color: var(--violet2);
background: rgba(124,92,252,0.12);
box-shadow: 0 0 10px rgba(124,92,252,0.15);
}
/* ── Sidebar ── */
aside {
background: rgba(13,13,32,0.9);
backdrop-filter: blur(16px);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
overflow: hidden;
transition: transform 0.25s ease;
position: relative;
}
.aside-section {
padding: 18px 16px 14px;
border-bottom: 1px solid var(--border);
}
.section-label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 2px;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 12px;
display: flex; align-items: center; gap: 6px;
}
.section-label::before {
content: '';
display: inline-block; width: 14px; height: 1px;
background: linear-gradient(90deg, var(--violet), transparent);
}
/* Upload zone */
.upload-zone {
border: 1.5px dashed rgba(124,92,252,0.35);
border-radius: var(--radius);
padding: 22px 14px;
text-align: center;
cursor: pointer;
transition: all 0.25s;
position: relative;
background: rgba(124,92,252,0.04);
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--violet2);
background: rgba(124,92,252,0.10);
box-shadow: 0 0 24px rgba(124,92,252,0.15), inset 0 0 20px rgba(124,92,252,0.05);
}
.upload-zone input[type="file"] {
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%;
}
.upload-icon {
font-size: 28px; margin-bottom: 8px; display: block;
filter: drop-shadow(0 0 12px rgba(124,92,252,0.5));
}
.upload-zone p { font-size: 12px; color: var(--text2); line-height: 1.6; }
.upload-zone p strong { color: var(--violet2); font-weight: 600; }
/* File info */
#file-info {
display: none;
background: var(--card);
border: 1px solid var(--border2);
border-radius: var(--radius);
padding: 12px;
font-size: 12px;
margin-top: 12px;
}
#file-info.show { display: block; }
.file-name {
font-family: var(--mono); font-size: 10px;
color: var(--cyan); word-break: break-all;
margin-bottom: 9px; font-weight: 700;
text-shadow: 0 0 10px rgba(0,229,255,0.4);
}
.file-info-row {
display: flex; justify-content: space-between;
color: var(--text2); margin-bottom: 4px; font-size: 11px;
}
.file-info-row span:first-child { color: var(--muted); }
.file-info-row span:last-child { color: var(--violet2); font-weight: 600; }
.schema-label {
font-family: var(--mono); font-size: 9px;
letter-spacing: 1.2px; color: var(--muted);
text-transform: uppercase; margin-top: 10px; margin-bottom: 5px;
}
#schema-box {
display: none;
font-family: var(--mono); font-size: 9.5px;
color: var(--text2);
background: rgba(0,0,0,0.4);
border: 1px solid var(--border);
border-radius: 6px; padding: 9px;
max-height: 90px; overflow-y: auto;
white-space: pre-wrap; word-break: break-all;
line-height: 1.8;
}
#schema-box.show { display: block; }
/* Suggestions */
.aside-section.suggestions { flex: 1; overflow-y: auto; }
#suggestions-list { display: flex; flex-direction: column; gap: 6px; }
.suggestion-chip {
padding: 9px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
font-size: 11px; color: var(--text2);
cursor: pointer;
transition: all 0.18s;
text-align: left;
font-family: var(--sans);
line-height: 1.4;
}
.suggestion-chip:hover {
border-color: var(--cyan2);
color: var(--cyan);
background: rgba(0,229,255,0.06);
box-shadow: 0 0 12px rgba(0,229,255,0.10);
transform: translateX(3px);
}
/* ── Main ── */
main {
display: flex; flex-direction: column;
overflow: hidden; background: transparent;
}
#chat {
flex: 1; overflow-y: auto;
padding: 24px 28px;
display: flex; flex-direction: column;
gap: 18px; scroll-behavior: smooth;
}
/* Empty state */
#empty-state {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 14px; text-align: center; padding: 40px 24px;
}
.empty-icon {
font-size: 52px;
animation: float 4s ease-in-out infinite;
filter: drop-shadow(0 0 24px rgba(124,92,252,0.5));
}
@keyframes float {
0%,100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-10px) rotate(2deg); }
}
#empty-state h2 {
font-size: 20px; color: var(--text); font-weight: 600;
background: linear-gradient(90deg, var(--violet2), var(--cyan));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
#empty-state p {
font-size: 13px; max-width: 320px;
line-height: 1.8; color: var(--text2);
}
.empty-hint {
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
margin-top: 6px;
}
.empty-tag {
padding: 4px 12px; border-radius: 20px;
border: 1px solid var(--border2);
font-size: 11px; color: var(--muted);
font-family: var(--mono);
}
/* ── Messages ── */
.msg { display: flex; flex-direction: column; gap: 4px; max-width: 860px; width: 100%; }
.msg.user { align-self: flex-end; align-items: flex-end; max-width: 72%; }
.msg.assistant { align-self: flex-start; align-items: flex-start; }
.msg-meta {
font-size: 10px; color: var(--muted);
font-family: var(--mono); padding: 0 4px;
letter-spacing: 0.5px;
}
.bubble {
padding: 12px 17px;
border-radius: var(--radius-lg);
font-size: 13.5px; line-height: 1.7;
}
.msg.user .bubble {
background: linear-gradient(135deg, var(--violet), #5a3fc0);
color: #fff;
border-bottom-right-radius: 4px;
box-shadow: 0 4px 20px rgba(124,92,252,0.35), 0 0 0 1px rgba(155,126,255,0.25);
}
.msg.assistant .bubble {
background: var(--card);
border: 1px solid var(--border2);
border-bottom-left-radius: 4px;
color: var(--text);
width: 100%;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
/* ── SQL Block ── */
.sql-block {
background: #030308;
border: 1px solid rgba(124,92,252,0.3);
border-radius: var(--radius);
margin-top: 12px;
overflow: hidden;
box-shadow: 0 0 30px rgba(124,92,252,0.08);
}
.sql-block-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 14px;
background: rgba(124,92,252,0.08);
border-bottom: 1px solid rgba(124,92,252,0.2);
}
.sql-block-label { display: flex; align-items: center; gap: 7px; }
.sql-dot { width: 9px; height: 9px; border-radius: 50%; }
.sql-dot-r { background: #ff5f57; box-shadow: 0 0 6px #ff5f57; }
.sql-dot-y { background: #febc2e; box-shadow: 0 0 6px #febc2e; }
.sql-dot-g { background: #28c840; box-shadow: 0 0 6px #28c840; }
.sql-block-header span.sql-lang {
font-family: var(--mono); font-size: 9px;
letter-spacing: 2px; text-transform: uppercase;
color: var(--violet2); margin-left: 4px;
}
.copy-btn {
background: none;
border: 1px solid rgba(124,92,252,0.3);
color: var(--text2); font-size: 10px;
padding: 3px 10px; border-radius: 5px;
cursor: pointer; font-family: var(--mono);
transition: all 0.18s;
}
.copy-btn:hover {
border-color: var(--violet2); color: var(--violet2);
background: rgba(124,92,252,0.1);
box-shadow: 0 0 10px rgba(124,92,252,0.2);
}
/* SQL syntax highlighting */
.sql-code {
padding: 18px 20px;
font-family: var(--mono); font-size: 12.5px;
line-height: 2; color: var(--sql-default);
white-space: pre-wrap; word-break: break-word;
}
.sql-code .kw { color: var(--sql-keyword); font-weight: 700; }
.sql-code .fn { color: var(--sql-func); }
.sql-code .st { color: var(--sql-string); }
.sql-code .nm { color: var(--sql-num); }
.sql-code .cm { color: var(--sql-comment); font-style: italic; }
.sql-code .op { color: var(--sql-op); }
.sql-code .id { color: var(--sql-table); }
/* ── Result Table ── */
.result-table-wrap {
margin-top: 12px;
border: 1px solid var(--border2);
border-radius: var(--radius);
overflow: auto; max-height: 300px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
table { width: 100%; border-collapse: collapse; font-size: 12px; }
thead th {
position: sticky; top: 0;
background: rgba(124,92,252,0.15);
padding: 9px 14px;
text-align: left;
font-family: var(--mono); font-size: 10px;
color: var(--cyan); letter-spacing: 1px;
border-bottom: 1px solid var(--border2);
white-space: nowrap;
text-shadow: 0 0 10px rgba(0,229,255,0.4);
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.12s;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: rgba(124,92,252,0.06); }
td {
padding: 8px 14px; color: var(--text2);
white-space: nowrap; font-size: 12px;
}
.result-count {
font-family: var(--mono); font-size: 10px;
color: var(--muted); margin-top: 6px; padding-left: 3px;
}
.result-count strong { color: var(--violet2); }
/* Error */
.error-bubble {
background: rgba(255,82,82,0.07);
border: 1px solid rgba(255,82,82,0.3);
border-radius: var(--radius);
padding: 11px 15px; font-size: 12px;
color: #ff9090; margin-top: 10px;
font-family: var(--mono);
box-shadow: 0 0 16px rgba(255,82,82,0.08);
}
/* Thinking */
.thinking {
display: flex; gap: 6px; align-items: center;
padding: 14px 18px;
background: var(--card);
border: 1px solid var(--border2);
border-radius: var(--radius-lg);
border-bottom-left-radius: 4px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.thinking span {
width: 7px; height: 7px; border-radius: 50%;
background: var(--violet);
animation: think 1.3s ease-in-out infinite;
box-shadow: 0 0 8px var(--violet);
}
.thinking span:nth-child(1) { background: var(--violet); box-shadow: 0 0 8px var(--violet); }
.thinking span:nth-child(2) { animation-delay: .18s; background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }
.thinking span:nth-child(3) { animation-delay: .36s; background: var(--pink); box-shadow: 0 0 8px var(--pink); }
@keyframes think {
0%,60%,100% { transform: translateY(0); opacity: 0.3; }
30% { transform: translateY(-8px); opacity: 1; }
}
/* ── Input Bar ── */
.input-bar {
padding: 14px 22px 16px;
background: rgba(14,14,32,0.9);
backdrop-filter: blur(20px);
border-top: 1px solid var(--border);
position: relative;
}
.input-bar::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; height: 1px;
background: linear-gradient(90deg, transparent, var(--violet), var(--cyan), transparent);
opacity: 0.5;
}
.input-row { display: flex; gap: 10px; align-items: flex-end; }
#question-input {
flex: 1;
background: var(--card);
border: 1.5px solid var(--border2);
border-radius: var(--radius);
padding: 11px 15px;
font-size: 14px; font-family: var(--sans);
color: var(--text); resize: none;
min-height: 46px; max-height: 130px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
line-height: 1.5;
}
#question-input:focus {
border-color: var(--violet2);
box-shadow: 0 0 0 3px rgba(124,92,252,0.15), 0 0 20px rgba(124,92,252,0.08);
}
#question-input::placeholder { color: var(--muted); }
#question-input:disabled { opacity: 0.35; cursor: not-allowed; }
#send-btn {
background: linear-gradient(135deg, var(--violet), var(--cyan2));
color: #fff; border: none;
border-radius: var(--radius);
width: 46px; height: 46px;
cursor: pointer; font-size: 20px;
display: flex; align-items: center; justify-content: center;
transition: all 0.22s; flex-shrink: 0; font-weight: 700;
box-shadow: 0 4px 16px rgba(124,92,252,0.4);
}
#send-btn:hover {
box-shadow: 0 4px 28px rgba(124,92,252,0.6), 0 0 40px rgba(0,229,255,0.2);
transform: scale(1.06) translateY(-1px);
}
#send-btn:disabled {
opacity: 0.25; cursor: not-allowed;
transform: none; box-shadow: none;
}
.input-hint {
font-size: 11px; color: var(--muted);
margin-top: 6px; padding-left: 2px;
font-family: var(--mono); letter-spacing: 0.3px;
}
/* ── Toast ── */
#toast {
position: fixed; bottom: 22px; right: 22px;
background: var(--card2);
border: 1px solid var(--border2);
border-radius: var(--radius);
padding: 10px 16px; font-size: 12px;
font-family: var(--mono); color: var(--text);
z-index: 9999;
transform: translateY(60px); opacity: 0;
transition: all 0.3s cubic-bezier(.34,1.56,.64,1);
pointer-events: none; max-width: 290px;
}
#toast.show { transform: translateY(0); opacity: 1; }
#toast.success {
border-color: rgba(0,255,163,0.4); color: var(--success);
box-shadow: 0 0 20px rgba(0,255,163,0.15);
}
#toast.error {
border-color: rgba(255,82,82,0.4); color: #ff9090;
box-shadow: 0 0 20px rgba(255,82,82,0.15);
}
/* ── Loading bar ── */
#loading-bar {
position: fixed; top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, var(--violet), var(--cyan), var(--pink), var(--violet));
background-size: 200% 100%;
z-index: 10000;
transform: scaleX(0); transform-origin: left;
transition: transform 0.4s ease;
}
#loading-bar.active {
transform: scaleX(0.75);
animation: shimmer 1.5s linear infinite;
}
#loading-bar.done { transform: scaleX(1); opacity: 0; transition: opacity 0.4s 0.1s; }
@keyframes shimmer { 0%{background-position:0% 0%} 100%{background-position:200% 0%} }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(124,92,252,0.3); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--violet); }
/* ── Mobile overlay ── */
#overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.7); z-index: 90;
backdrop-filter: blur(4px);
}
#overlay.show { display: block; }
/* ── Responsive ── */
@media (max-width: 900px) {
:root { --sidebar-w: 250px; }
#chat { padding: 16px 18px; }
.input-bar { padding: 10px 18px 13px; }
.msg.user { max-width: 84%; }
}
@media (max-width: 640px) {
:root { --header-h: 52px; }
#app { grid-template-columns: 1fr; }
#menu-btn { display: flex; align-items: center; }
aside {
position: fixed; top: var(--header-h); left: 0;
width: 82vw; max-width: 300px;
height: calc(100vh - var(--header-h));
z-index: 95; transform: translateX(-100%);
box-shadow: 8px 0 40px rgba(0,0,0,0.6);
}
aside.open { transform: translateX(0); }
main { grid-column: 1; }
.msg { max-width: 100%; }
.msg.user { max-width: 86%; }
#chat { padding: 12px 14px; gap: 14px; }
.bubble { padding: 10px 14px; font-size: 13px; }
.sql-code { font-size: 11px; padding: 12px 14px; line-height: 1.9; }
td, thead th { padding: 6px 10px; font-size: 11px; }
.input-bar { padding: 8px 12px 10px; }
.input-hint { display: none; }
.status-wrap .badge { display: none; }
#toast { bottom: 12px; right: 12px; left: 12px; max-width: none; }
}
@media (max-width: 380px) {
.logo-text { font-size: 13px; }
.clear-btn { display: none; }
}
</style>
</head>
<body>
<div id="loading-bar"></div>
<div id="toast"></div>
<div id="overlay" onclick="closeSidebar()"></div>
<div id="app">
<header>
<button id="menu-btn" onclick="toggleSidebar()"></button>
<div class="logo-wrap">
<div class="logo-icon"></div>
<div class="logo-text">Query<span>Mind</span></div>
</div>
<div class="header-right">
<button class="clear-btn" onclick="clearChat()">⌫ Clear</button>
<div class="status-wrap">
<span id="status-dot" class="status-dot"></span>
<span id="status-label" class="badge">Loading…</span>
</div>
</div>
</header>
<aside id="sidebar">
<div class="aside-section">
<div class="section-label">Data Source</div>
<div class="upload-zone" id="upload-zone">
<input type="file" id="csv-input" accept=".csv" />
<span class="upload-icon">🗂️</span>
<p><strong>Drop your CSV here</strong><br/>or click to browse files</p>
</div>
<div id="file-info">
<div class="file-name" id="file-name-display"></div>
<div class="file-info-row"><span>Rows</span><span id="row-count"></span></div>
<div class="file-info-row"><span>Columns</span><span id="col-count"></span></div>
<div class="schema-label">Schema</div>
<div id="schema-box"></div>
</div>
</div>
<div class="aside-section suggestions">
<div class="section-label">Example Queries</div>
<div id="suggestions-list">
<div class="suggestion-chip" style="cursor:default;font-style:italic;opacity:0.5;">
Upload a CSV to see smart suggestions
</div>
</div>
</div>
</aside>
<main>
<div id="chat">
<div id="empty-state">
<div class="empty-icon">🔮</div>
<h2>Ask anything about your data</h2>
<p>Upload a CSV from the sidebar, then ask a question in plain English — QueryMind converts it to SQL and returns results instantly.</p>
<div class="empty-hint">
<span class="empty-tag">CSV Upload</span>
<span class="empty-tag">Natural Language</span>
<span class="empty-tag">Live SQL</span>
<span class="empty-tag">Instant Results</span>
</div>
</div>
</div>
<div class="input-bar">
<div class="input-row">
<textarea id="question-input" placeholder="e.g. Show top 10 rows by revenue…" rows="1" disabled></textarea>
<button id="send-btn" disabled title="Send (Enter)"></button>
</div>
<div class="input-hint">Enter to send &nbsp;·&nbsp; Shift+Enter for new line</div>
</div>
</main>
</div>
<script>
let sessionId = null, isLoading = false, columns = [];
const chat = document.getElementById('chat'),
emptyState = document.getElementById('empty-state'),
input = document.getElementById('question-input'),
sendBtn = document.getElementById('send-btn'),
csvInput = document.getElementById('csv-input'),
uploadZone = document.getElementById('upload-zone'),
fileInfo = document.getElementById('file-info'),
fileNameDisp= document.getElementById('file-name-display'),
rowCountEl = document.getElementById('row-count'),
colCountEl = document.getElementById('col-count'),
schemaBox = document.getElementById('schema-box'),
suggList = document.getElementById('suggestions-list'),
statusDot = document.getElementById('status-dot'),
statusLabel = document.getElementById('status-label'),
loadingBar = document.getElementById('loading-bar'),
toast = document.getElementById('toast'),
sidebar = document.getElementById('sidebar'),
overlay = document.getElementById('overlay');
function toggleSidebar() { sidebar.classList.toggle('open'); overlay.classList.toggle('show'); }
function closeSidebar() { sidebar.classList.remove('open'); overlay.classList.remove('show'); }
// ── Health check ──
async function checkHealth() {
try {
const d = await (await fetch('/health')).json();
if (d.status === 'ok') {
statusDot.classList.add('ready');
// Show shortened model name from model_name field
const label = d.model_loaded
? (d.model_name || '').split('/').pop().toUpperCase()
: 'READY';
statusLabel.textContent = label;
statusLabel.classList.add('active');
}
} catch {
statusLabel.textContent = 'OFFLINE';
statusDot.style.background = 'var(--danger)';
}
}
checkHealth();
// ── Toast ──
let toastTimer;
function showToast(msg, type = 'success') {
toast.textContent = msg;
toast.className = `show ${type}`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.className = '', 3400);
}
// ── Loading state ──
function startLoading() {
loadingBar.className = 'active'; isLoading = true;
sendBtn.disabled = true; input.disabled = true;
}
function stopLoading() {
loadingBar.className = 'done'; isLoading = false;
if (sessionId) { sendBtn.disabled = false; input.disabled = false; }
setTimeout(() => loadingBar.className = '', 650);
}
// ── Drag & drop ──
uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
uploadZone.addEventListener('drop', e => {
e.preventDefault(); uploadZone.classList.remove('dragover');
const f = e.dataTransfer.files[0]; if (f) handleUpload(f);
});
csvInput.addEventListener('change', e => { if (e.target.files[0]) handleUpload(e.target.files[0]); });
// ── Upload ──
async function handleUpload(file) {
if (!file.name.endsWith('.csv')) { showToast('Only .csv files accepted', 'error'); return; }
startLoading();
const fd = new FormData(); fd.append('file', file);
try {
const r = await fetch('/upload', { method: 'POST', body: fd });
const d = await r.json();
if (!r.ok) throw new Error(d.detail || 'Upload failed');
sessionId = d.session_id; columns = d.columns;
fileNameDisp.textContent = file.name;
rowCountEl.textContent = d.row_count.toLocaleString();
colCountEl.textContent = d.columns.length;
schemaBox.textContent = d.schema || '';
if (d.schema) schemaBox.classList.add('show');
fileInfo.classList.add('show');
buildSuggestions(d.columns, d.table_name);
showToast(`✓ Loaded ${d.row_count.toLocaleString()} rows`, 'success');
emptyState.style.display = 'none';
closeSidebar();
addMsg('assistant',
`<strong>✓ File loaded:</strong> <code>${escapeHtml(file.name)}</code><br/>` +
`Table <code>${escapeHtml(d.table_name)}</code> · <strong>${d.row_count.toLocaleString()}</strong> rows · <strong>${d.columns.length}</strong> columns.<br/><br/>` +
`<span style="color:var(--muted);font-size:12px;">Columns:</span> <code style="color:var(--cyan);font-size:11px;">${escapeHtml(d.columns.join(', '))}</code><br/><br/>` +
`Ask me anything about this data in plain English.`
);
} catch(e) {
showToast(e.message, 'error');
addMsg('assistant', `<div class="error-bubble">⚠ Upload error: ${escapeHtml(e.message)}</div>`);
}
stopLoading();
}
// ── Smart suggestions ──
function buildSuggestions(cols, table) {
const numCol = cols.find(c => /num|price|val|amt|count|qty|sal|rev|cost|total/i.test(c)) || cols[1] || cols[0];
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`,
`What is the maximum ${numCol}?`,
`Show last 5 rows`,
];
suggList.innerHTML = '';
qs.forEach(q => {
const btn = document.createElement('button');
btn.className = 'suggestion-chip'; btn.textContent = q;
btn.onclick = () => { input.value = q; input.focus(); closeSidebar(); sendQuery(); };
suggList.appendChild(btn);
});
}
// ── Auto-resize textarea ──
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 130) + 'px';
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!isLoading && sessionId && input.value.trim()) sendQuery();
}
});
sendBtn.addEventListener('click', () => {
if (!isLoading && sessionId && input.value.trim()) sendQuery();
});
// ── Query ──
async function sendQuery() {
const question = input.value.trim(); if (!question || !sessionId) return;
addMsg('user', escapeHtml(question));
input.value = ''; input.style.height = 'auto';
startLoading();
const thinkId = addThinking();
try {
const r = await fetch('/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, question })
});
const d = await r.json(); removeThinking(thinkId);
if (!r.ok) throw new Error(d.detail || 'Query failed');
addMsg('assistant', buildResultHtml(d.sql, d.results));
} catch(e) {
removeThinking(thinkId);
addMsg('assistant', `<div class="error-bubble">⚠ ${escapeHtml(e.message)}</div>`);
showToast(e.message, 'error');
}
stopLoading();
}
// ── SQL syntax highlighting ──
function highlightSQL(raw) {
const kw = /\b(SELECT|FROM|WHERE|AND|OR|NOT|IN|IS|NULL|LIKE|BETWEEN|ORDER\s+BY|GROUP\s+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;
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;
return escapeHtml(raw)
.replace(/(--[^\n]*)/g, m => `<span class="cm">${m}</span>`)
.replace(/('(?:[^'\\]|\\.)*')/g, m => `<span class="st">${m}</span>`)
.replace(/\b(\d+(?:\.\d+)?)\b/g, m => `<span class="nm">${m}</span>`)
.replace(kw, m => `<span class="kw">${m}</span>`)
.replace(fn, m => `<span class="fn">${m}</span>`)
.replace(/(?<![<\w])([=<>!]+|[+\-*\/])(?![\w>])/g, m => `<span class="op">${m}</span>`);
}
// ── Build result HTML ──
function buildResultHtml(sql, results) {
let html = `
<div class="sql-block">
<div class="sql-block-header">
<div class="sql-block-label">
<span class="sql-dot sql-dot-r"></span>
<span class="sql-dot sql-dot-y"></span>
<span class="sql-dot sql-dot-g"></span>
<span class="sql-lang">SQL Query</span>
</div>
<button class="copy-btn" onclick="copySql(this)">Copy</button>
</div>
<div class="sql-code">${highlightSQL(sql)}</div>
</div>`;
if (!results || results.length === 0) {
return html + `<div style="margin-top:10px;font-size:12px;color:var(--muted);font-family:var(--mono);">— No rows returned —</div>`;
}
const cols = Object.keys(results[0]);
let tbl = `<div class="result-table-wrap"><table>
<thead><tr>${cols.map(c => `<th>${escapeHtml(c)}</th>`).join('')}</tr></thead>
<tbody>`;
results.forEach(row => {
tbl += `<tr>${cols.map(c =>
`<td>${row[c] === null
? `<span style="color:var(--muted);font-style:italic;">null</span>`
: escapeHtml(String(row[c]))}</td>`
).join('')}</tr>`;
});
tbl += `</tbody></table></div>
<div class="result-count"><strong>${results.length.toLocaleString()}</strong> row${results.length !== 1 ? 's' : ''} returned</div>`;
return html + tbl;
}
window.copySql = function(btn) {
const code = btn.closest('.sql-block').querySelector('.sql-code').textContent;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = '✓ Copied'; btn.style.color = 'var(--success)';
setTimeout(() => { btn.textContent = 'Copy'; btn.style.color = ''; }, 1800);
});
};
// ── Message helpers ──
function addMsg(role, html) {
const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const div = document.createElement('div'); div.className = `msg ${role}`;
div.innerHTML = `<div class="msg-meta">${role === 'user' ? 'You' : 'QueryMind'} · ${now}</div><div class="bubble">${html}</div>`;
chat.appendChild(div); chat.scrollTop = chat.scrollHeight; return div;
}
let tc = 0;
function addThinking() {
const id = 'think-' + (++tc), div = document.createElement('div');
div.id = id; div.className = 'msg assistant';
div.innerHTML = `<div class="msg-meta">QueryMind · thinking</div>
<div class="thinking"><span></span><span></span><span></span></div>`;
chat.appendChild(div); chat.scrollTop = chat.scrollHeight; return id;
}
function removeThinking(id) { const el = document.getElementById(id); if (el) el.remove(); }
function clearChat() {
chat.innerHTML = ''; emptyState.style.display = '';
chat.appendChild(emptyState); showToast('Chat cleared', 'success');
}
function escapeHtml(s) {
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>