Spaces:
Sleeping
Sleeping
| <html class="light" lang="en"><head> | |
| <meta charset="utf-8"/> | |
| <meta content="width=device-width, initial-scale=1.0" name="viewport"/> | |
| <title>Text-to-SQL Demo | Research Prototype</title> | |
| <!-- Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Hanken+Grotesk:wght@600;700;900&family=JetBrains+Mono:wght@400;600&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@100..900&family=Inter:wght@100..900&display=swap" rel="stylesheet"/> | |
| <!-- Tailwind --> | |
| <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script> | |
| <!-- Chart.js -- dung cho bieu do bar/line thuc (thay cho mock div tinh ban dau) --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> | |
| <script id="tailwind-config"> | |
| tailwind.config = { | |
| darkMode: "class", | |
| theme: { | |
| extend: { | |
| "colors": { | |
| "on-tertiary-fixed": "#291800", | |
| "surface-container-lowest": "#ffffff", | |
| "tertiary": "#815600", | |
| "surface-tint": "#006494", | |
| "on-tertiary-fixed-variant": "#614000", | |
| "on-secondary-fixed-variant": "#43474b", | |
| "on-surface-variant": "#3f4850", | |
| "warning": "#D97706", | |
| "outline": "#6f7881", | |
| "on-secondary": "#ffffff", | |
| "primary": "#006494", | |
| "surface-dim": "#cbdbf5", | |
| "secondary-container": "#dce0e4", | |
| "primary-container": "#25a2e8", | |
| "error-container": "#ffdad6", | |
| "code-bg": "#F1F5F9", | |
| "tertiary-container": "#ce8c00", | |
| "surface-container-low": "#eff4ff", | |
| "inverse-primary": "#8ecdff", | |
| "secondary-fixed-dim": "#c3c7cb", | |
| "primary-fixed-dim": "#8ecdff", | |
| "success": "#28A745", | |
| "surface-bright": "#f8f9ff", | |
| "secondary-fixed": "#dfe3e7", | |
| "tertiary-fixed-dim": "#ffba48", | |
| "primary-fixed": "#cbe6ff", | |
| "background": "#f8f9ff", | |
| "surface-variant": "#d3e4fe", | |
| "border-subtle": "#E2E8F0", | |
| "on-primary-fixed": "#001e30", | |
| "secondary": "#5a5f62", | |
| "inverse-on-surface": "#eaf1ff", | |
| "error": "#ba1a1a", | |
| "on-secondary-container": "#5e6367", | |
| "on-secondary-fixed": "#171c1f", | |
| "on-primary-container": "#003551", | |
| "on-primary": "#ffffff", | |
| "outline-variant": "#bec7d2", | |
| "surface-container-highest": "#d3e4fe", | |
| "surface": "#f8f9ff", | |
| "on-error": "#ffffff", | |
| "on-tertiary-container": "#452d00", | |
| "inverse-surface": "#213145", | |
| "on-error-container": "#93000a", | |
| "on-background": "#0b1c30", | |
| "surface-container-high": "#dce9ff", | |
| "on-surface": "#0b1c30", | |
| "tertiary-fixed": "#ffddb1", | |
| "on-primary-fixed-variant": "#004b71", | |
| "on-tertiary": "#ffffff", | |
| "surface-container": "#e5eeff" | |
| }, | |
| "borderRadius": { | |
| "sm": "0.125rem", | |
| "DEFAULT": "0.25rem", | |
| "md": "0.375rem", | |
| "lg": "0.5rem", | |
| "xl": "0.75rem", | |
| "full": "9999px" | |
| }, | |
| "spacing": { | |
| "container-max": "1200px", | |
| "stack-md": "1rem", | |
| "margin-mobile": "1rem", | |
| "gutter": "1.5rem", | |
| "stack-sm": "0.5rem", | |
| "stack-lg": "2rem" | |
| }, | |
| "fontFamily": { | |
| "code-block": ["JetBrains Mono"], | |
| "headline-md": ["Hanken Grotesk"], | |
| "headline-lg": ["Hanken Grotesk"], | |
| "status-badge": ["JetBrains Mono"], | |
| "label-caps": ["JetBrains Mono"], | |
| "body-sm": ["Hanken Grotesk"], | |
| "body-md": ["Hanken Grotesk"] | |
| }, | |
| "fontSize": { | |
| "code-block": ["13px", {"lineHeight": "20px", "fontWeight": "400"}], | |
| "headline-md": ["18px", {"lineHeight": "24px", "fontWeight": "600"}], | |
| "headline-lg": ["24px", {"lineHeight": "32px", "fontWeight": "700"}], | |
| "status-badge": ["11px", {"lineHeight": "14px", "fontWeight": "600"}], | |
| "label-caps": ["11px", {"lineHeight": "16px", "letterSpacing": "0.05em", "fontWeight": "700"}], | |
| "body-sm": ["12px", {"lineHeight": "16px", "fontWeight": "400"}], | |
| "body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}] | |
| } | |
| }, | |
| }, | |
| } | |
| </script> | |
| <style> | |
| .material-symbols-outlined { | |
| font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; | |
| } | |
| body { font-family: 'Hanken Grotesk', 'Inter', sans-serif; background-color: #f8f9ff; } | |
| .glass-card { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); border: 1px solid #E2E8F0; box-shadow: 0 1px 2px 0 rgba(11, 28, 48, 0.04), 0 1px 8px 0 rgba(11, 28, 48, 0.04); } | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: #bec7d2; border-radius: 10px; } | |
| </style> | |
| </head> | |
| <body class="text-on-surface"> | |
| <!-- TopNavBar --> | |
| <header class="bg-surface sticky top-0 z-50 border-b border-border-subtle dark:border-outline h-16 shadow-sm"> | |
| <div class="flex justify-between items-center w-full px-gutter max-w-container-max mx-auto h-full"> | |
| <div class="flex items-center gap-8"> | |
| <span class="text-headline-lg font-headline-lg font-bold text-primary">Text-to-SQL Demo</span> | |
| <nav class="hidden md:flex gap-6 items-center h-full pt-1"> | |
| <a class="text-primary border-b-2 border-primary pb-1 font-label-caps text-label-caps uppercase" href="#">Research</a> | |
| <a class="text-secondary hover:text-primary transition-colors font-label-caps text-label-caps uppercase" href="#">Documentation</a> | |
| <a class="text-secondary hover:text-primary transition-colors font-label-caps text-label-caps uppercase" href="#">API</a> | |
| </nav> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <button class="bg-primary-container text-on-primary-container border border-primary/30 px-4 py-2 rounded font-label-caps text-label-caps flex items-center gap-2 hover:opacity-90 transition-opacity" id="new-query-btn"> | |
| <span class="material-symbols-outlined text-[18px]">add_circle</span> | |
| New Query | |
| </button> | |
| <button class="text-secondary hover:bg-surface-container-low p-2 rounded-full transition-all"> | |
| <span class="material-symbols-outlined" data-icon="settings">settings</span> | |
| </button> | |
| <button class="text-secondary hover:bg-surface-container-low p-2 rounded-full transition-all"> | |
| <span class="material-symbols-outlined" data-icon="help_outline">help_outline</span> | |
| </button> | |
| <div class="w-8 h-8 rounded-full overflow-hidden border border-outline-variant"> | |
| <img class="object-cover w-full h-full" data-alt="USTH logo" src="usth_logo.png"/> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- SideNavBar (Hidden on Mobile) -- decorative, khong wire API (ngoai scope demo) --> | |
| <aside class="hidden lg:flex fixed left-0 top-16 bottom-0 w-64 bg-on-background border-r border-black/20 flex-col p-stack-md z-40 overflow-y-auto"> | |
| <div class="mb-8 p-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-primary-container text-white flex items-center justify-center rounded-lg shadow-sm"> | |
| <span class="material-symbols-outlined">database</span> | |
| </div> | |
| <div> | |
| <h2 class="text-headline-md font-headline-md text-inverse-on-surface">SQL Engine</h2> | |
| <p class="text-body-sm font-body-sm text-inverse-on-surface/50">v2.4-stable</p> | |
| </div> | |
| </div> | |
| </div> | |
| <nav class="space-y-1"> | |
| <a class="flex items-center gap-3 px-4 py-3 bg-primary-container text-white rounded-lg shadow-sm" href="#"> | |
| <span class="material-symbols-outlined">view_quilt</span> | |
| <span class="font-body-md text-body-md">Query Builder</span> | |
| </a> | |
| <a class="flex items-center gap-3 px-4 py-3 text-inverse-on-surface/70 hover:bg-white/10 hover:text-inverse-on-surface rounded-lg transition-colors" href="#"> | |
| <span class="material-symbols-outlined">schema</span> | |
| <span class="font-body-md text-body-md">Schema Browser</span> | |
| </a> | |
| <a class="flex items-center gap-3 px-4 py-3 text-inverse-on-surface/70 hover:bg-white/10 hover:text-inverse-on-surface rounded-lg transition-colors" href="#"> | |
| <span class="material-symbols-outlined">history</span> | |
| <span class="font-body-md text-body-md">History</span> | |
| </a> | |
| <a class="flex items-center gap-3 px-4 py-3 text-inverse-on-surface/70 hover:bg-white/10 hover:text-inverse-on-surface rounded-lg transition-colors" href="#"> | |
| <span class="material-symbols-outlined">analytics</span> | |
| <span class="font-body-md text-body-md">Benchmarks</span> | |
| </a> | |
| </nav> | |
| <div class="mt-auto pt-8 border-t border-white/10 space-y-1"> | |
| <button class="w-full text-left flex items-center gap-3 px-4 py-3 text-inverse-on-surface/70 hover:bg-white/10 hover:text-inverse-on-surface rounded-lg transition-colors"> | |
| <span class="material-symbols-outlined">settings</span> | |
| <span class="font-body-md text-body-md">Settings</span> | |
| </button> | |
| <button class="w-full text-left flex items-center gap-3 px-4 py-3 text-inverse-on-surface/70 hover:bg-white/10 hover:text-inverse-on-surface rounded-lg transition-colors"> | |
| <span class="material-symbols-outlined">chat_bubble</span> | |
| <span class="font-body-md text-body-md">Feedback</span> | |
| </button> | |
| <button class="mt-4 w-full py-2 text-warning border border-warning/50 hover:bg-warning/10 transition-colors rounded font-label-caps text-label-caps uppercase" id="clear-history-btn"> | |
| Clear History | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main Content Area --> | |
| <main class="lg:ml-64 p-gutter max-w-container-max mx-auto min-h-[calc(100vh-4rem)] pb-24"> | |
| <div class="grid grid-cols-1 gap-stack-lg"> | |
| <!-- Section 1: Model Selection --> | |
| <section class="space-y-stack-sm"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-outlined text-secondary">memory</span> | |
| <h2 class="text-headline-md font-headline-md text-on-background">Model Selection</h2> | |
| <div class="h-[1px] flex-grow bg-border-subtle"></div> | |
| </div> | |
| <div class="glass-card p-4 rounded-xl flex flex-wrap items-center gap-3"> | |
| <span class="hidden"> | |
| <span id="backend-status-dot"></span> | |
| <span id="backend-status-badge"></span> | |
| </span> | |
| <select class="px-2 py-1 border border-border-subtle rounded text-status-badge font-status-badge bg-white text-on-surface uppercase" id="model-select" title="Choose which fine-tuned model to use"> | |
| </select> | |
| <span class="px-3 py-1 bg-primary text-on-primary border border-on-primary/30 rounded text-status-badge font-status-badge whitespace-nowrap" id="model-badge"> | |
| MODEL: LOADING... | |
| </span> | |
| </div> | |
| </section> | |
| <!-- Section 2: Data Schema --> | |
| <section class="space-y-stack-sm"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-outlined text-secondary">schema</span> | |
| <h2 class="text-headline-md font-headline-md text-on-background">Data Schema</h2> | |
| <div class="h-[1px] flex-grow bg-border-subtle"></div> | |
| </div> | |
| <div class="glass-card p-6 rounded-xl"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <div class="flex bg-surface-container rounded p-1 border border-outline-variant"> | |
| <button class="px-6 py-1.5 rounded bg-white shadow-sm text-primary font-label-caps text-label-caps uppercase transition-all" id="btn-sample" onclick="toggleDataSource('sample')">Sample Database</button> | |
| <button class="px-6 py-1.5 rounded text-secondary font-label-caps text-label-caps uppercase hover:text-on-surface transition-all" id="btn-upload" onclick="toggleDataSource('upload')">Upload File</button> | |
| </div> | |
| <button class="hidden text-secondary hover:text-primary flex items-center gap-1 font-label-caps text-label-caps uppercase" id="reset-btn"> | |
| <span class="material-symbols-outlined text-[16px]">refresh</span> Reset to sample | |
| </button> | |
| </div> | |
| <!-- Sample Mode Content --> | |
| <div class="space-y-4" id="source-sample"> | |
| <div class="flex items-center justify-between"> | |
| <h3 class="font-label-caps text-label-caps text-secondary uppercase" id="schema-title">Active Schema: ...</h3> | |
| <select class="px-2 py-1 border border-border-subtle rounded text-status-badge font-status-badge bg-white text-on-surface" id="sample-table-select" title="Select table to preview"></select> | |
| </div> | |
| <!-- Data preview table (same style as upload preview) --> | |
| <div id="sample-preview-wrap"> | |
| <div class="overflow-x-auto rounded border border-border-subtle"> | |
| <table class="w-full text-left font-body-sm text-body-sm" id="sample-preview-table"> | |
| <thead class="bg-surface-container-high border-b border-border-subtle font-code-block uppercase text-[11px]"><tr></tr></thead> | |
| <tbody class="divide-y divide-border-subtle bg-white"></tbody> | |
| </table> | |
| </div> | |
| <div class="flex items-center justify-between mt-3"> | |
| <button class="px-3 py-1 border border-border-subtle rounded text-body-sm font-label-caps text-label-caps uppercase bg-white hover:bg-surface-container-low disabled:opacity-40 disabled:cursor-not-allowed" disabled id="sample-prev-btn">‹ Previous</button> | |
| <span class="text-body-sm text-secondary" id="sample-page-label">Loading...</span> | |
| <button class="px-3 py-1 border border-border-subtle rounded text-body-sm font-label-caps text-label-caps uppercase bg-white hover:bg-surface-container-low disabled:opacity-40 disabled:cursor-not-allowed" disabled id="sample-next-btn">Next ›</button> | |
| </div> | |
| </div> | |
| <!-- Schema string (collapsible) --> | |
| <details class="mt-1"> | |
| <summary class="font-label-caps text-label-caps text-secondary uppercase cursor-pointer select-none hover:text-primary transition-colors text-[11px]">View schema string</summary> | |
| <div class="bg-code-bg border border-border-subtle p-4 rounded font-code-block text-code-block text-on-surface-variant overflow-x-auto mt-2"> | |
| <pre id="schema-block">Loading schema...</pre> | |
| </div> | |
| </details> | |
| </div> | |
| <!-- Upload Mode Content (Hidden by default) --> | |
| <div class="hidden space-y-4" id="source-upload"> | |
| <div class="border-2 border-dashed border-outline-variant rounded-xl p-8 flex flex-col items-center justify-center bg-surface-container-low hover:bg-surface-container hover:border-primary transition-all cursor-pointer" id="csv-dropzone"> | |
| <input accept=".csv,.sqlite,.db,.xlsx,.xls" class="hidden" id="csv-file-input" type="file"/> | |
| <span class="material-symbols-outlined text-4xl text-outline mb-3">upload_file</span> | |
| <p class="font-body-md text-body-md text-secondary">Drag and drop a file here or <span class="text-primary font-bold">browse files</span></p> | |
| <p class="text-[10px] text-outline mt-2 uppercase tracking-widest">CSV (single table) · SQLite / DB (multi-table) · Excel XLSX (each sheet = one table)</p> | |
| </div> | |
| <div class="mt-6 hidden" id="upload-preview-wrap"> | |
| <h3 class="font-label-caps text-label-caps text-secondary uppercase mb-3" id="upload-row-count">Data Preview</h3> | |
| <div class="overflow-x-auto rounded border border-border-subtle"> | |
| <table class="w-full text-left font-body-sm text-body-sm" id="upload-preview-table"> | |
| <thead class="bg-surface-container-high border-b border-border-subtle font-code-block uppercase text-[11px]"><tr></tr></thead> | |
| <tbody class="divide-y divide-border-subtle bg-white"></tbody> | |
| </table> | |
| </div> | |
| <div class="flex items-center justify-between mt-3"> | |
| <button class="px-3 py-1 border border-border-subtle rounded text-body-sm font-label-caps text-label-caps uppercase bg-white hover:bg-surface-container-low disabled:opacity-40 disabled:cursor-not-allowed" disabled="" id="preview-prev-btn">‹ Previous</button> | |
| <span class="text-body-sm text-secondary" id="preview-page-label"></span> | |
| <button class="px-3 py-1 border border-border-subtle rounded text-body-sm font-label-caps text-label-caps uppercase bg-white hover:bg-surface-container-low disabled:opacity-40 disabled:cursor-not-allowed" disabled="" id="preview-next-btn">Next ›</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Section 3: Query Input --> | |
| <section class="space-y-stack-sm"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-outlined text-secondary">edit_note</span> | |
| <h2 class="text-headline-md font-headline-md text-on-background">Query Input</h2> | |
| <div class="h-[1px] flex-grow bg-border-subtle"></div> | |
| </div> | |
| <div class="space-y-stack-md"> | |
| <div class="relative"> | |
| <textarea class="w-full bg-white border border-border-subtle rounded-xl p-5 text-headline-md font-body-md focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none shadow-sm" id="query-input" placeholder="Ask a question about the data in natural language..." rows="3"></textarea> | |
| <button class="absolute bottom-4 right-4 bg-primary text-on-primary px-6 py-2 rounded font-label-caps text-label-caps uppercase flex items-center gap-2 hover:opacity-90 active:scale-95 transition-all" id="submit-query"> | |
| Synthesize SQL <span class="material-symbols-outlined">bolt</span> | |
| </button> | |
| </div> | |
| <div class="flex flex-wrap items-center gap-2" id="suggestions-row"> | |
| <span class="text-body-sm font-label-caps text-secondary uppercase mr-2">Suggestions:</span> | |
| <div class="flex flex-wrap gap-2" id="example-chips"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Section 4: Synthesis Result --> | |
| <section class="space-y-stack-md hidden" id="result-area"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-outlined text-secondary">bolt</span> | |
| <h2 class="text-headline-md font-headline-md text-on-background">Synthesis Result</h2> | |
| <div class="h-[1px] flex-grow bg-border-subtle"></div> | |
| <span class="text-body-sm text-secondary" id="gen-time-label"></span> | |
| </div> | |
| <!-- Runtime metrics --> | |
| <div class="grid grid-cols-3 gap-3" id="runtime-row"> | |
| <div class="glass-card rounded-xl p-4 text-center"> | |
| <p class="font-label-caps text-label-caps text-secondary uppercase mb-1">SQL Generation</p> | |
| <p class="text-[28px] font-headline-lg text-primary leading-none" id="rt-gen-time">—</p> | |
| <p class="text-body-sm text-secondary mt-1">beam=4</p> | |
| </div> | |
| <div class="glass-card rounded-xl p-4 text-center"> | |
| <p class="font-label-caps text-label-caps text-secondary uppercase mb-1">Query Execution</p> | |
| <p class="text-[28px] font-headline-lg text-primary leading-none" id="rt-exec-time">—</p> | |
| <p class="text-body-sm text-secondary mt-1">SQLite</p> | |
| </div> | |
| <div class="glass-card rounded-xl p-4 text-center"> | |
| <p class="font-label-caps text-label-caps text-secondary uppercase mb-1">Total</p> | |
| <p class="text-[28px] font-headline-lg text-primary leading-none" id="rt-total-time">—</p> | |
| <p class="text-body-sm text-secondary mt-1">end to end</p> | |
| </div> | |
| </div> | |
| <!-- Warnings/Errors Container --> | |
| <div class="space-y-2" id="banners"></div> | |
| <div class="grid grid-cols-1 xl:grid-cols-3 gap-stack-lg"> | |
| <!-- SQL Code Block --> | |
| <div class="xl:col-span-2 space-y-stack-sm"> | |
| <div class="flex justify-between items-center px-2"> | |
| <span class="font-label-caps text-label-caps text-secondary uppercase">Generated SQLite</span> | |
| <button class="text-primary font-label-caps text-label-caps uppercase flex items-center gap-1 hover:underline" id="copy-sql-btn"> | |
| <span class="material-symbols-outlined text-sm">content_copy</span> Copy | |
| </button> | |
| </div> | |
| <div class="bg-code-bg border border-border-subtle rounded-lg p-6 relative group overflow-hidden"> | |
| <pre class="font-code-block text-code-block leading-relaxed" id="sql-block"></pre> | |
| </div> | |
| </div> | |
| <!-- Metric/Visualization --> | |
| <div class="space-y-stack-sm" id="viz-section"> | |
| <span class="font-label-caps text-label-caps text-secondary uppercase px-2">Visualization</span> | |
| <div class="bg-white border border-border-subtle rounded-lg p-6 h-[calc(100%-1.5rem)] flex flex-col items-center justify-center text-center" id="viz-container"></div> | |
| </div> | |
| </div> | |
| <!-- Execution Results Table --> | |
| <div class="space-y-stack-sm" id="results-section"> | |
| <span class="font-label-caps text-label-caps text-secondary uppercase px-2">Query Results</span> | |
| <div class="overflow-x-auto rounded border border-border-subtle bg-white"> | |
| <table class="w-full text-left font-body-sm text-body-md" id="results-table"> | |
| <thead class="bg-surface-container border-b border-border-subtle font-code-block uppercase text-[11px] text-secondary"><tr></tr></thead> | |
| <tbody class="divide-y divide-border-subtle"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- History Section --> | |
| <section class="mt-stack-lg border-t border-border-subtle pt-8"> | |
| <details class="group bg-surface-container-low rounded-xl overflow-hidden border border-border-subtle" open=""> | |
| <summary class="flex items-center justify-between p-4 cursor-pointer list-none hover:bg-surface-container transition-colors"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-outlined text-secondary">history</span> | |
| <span class="text-headline-md font-headline-md">Session History</span> | |
| </div> | |
| <span class="material-symbols-outlined transform group-open:rotate-180 transition-transform">expand_more</span> | |
| </summary> | |
| <div class="p-4 space-y-3" id="history-list"> | |
| <p class="text-body-sm text-secondary px-2" id="history-empty">No questions asked yet this session.</p> | |
| </div> | |
| </details> | |
| </section> | |
| </div> | |
| </main> | |
| <!-- Fixed Footer Disclaimer --> | |
| <footer class="fixed bottom-0 left-0 right-0 bg-inverse-surface text-inverse-on-surface z-50 lg:ml-64"> | |
| <div class="px-6 py-2 flex items-center justify-center gap-3"> | |
| <span class="material-symbols-outlined text-[20px] text-warning">warning</span> | |
| <p class="text-body-sm font-body-sm"> | |
| Research prototype — generated SQL may occasionally be incorrect. Always verify before trusting results. | |
| </p> | |
| </div> | |
| </footer> | |
| <script> | |
| // ---- Config ---- | |
| // Relative URL when served through FastAPI (localhost or HF Spaces). | |
| // Falls back to localhost:8000 when opened directly as a local file. | |
| const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:8000' : ''; | |
| const queryInput = document.getElementById('query-input'); | |
| const submitBtn = document.getElementById('submit-query'); | |
| let chartInstance = null; | |
| let history = []; | |
| function escapeHtml(value) { | |
| const div = document.createElement('div'); | |
| div.textContent = value === null || value === undefined ? '' : String(value); | |
| return div.innerHTML; | |
| } | |
| // ---- Data source tab toggle (sample DB <-> upload CSV) ---- | |
| function toggleDataSource(mode) { | |
| const btnSample = document.getElementById('btn-sample'); | |
| const btnUpload = document.getElementById('btn-upload'); | |
| const sourceSample = document.getElementById('source-sample'); | |
| const sourceUpload = document.getElementById('source-upload'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| if (mode === 'sample') { | |
| btnSample.classList.add('bg-white', 'shadow-sm', 'text-primary'); | |
| btnSample.classList.remove('text-secondary'); | |
| btnUpload.classList.remove('bg-white', 'shadow-sm', 'text-primary'); | |
| btnUpload.classList.add('text-secondary'); | |
| sourceSample.classList.remove('hidden'); | |
| sourceUpload.classList.add('hidden'); | |
| resetBtn.classList.add('hidden'); | |
| } else { | |
| btnUpload.classList.add('bg-white', 'shadow-sm', 'text-primary'); | |
| btnUpload.classList.remove('text-secondary'); | |
| btnSample.classList.remove('bg-white', 'shadow-sm', 'text-primary'); | |
| btnSample.classList.add('text-secondary'); | |
| sourceSample.classList.add('hidden'); | |
| sourceUpload.classList.remove('hidden'); | |
| resetBtn.classList.remove('hidden'); | |
| } | |
| } | |
| // ---- Backend status badge ---- | |
| function setBackendStatus(ok) { | |
| document.getElementById('backend-status-badge').textContent = ok ? 'BACKEND: CONNECTED' : 'BACKEND: OFFLINE'; | |
| document.getElementById('backend-status-dot').className = | |
| `w-2 h-2 rounded-full ${ok ? 'bg-success animate-pulse' : 'bg-error'}`; | |
| } | |
| // ---- GET /api/meta -- schema, model badge, example chips ---- | |
| async function loadMeta() { | |
| try { | |
| const res = await fetch(`${API_BASE}/api/meta`); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const meta = await res.json(); | |
| setBackendStatus(true); | |
| renderMeta(meta); | |
| } catch (err) { | |
| setBackendStatus(false); | |
| console.error('Failed to load /api/meta:', err); | |
| } | |
| } | |
| function renderMeta(meta) { | |
| document.getElementById('model-badge').textContent = `MODEL: ${meta.model}`; | |
| document.getElementById('schema-title').textContent = `Active Schema: ${meta.db_id}`; | |
| document.getElementById('schema-block').textContent = meta.schema; | |
| const chipContainer = document.getElementById('example-chips'); | |
| const suggestionsRow = document.getElementById('suggestions-row'); | |
| chipContainer.innerHTML = ''; | |
| if (meta.examples && meta.examples.length > 0) { | |
| suggestionsRow.classList.remove('hidden'); | |
| meta.examples.forEach((q) => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'px-4 py-1.5 border border-border-subtle rounded-full bg-white hover:bg-primary-container hover:text-on-primary-container transition-colors text-body-sm font-body-sm'; | |
| btn.textContent = `"${q}"`; | |
| btn.addEventListener('click', () => { queryInput.value = q; }); | |
| chipContainer.appendChild(btn); | |
| }); | |
| } else { | |
| suggestionsRow.classList.add('hidden'); | |
| } | |
| toggleDataSource(meta.mode === 'uploaded' ? 'upload' : 'sample'); | |
| if (meta.mode !== 'uploaded') loadSamplePreview(); | |
| loadModels(); | |
| } | |
| // ---- GET /api/models -- populate model-select dropdown with full 12-checkpoint catalog ---- | |
| async function loadModels() { | |
| try { | |
| const res = await fetch(`${API_BASE}/api/models`); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| const select = document.getElementById('model-select'); | |
| select.innerHTML = data.options.map((opt) => | |
| `<option value="${opt.model_id}">${escapeHtml(opt.label)}</option>` | |
| ).join(''); | |
| select.value = data.current_model_id; | |
| } catch (err) { | |
| console.error('Failed to load /api/models:', err); | |
| } | |
| } | |
| document.getElementById('model-select').addEventListener('change', async (e) => { | |
| try { | |
| await fetch(`${API_BASE}/api/model`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model_id: e.target.value }), | |
| }); | |
| await loadMeta(); | |
| } catch (err) { | |
| console.error('Failed to switch model:', err); | |
| } | |
| }); | |
| // ---- File upload (CSV / SQLite / Excel) ---- | |
| const dropzone = document.getElementById('csv-dropzone'); | |
| const fileInput = document.getElementById('csv-file-input'); | |
| dropzone.addEventListener('click', () => fileInput.click()); | |
| dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('border-primary'); }); | |
| dropzone.addEventListener('dragleave', () => dropzone.classList.remove('border-primary')); | |
| dropzone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('border-primary'); | |
| if (e.dataTransfer.files.length) handleUpload(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length) handleUpload(e.target.files[0]); | |
| }); | |
| async function handleUpload(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', body: formData }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| initUploadPreview(data); | |
| await loadMeta(); | |
| } catch (err) { | |
| console.error('Upload failed:', err); | |
| alert(`Upload failed: ${err.message}`); | |
| } | |
| } | |
| // ---- Upload preview pagination (client-side over data.rows, da gioi han MAX_PREVIEW_ROWS o backend) ---- | |
| const PREVIEW_PAGE_SIZE = 5; | |
| let previewState = { columns: [], rows: [], rowCount: 0, truncated: false, page: 0 }; | |
| function initUploadPreview(data) { | |
| previewState = { columns: data.columns, rows: data.rows, rowCount: data.row_count, truncated: data.truncated, page: 0 }; | |
| document.getElementById('upload-preview-wrap').classList.remove('hidden'); | |
| const theadRow = document.querySelector('#upload-preview-table thead tr'); | |
| theadRow.innerHTML = previewState.columns.map((c) => `<th class="px-4 py-2 border-r border-border-subtle">${escapeHtml(c)}</th>`).join(''); | |
| renderPreviewPage(); | |
| } | |
| function renderPreviewPage() { | |
| const { columns, rows, rowCount, truncated, page } = previewState; | |
| const totalPages = Math.max(1, Math.ceil(rows.length / PREVIEW_PAGE_SIZE)); | |
| const start = page * PREVIEW_PAGE_SIZE; | |
| const pageRows = rows.slice(start, start + PREVIEW_PAGE_SIZE); | |
| const tbody = document.querySelector('#upload-preview-table tbody'); | |
| tbody.innerHTML = pageRows.map((row) => | |
| `<tr class="hover:bg-surface-container-low">${row.map((v) => `<td class="px-4 py-2 border-r border-border-subtle">${escapeHtml(v)}</td>`).join('')}</tr>` | |
| ).join(''); | |
| const truncatedNote = truncated ? ` (loaded first ${rows.length} of ${rowCount})` : ''; | |
| document.getElementById('upload-row-count').textContent = | |
| `Data Preview — ${rowCount} rows total${truncatedNote}`; | |
| document.getElementById('preview-page-label').textContent = | |
| `Rows ${start + 1}-${start + pageRows.length} of ${rows.length} loaded (page ${page + 1} / ${totalPages})`; | |
| document.getElementById('preview-prev-btn').disabled = page <= 0; | |
| document.getElementById('preview-next-btn').disabled = page >= totalPages - 1; | |
| } | |
| document.getElementById('preview-prev-btn').addEventListener('click', () => { | |
| if (previewState.page > 0) { previewState.page -= 1; renderPreviewPage(); } | |
| }); | |
| document.getElementById('preview-next-btn').addEventListener('click', () => { | |
| const totalPages = Math.max(1, Math.ceil(previewState.rows.length / PREVIEW_PAGE_SIZE)); | |
| if (previewState.page < totalPages - 1) { previewState.page += 1; renderPreviewPage(); } | |
| }); | |
| // ---- Reset to sample DB ---- | |
| document.getElementById('reset-btn').addEventListener('click', async () => { | |
| try { | |
| await fetch(`${API_BASE}/api/reset`, { method: 'POST' }); | |
| toggleDataSource('sample'); | |
| await loadMeta(); | |
| } catch (err) { | |
| console.error('Reset failed:', err); | |
| } | |
| }); | |
| // ---- Sample database table preview ---- | |
| let samplePreviewState = { columns: [], rows: [], rowCount: 0, tableNames: [], activeTable: '', page: 0 }; | |
| async function loadSamplePreview(table) { | |
| try { | |
| const url = `${API_BASE}/api/preview` + (table ? `?table=${encodeURIComponent(table)}` : ''); | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| samplePreviewState = { columns: data.columns, rows: data.rows, rowCount: data.row_count, | |
| truncated: data.truncated, tableNames: data.table_names, activeTable: data.active_table, page: 0 }; | |
| const sel = document.getElementById('sample-table-select'); | |
| sel.innerHTML = samplePreviewState.tableNames.map(t => | |
| `<option value="${t}" ${t === data.active_table ? 'selected' : ''}>${t}</option>` | |
| ).join(''); | |
| renderSamplePreviewPage(); | |
| } catch (err) { | |
| console.error('Failed to load sample preview:', err); | |
| } | |
| } | |
| function renderSamplePreviewPage() { | |
| const { columns, rows, rowCount, page } = samplePreviewState; | |
| const totalPages = Math.max(1, Math.ceil(rows.length / PREVIEW_PAGE_SIZE)); | |
| const start = page * PREVIEW_PAGE_SIZE; | |
| const pageRows = rows.slice(start, start + PREVIEW_PAGE_SIZE); | |
| const thead = document.querySelector('#sample-preview-table thead tr'); | |
| thead.innerHTML = columns.map(c => `<th class="px-4 py-2 border-r border-border-subtle">${escapeHtml(String(c))}</th>`).join(''); | |
| const tbody = document.querySelector('#sample-preview-table tbody'); | |
| tbody.innerHTML = pageRows.map(row => | |
| `<tr class="hover:bg-surface-container-low">${row.map(v => | |
| `<td class="px-4 py-2 border-r border-border-subtle">${escapeHtml(v === null ? '' : String(v))}</td>` | |
| ).join('')}</tr>` | |
| ).join(''); | |
| document.getElementById('sample-page-label').textContent = | |
| `Rows ${start + 1}–${start + pageRows.length} of ${rowCount} total (page ${page + 1} / ${totalPages})`; | |
| document.getElementById('sample-prev-btn').disabled = page <= 0; | |
| document.getElementById('sample-next-btn').disabled = page >= totalPages - 1; | |
| } | |
| document.getElementById('sample-prev-btn').addEventListener('click', () => { | |
| if (samplePreviewState.page > 0) { samplePreviewState.page -= 1; renderSamplePreviewPage(); } | |
| }); | |
| document.getElementById('sample-next-btn').addEventListener('click', () => { | |
| const totalPages = Math.max(1, Math.ceil(samplePreviewState.rows.length / PREVIEW_PAGE_SIZE)); | |
| if (samplePreviewState.page < totalPages - 1) { samplePreviewState.page += 1; renderSamplePreviewPage(); } | |
| }); | |
| document.getElementById('sample-table-select').addEventListener('change', (e) => { | |
| loadSamplePreview(e.target.value); | |
| }); | |
| // ---- Banners (validator block / execution error) ---- | |
| function renderBanners(data) { | |
| const container = document.getElementById('banners'); | |
| container.innerHTML = ''; | |
| if (!data.is_safe) { | |
| container.appendChild(makeBanner('error', 'Blocked', data.validator_message)); | |
| } else if (data.exec_error) { | |
| container.appendChild(makeBanner( | |
| 'warning', | |
| 'Execution Warning', | |
| `Generated SQL failed to execute: ${data.exec_error}. This can happen with complex multi-table questions -- try rephrasing or simplifying your question.` | |
| )); | |
| } | |
| } | |
| function makeBanner(type, title, message) { | |
| const isError = type === 'error'; | |
| const div = document.createElement('div'); | |
| div.className = `${isError ? 'bg-error/10 border-error' : 'bg-warning/10 border-warning'} border-l-4 p-4 rounded flex items-start gap-3`; | |
| const icon = document.createElement('span'); | |
| icon.className = `material-symbols-outlined ${isError ? 'text-error' : 'text-warning'}`; | |
| icon.textContent = isError ? 'block' : 'warning'; | |
| const textWrap = document.createElement('div'); | |
| const p1 = document.createElement('p'); | |
| p1.className = `text-body-md font-bold ${isError ? 'text-error' : 'text-warning'}`; | |
| p1.textContent = title; | |
| const p2 = document.createElement('p'); | |
| p2.className = 'text-body-sm text-on-surface-variant'; | |
| p2.textContent = message; | |
| textWrap.appendChild(p1); | |
| textWrap.appendChild(p2); | |
| div.appendChild(icon); | |
| div.appendChild(textWrap); | |
| return div; | |
| } | |
| // Tim cot SO dau tien (bo qua cot 0 -- la nhan/truc x) de ve chart -- | |
| // khong the gia dinh cot index 1 luon la gia tri (vd cau hoi 3 cot | |
| // name/country/age: country o index 1 la text, age moi la cot so). | |
| function findNumericValueColumnIndex(data) { | |
| for (let i = 1; i < data.columns.length; i++) { | |
| const allNumeric = data.rows.every((r) => typeof r[i] === 'number' || r[i] === null); | |
| if (allNumeric) return i; | |
| } | |
| return 1; | |
| } | |
| // ---- Visualization (metric / bar / line / none) ---- | |
| function renderViz(data) { | |
| const container = document.getElementById('viz-container'); | |
| container.innerHTML = ''; | |
| if (chartInstance) { chartInstance.destroy(); chartInstance = null; } | |
| if (data.chart_type === 'metric') { | |
| const label = document.createElement('p'); | |
| label.className = 'text-body-sm font-label-caps text-secondary uppercase mb-2'; | |
| label.textContent = data.columns[0] || 'Result'; | |
| const value = document.createElement('p'); | |
| value.className = 'text-[48px] font-headline-lg text-primary leading-none'; | |
| value.textContent = (data.rows[0] && data.rows[0][0] !== null && data.rows[0][0] !== undefined) ? data.rows[0][0] : '—'; | |
| container.appendChild(label); | |
| container.appendChild(value); | |
| } else if (data.chart_type === 'bar' || data.chart_type === 'line') { | |
| const canvas = document.createElement('canvas'); | |
| container.appendChild(canvas); | |
| const valueIdx = findNumericValueColumnIndex(data); | |
| const labels = data.rows.map((r) => r[0]); | |
| const values = data.rows.map((r) => r[valueIdx]); | |
| chartInstance = new Chart(canvas, { | |
| type: data.chart_type, | |
| data: { | |
| labels, | |
| datasets: [{ | |
| label: data.columns[valueIdx] || 'Value', | |
| data: values, | |
| backgroundColor: 'rgba(37, 162, 232, 0.5)', | |
| borderColor: '#006494', | |
| borderWidth: 2, | |
| borderRadius: data.chart_type === 'bar' ? 4 : 0, | |
| }], | |
| }, | |
| options: { responsive: true, plugins: { legend: { display: false } } }, | |
| }); | |
| } else { | |
| const p = document.createElement('p'); | |
| p.className = 'text-body-sm text-secondary'; | |
| p.textContent = 'No chart for this result type — see the table below.'; | |
| container.appendChild(p); | |
| } | |
| } | |
| // ---- Results table ---- | |
| function renderTable(data) { | |
| const thead = document.querySelector('#results-table thead tr'); | |
| const tbody = document.querySelector('#results-table tbody'); | |
| thead.innerHTML = data.columns.map((c) => `<th class="px-6 py-4 border-r border-border-subtle">${escapeHtml(c)}</th>`).join(''); | |
| tbody.innerHTML = data.rows.map((row) => | |
| `<tr class="hover:bg-surface-container-low transition-colors">${row.map((v) => | |
| `<td class="px-6 py-3 border-r border-border-subtle font-code-block">${v === null ? 'NULL' : escapeHtml(v)}</td>` | |
| ).join('')}</tr>` | |
| ).join(''); | |
| } | |
| // ---- Orchestrate full result render ---- | |
| function renderResult(data) { | |
| document.getElementById('result-area').classList.remove('hidden'); | |
| document.getElementById('sql-block').textContent = data.sql || '(empty)'; | |
| document.getElementById('gen-time-label').textContent = `${data.gen_time.toFixed(1)}s`; | |
| // Runtime cards | |
| const hasExec = data.is_safe && typeof data.exec_time === 'number'; | |
| document.getElementById('rt-gen-time').textContent = `${data.gen_time.toFixed(2)}s`; | |
| document.getElementById('rt-exec-time').textContent = hasExec ? `${data.exec_time.toFixed(3)}s` : '—'; | |
| document.getElementById('rt-total-time').textContent = hasExec | |
| ? `${(data.gen_time + data.exec_time).toFixed(2)}s` | |
| : `${data.gen_time.toFixed(2)}s`; | |
| renderBanners(data); | |
| const showData = data.is_safe && !data.exec_error; | |
| document.getElementById('viz-section').classList.toggle('hidden', !showData); | |
| document.getElementById('results-section').classList.toggle('hidden', !showData); | |
| if (showData) { | |
| renderViz(data); | |
| renderTable(data); | |
| } | |
| } | |
| // ---- Session history (frontend-only state, no API) ---- | |
| function addHistory(data) { | |
| const success = data.is_safe && !data.exec_error; | |
| history.unshift({ question: data.question, sql: data.sql, time: Date.now(), success }); | |
| renderHistory(); | |
| } | |
| function timeAgo(ts) { | |
| const sec = Math.floor((Date.now() - ts) / 1000); | |
| if (sec < 60) return 'just now'; | |
| if (sec < 3600) return `${Math.floor(sec / 60)} minute${Math.floor(sec / 60) === 1 ? '' : 's'} ago`; | |
| return `${Math.floor(sec / 3600)} hour${Math.floor(sec / 3600) === 1 ? '' : 's'} ago`; | |
| } | |
| function renderHistory() { | |
| const container = document.getElementById('history-list'); | |
| if (history.length === 0) { | |
| container.innerHTML = '<p class="text-body-sm text-secondary px-2" id="history-empty">No questions asked yet this session.</p>'; | |
| return; | |
| } | |
| container.innerHTML = history.map((h, i) => ` | |
| <div class="bg-white p-3 rounded border border-border-subtle flex items-center justify-between ${h.success ? '' : 'opacity-70'}"> | |
| <div class="flex flex-col"> | |
| <span class="text-body-md font-body-md">"${escapeHtml(h.question)}"</span> | |
| <span class="text-[10px] font-code-block ${h.success ? 'text-secondary' : 'text-error'} uppercase mt-1">${timeAgo(h.time)} • ${h.success ? 'Success' : 'Error'}</span> | |
| </div> | |
| <button class="text-primary hover:bg-primary-container p-2 rounded transition-all" data-history-index="${i}"> | |
| <span class="material-symbols-outlined">restore</span> | |
| </button> | |
| </div> | |
| `).join(''); | |
| container.querySelectorAll('button[data-history-index]').forEach((btn) => { | |
| btn.addEventListener('click', () => { | |
| const item = history[Number(btn.dataset.historyIndex)]; | |
| queryInput.value = item.question; | |
| queryInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }); | |
| }); | |
| } | |
| document.getElementById('clear-history-btn').addEventListener('click', () => { | |
| history = []; | |
| renderHistory(); | |
| }); | |
| // ---- Submit question -> POST /api/query ---- | |
| async function submitQuery() { | |
| const question = queryInput.value.trim(); | |
| if (!question) { | |
| queryInput.classList.add('border-error'); | |
| setTimeout(() => queryInput.classList.remove('border-error'), 500); | |
| return; | |
| } | |
| submitBtn.innerHTML = 'Synthesizing... <span class="animate-spin material-symbols-outlined">sync</span>'; | |
| submitBtn.disabled = true; | |
| try { | |
| const res = await fetch(`${API_BASE}/api/query`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question }), | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| renderResult(data); | |
| addHistory(data); | |
| document.getElementById('result-area').scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } catch (err) { | |
| console.error('Query failed:', err); | |
| document.getElementById('result-area').classList.remove('hidden'); | |
| document.getElementById('sql-block').textContent = ''; | |
| document.getElementById('viz-section').classList.add('hidden'); | |
| document.getElementById('results-section').classList.add('hidden'); | |
| const container = document.getElementById('banners'); | |
| container.innerHTML = ''; | |
| container.appendChild(makeBanner('error', 'Network Error', `Could not reach backend at ${API_BASE}: ${err.message}`)); | |
| } finally { | |
| submitBtn.innerHTML = 'Synthesize SQL <span class="material-symbols-outlined">bolt</span>'; | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| submitBtn.addEventListener('click', submitQuery); | |
| queryInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) submitQuery(); | |
| }); | |
| document.getElementById('copy-sql-btn').addEventListener('click', () => { | |
| navigator.clipboard.writeText(document.getElementById('sql-block').textContent || ''); | |
| }); | |
| document.getElementById('new-query-btn').addEventListener('click', () => { | |
| queryInput.value = ''; | |
| document.getElementById('result-area').classList.add('hidden'); | |
| queryInput.focus(); | |
| }); | |
| // ---- Init ---- | |
| // Reset to default database on every page load so stale server-side | |
| // state (e.g. from a previous CSV upload on HF Spaces) does not persist. | |
| fetch(`${API_BASE}/api/reset`, { method: 'POST' }).finally(() => loadMeta()); | |
| </script> | |
| </body></html> | |