Spaces:
Sleeping
Sleeping
| <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 · 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| </script> | |
| </body> | |
| </html> |