minimew's picture
Upload src/app/giao_dien/text_to_sql_dashboard/code.html with huggingface_hub
74bb6a2 verified
Raw
History Blame Contribute Delete
48.3 kB
<!DOCTYPE html>
<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) &middot; SQLite / DB (multi-table) &middot; 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>