LalitChaudhari3 commited on
Commit
b5ffc55
·
verified ·
1 Parent(s): f6b402d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +428 -430
index.html CHANGED
@@ -1,430 +1,428 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PlainSQL | Enterprise Data Assistant</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
- <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
-
14
- <script>
15
- tailwind.config = {
16
- theme: {
17
- extend: {
18
- fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
19
- colors: {
20
- dark: { 900: '#0B0C10', 800: '#15171E', 700: '#1F222E' },
21
- brand: { 500: '#38bdf8', 400: '#0ea5e9' }
22
- }
23
- }
24
- }
25
- }
26
- </script>
27
- <style>
28
- body { background-color: #0B0C10; color: #E2E8F0; }
29
- .scrollbar-custom::-webkit-scrollbar { width: 8px; }
30
- .scrollbar-custom::-webkit-scrollbar-track { background: #15171E; }
31
- .scrollbar-custom::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
32
-
33
- .bubble-user { background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%); color: white; border-radius: 16px 16px 4px 16px; }
34
- .bubble-ai { background: #1F222E; border: 1px solid #2D3142; border-radius: 16px 16px 16px 4px; }
35
-
36
- .custom-table th { background: #262A3B; color: #94A3B8; font-size: 0.75rem; padding: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
37
- .custom-table td { border-bottom: 1px solid #1F222E; color: #CBD5E1; padding: 12px; font-size: 0.9rem; }
38
- .custom-table tr:last-child td { border-bottom: none; }
39
-
40
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
41
- .animate-fade { animation: fadeIn 0.3s ease-out forwards; }
42
- .typing-cursor::after { content: '▋'; animation: blink 1s step-start infinite; color: #38bdf8; margin-left: 2px; }
43
- @keyframes blink { 50% { opacity: 0; } }
44
-
45
- .scrollbar-hide::-webkit-scrollbar { display: none; }
46
- .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
47
- </style>
48
- </head>
49
- <body class="flex h-screen overflow-hidden selection:bg-brand-500 selection:text-white relative font-sans">
50
-
51
- <audio id="sound-welcome" src="https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3" preload="auto"></audio>
52
- <audio id="sound-message" src="https://assets.mixkit.co/active_storage/sfx/2346/2346-preview.mp3" preload="auto"></audio>
53
-
54
- <div id="splash-screen" class="fixed inset-0 z-[100] bg-dark-900 flex flex-col items-center justify-center transition-opacity duration-700">
55
- <div class="text-center space-y-6 animate-fade">
56
- <div class="w-20 h-20 bg-brand-500 rounded-2xl flex items-center justify-center shadow-2xl shadow-brand-500/50 mx-auto mb-6">
57
- <svg class="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.58 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.58 4 8 4s8-1.79 8-4M4 7c0-2.21 3.58-4 8-4s8 1.79 8 4m0 5c0 2.21-3.58 4-8 4s-8-1.79-8-4"/></svg>
58
- </div>
59
- <h1 class="text-4xl font-bold text-white tracking-tight">Plain<span class="text-brand-400">SQL</span></h1>
60
- <p class="text-gray-400 text-sm">Enterprise Text-to-SQL Engine</p>
61
- <button onclick="enterSystem()" class="mt-8 px-8 py-3 bg-white text-dark-900 font-bold rounded-full hover:bg-gray-100 transition-all shadow-lg hover:scale-105 transform">
62
- Initialize System
63
- </button>
64
- </div>
65
- </div>
66
-
67
- <div id="chart-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm hidden opacity-0 transition-opacity duration-300">
68
- <div class="bg-dark-800 border border-dark-700 w-full max-w-4xl h-[600px] rounded-2xl shadow-2xl flex flex-col p-6 transform scale-95 transition-transform duration-300" id="chart-content">
69
- <div class="flex justify-between items-center mb-4">
70
- <h3 class="text-lg font-semibold text-white">Data Visualization</h3>
71
- <button onclick="closeChart()" class="text-gray-500 hover:text-white p-2 text-xl">✕</button>
72
- </div>
73
- <div class="flex-1 relative w-full h-full"><canvas id="myChart"></canvas></div>
74
- </div>
75
- </div>
76
-
77
- <aside class="w-64 bg-dark-800 border-r border-dark-700 flex flex-col hidden md:flex">
78
- <div class="p-6 flex items-center gap-3">
79
- <div class="w-8 h-8 bg-brand-500 rounded-lg flex items-center justify-center shadow-lg">
80
- <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.58 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.58 4 8 4s8-1.79 8-4M4 7c0-2.21 3.58-4 8-4s8 1.79 8 4m0 5c0 2.21-3.58 4-8 4s-8-1.79-8-4"/></svg>
81
- </div>
82
- <span class="font-bold text-xl tracking-tight">PlainSQL</span>
83
- </div>
84
- <nav class="flex-1 px-4 space-y-2 overflow-y-auto scrollbar-custom">
85
- <button onclick="window.location.reload()" class="w-full flex items-center gap-3 px-4 py-3 bg-brand-500/10 text-brand-400 rounded-xl border border-brand-500/20 hover:bg-brand-500/20 transition-all group">
86
- <svg class="w-5 h-5 group-hover:rotate-90 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
87
- <span class="text-sm font-medium">New Analysis</span>
88
- </button>
89
- <div class="mt-8">
90
- <p class="px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent Queries</p>
91
- <div id="history-list" class="space-y-1"></div>
92
- </div>
93
- </nav>
94
- <div class="p-4 border-t border-dark-700">
95
- <div class="flex items-center gap-3 px-3 py-2 bg-green-500/10 rounded-lg border border-green-500/20">
96
- <div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
97
- <div class="flex flex-col">
98
- <span class="text-[11px] font-bold text-green-400 uppercase">System Online</span>
99
- <span class="text-[10px] text-gray-500">Read-Only Mode</span>
100
- </div>
101
- </div>
102
- </div>
103
- </aside>
104
-
105
- <main class="flex-1 flex flex-col relative bg-dark-900">
106
- <div id="chat-box" class="flex-1 overflow-y-auto p-4 md:p-8 space-y-6 pb-80 scroll-smooth scrollbar-custom">
107
- <div class="flex gap-4 max-w-3xl mx-auto animate-fade">
108
- <div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center flex-shrink-0 border border-brand-500/30 text-brand-400">
109
- <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
110
- </div>
111
- <div class="bubble-ai p-5 shadow-sm">
112
- <p class="text-sm leading-relaxed text-gray-200">
113
- Hello. I am <strong>PlainSQL</strong>. I can access your database securely to fetch real-time insights.<br><br>
114
- <em>Try asking: "Show me top 5 employees by salary" or "List all active users."</em>
115
- </p>
116
- </div>
117
- </div>
118
- </div>
119
-
120
- <div class="absolute bottom-0 w-full p-4 md:p-6 bg-gradient-to-t from-dark-900 via-dark-900 to-transparent z-20">
121
- <div class="max-w-3xl mx-auto">
122
- <div id="suggestions" class="flex gap-2 mb-3 overflow-x-auto pb-1 scrollbar-hide"></div>
123
- <form id="chat-form" class="relative group">
124
- <div class="absolute inset-0 bg-brand-500/20 rounded-2xl blur-lg group-hover:bg-brand-500/30 transition-all opacity-0 group-hover:opacity-100"></div>
125
- <input type="text" id="user-input"
126
- class="relative w-full bg-dark-800 text-white border border-dark-700 rounded-2xl py-4 pl-5 pr-14 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all placeholder-gray-500 shadow-xl"
127
- placeholder="Ask a question in plain English..." autocomplete="off">
128
- <button type="submit" class="absolute right-2 top-2 p-2 bg-brand-500 hover:bg-brand-400 text-white rounded-xl transition-all shadow-lg active:scale-95">
129
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
130
- </button>
131
- </form>
132
- <div class="text-center mt-2">
133
- <p class="text-[10px] text-gray-600">AI Generated SQL can be inaccurate. Always verify important data.</p>
134
- </div>
135
- </div>
136
- </div>
137
- </main>
138
-
139
- <script>
140
- const API_URL = "http://127.0.0.1:8000/chat";
141
-
142
- const form = document.getElementById('chat-form');
143
- const input = document.getElementById('user-input');
144
- const chatBox = document.getElementById('chat-box');
145
- const suggestionsBox = document.getElementById('suggestions');
146
- const historyList = document.getElementById('history-list');
147
- const soundWelcome = document.getElementById('sound-welcome');
148
- const soundMessage = document.getElementById('sound-message');
149
- const splashScreen = document.getElementById('splash-screen');
150
- const modal = document.getElementById('chart-modal');
151
- const modalContent = document.getElementById('chart-content');
152
-
153
- let chartInstance = null;
154
- let conversationHistory = [];
155
-
156
- function unlockAudio() {
157
- soundMessage.volume = 0;
158
- soundMessage.play().then(() => {
159
- soundMessage.pause();
160
- soundMessage.currentTime = 0;
161
- }).catch(() => {});
162
- }
163
-
164
- function playIncomingSound() {
165
- soundMessage.volume = 0.4;
166
- soundMessage.currentTime = 0;
167
- soundMessage.play().catch(e => console.warn("Audio blocked:", e));
168
- }
169
-
170
- function enterSystem() {
171
- soundWelcome.volume = 0.5;
172
- soundWelcome.play().catch(e => console.log("Init Audio Error:", e));
173
- splashScreen.style.opacity = '0';
174
- setTimeout(() => { splashScreen.style.display = 'none'; }, 700);
175
- setTimeout(() => input.focus(), 800);
176
- }
177
-
178
- function checkGreeting(text) {
179
- const t = text.toLowerCase();
180
- const greetings = ['hello', 'hi', 'hey', 'good morning', 'good afternoon', 'hola'];
181
-
182
- if (greetings.some(g => t === g || t.startsWith(g + ' '))) {
183
- return "Hello! 👋 I am **PlainSQL**, your data assistant. I'm here to help you query your database without writing code.<br><br>You can ask me things like: <em>'Show me top 5 employees by salary'</em> or <em>'List all active users.'</em>";
184
- }
185
- return null;
186
- }
187
-
188
- // FIX 2: IMPROVED SCROLL FUNCTION
189
- // Uses setTimeout to ensure DOM is fully rendered before scrolling
190
- function scrollToBottom() {
191
- setTimeout(() => {
192
- chatBox.scrollTop = chatBox.scrollHeight;
193
- }, 50);
194
- }
195
-
196
- async function typeText(element, text) {
197
- element.classList.add('typing-cursor');
198
- return new Promise(resolve => {
199
- setTimeout(() => {
200
- element.innerHTML = text;
201
- element.classList.remove('typing-cursor');
202
- resolve();
203
- }, 300 + Math.random() * 500);
204
- });
205
- }
206
-
207
- function showLoading() {
208
- const id = 'loading-' + Date.now();
209
- const html = `
210
- <div id="${id}" class="flex gap-4 max-w-3xl mx-auto animate-fade">
211
- <div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center text-brand-400">
212
- <svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
213
- </div>
214
- <div class="bubble-ai p-4 flex gap-2 items-center">
215
- <span class="text-xs text-gray-400">Analyzing database schema...</span>
216
- </div>
217
- </div>`;
218
- chatBox.insertAdjacentHTML('beforeend', html);
219
- scrollToBottom();
220
- return id;
221
- }
222
-
223
- async function appendMessage(role, content, isHtml = false) {
224
- const wrapper = document.createElement('div');
225
- wrapper.className = `flex gap-4 max-w-3xl mx-auto animate-fade ${role === 'user' ? 'justify-end' : ''}`;
226
-
227
- const aiIcon = `<div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center flex-shrink-0 border border-brand-500/30 text-brand-400"><svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg></div>`;
228
- const userIcon = `<div class="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center flex-shrink-0 text-gray-300"><svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg></div>`;
229
-
230
- const bubble = document.createElement('div');
231
- bubble.className = role === 'user' ? 'bubble-user p-4 max-w-[85%] shadow-lg text-sm' : 'bubble-ai p-5 max-w-[90%] shadow-sm w-full text-sm leading-relaxed';
232
-
233
- if (role === 'user') {
234
- bubble.textContent = content;
235
- wrapper.innerHTML = bubble.outerHTML + userIcon;
236
- } else {
237
- wrapper.innerHTML = aiIcon + bubble.outerHTML;
238
- }
239
- chatBox.appendChild(wrapper);
240
-
241
- if (role === 'ai') {
242
- playIncomingSound();
243
- if (isHtml) {
244
- wrapper.querySelector('.bubble-ai').innerHTML = content;
245
- Prism.highlightAll();
246
- } else {
247
- await typeText(wrapper.querySelector('.bubble-ai'), content);
248
- }
249
- }
250
- scrollToBottom();
251
- }
252
-
253
- function renderSuggestions(questions) {
254
- suggestionsBox.innerHTML = '';
255
- if (!questions) return;
256
- questions.forEach((q, i) => {
257
- const btn = document.createElement('button');
258
- btn.className = "whitespace-nowrap px-4 py-1.5 bg-dark-800 border border-dark-700 hover:border-brand-500 hover:text-brand-400 text-gray-400 text-xs font-medium rounded-full transition-all animate-fade";
259
- btn.style.animationDelay = `${i * 0.1}s`;
260
- btn.textContent = q;
261
- btn.onclick = () => { input.value = q; form.dispatchEvent(new Event('submit')); };
262
- suggestionsBox.appendChild(btn);
263
- });
264
- scrollToBottom();
265
- }
266
-
267
- function addToHistory(question) {
268
- const btn = document.createElement('button');
269
- btn.className = "w-full text-left px-4 py-2 text-xs text-gray-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors truncate animate-fade";
270
- btn.textContent = question;
271
- btn.onclick = () => { input.value = question; form.dispatchEvent(new Event('submit')); };
272
- historyList.prepend(btn);
273
- }
274
-
275
- // FIX 3: EXPORT CSV FUNCTION
276
- function downloadCSV(dataString) {
277
- try {
278
- const data = JSON.parse(decodeURIComponent(dataString));
279
- if (!data || !data.length) return;
280
-
281
- const headers = Object.keys(data[0]);
282
- const csvRows = [];
283
- csvRows.push(headers.join(','));
284
-
285
- for (const row of data) {
286
- const values = headers.map(header => {
287
- const escaped = ('' + row[header]).replace(/"/g, '\\"');
288
- return `"${escaped}"`;
289
- });
290
- csvRows.push(values.join(','));
291
- }
292
-
293
- const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
294
- const url = window.URL.createObjectURL(blob);
295
- const a = document.createElement('a');
296
- a.setAttribute('hidden', '');
297
- a.setAttribute('href', url);
298
- a.setAttribute('download', 'data_export.csv');
299
- document.body.appendChild(a);
300
- a.click();
301
- document.body.removeChild(a);
302
- } catch (e) {
303
- console.error("Export failed", e);
304
- }
305
- }
306
-
307
- form.addEventListener('submit', async (e) => {
308
- e.preventDefault();
309
- const question = input.value.trim();
310
- if (!question) return;
311
-
312
- unlockAudio();
313
- suggestionsBox.innerHTML = '';
314
- input.value = '';
315
-
316
- await appendMessage('user', question);
317
- addToHistory(question);
318
-
319
- const greetingResponse = checkGreeting(question);
320
- if (greetingResponse) {
321
- setTimeout(() => appendMessage('ai', greetingResponse, true), 500);
322
- return;
323
- }
324
-
325
- const loadingId = showLoading();
326
-
327
- try {
328
- const res = await fetch(`${API_URL}?ts=${Date.now()}`, {
329
- method: 'POST',
330
- headers: { 'Content-Type': 'application/json' },
331
- body: JSON.stringify({ question, history: conversationHistory })
332
- });
333
-
334
- if (!res.ok) throw new Error("Backend connection failed.");
335
-
336
- const data = await res.json();
337
- document.getElementById(loadingId).remove();
338
-
339
- if (data.sql && !data.sql.includes("Error")) {
340
- conversationHistory.push({ user: question, sql: data.sql });
341
- if(conversationHistory.length > 5) conversationHistory.shift();
342
- }
343
-
344
- let content = "";
345
- if (data.message) content += `<div class="mb-4">${data.message}</div>`;
346
-
347
- if (Array.isArray(data.answer) && data.answer.length > 0) {
348
- const firstRow = data.answer[0];
349
- if (typeof firstRow === 'string' && (firstRow.toLowerCase().includes("error"))) {
350
- content += `<div class="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 font-mono text-xs mb-3">⚠️ ${firstRow}</div>`;
351
- } else {
352
- const headers = Object.keys(firstRow);
353
- const dataStr = encodeURIComponent(JSON.stringify(data.answer));
354
-
355
- content += `
356
- <div class="overflow-hidden rounded-xl border border-dark-700 shadow-xl mb-3 bg-[#15171E]">
357
- <div class="overflow-x-auto">
358
- <table class="w-full text-left custom-table">
359
- <thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>
360
- <tbody>${data.answer.map(row => `<tr>${headers.map(h => `<td>${row[h]}</td>`).join('')}</tr>`).join('')}</tbody>
361
- </table>
362
- </div>
363
- </div>
364
- <div class="flex gap-2 mb-4">
365
- <button onclick="openChart('${dataStr}')" class="flex items-center gap-2 px-3 py-2 bg-brand-500 text-white text-xs font-bold rounded-lg hover:bg-brand-400 transition-colors">
366
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/></svg>
367
- Visualize
368
- </button>
369
- <button onclick="downloadCSV('${dataStr}')" class="flex items-center gap-2 px-3 py-2 bg-dark-700 border border-dark-600 text-gray-300 text-xs font-bold rounded-lg hover:bg-dark-600 transition-colors">
370
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
371
- Export CSV
372
- </button>
373
- </div>
374
- `;
375
- }
376
- } else if (Array.isArray(data.answer) && data.answer.length === 0) {
377
- content += `<div class="p-4 bg-yellow-500/5 border border-yellow-500/10 rounded-lg text-yellow-500/80 text-xs mb-3">No records found matching query.</div>`;
378
- }
379
-
380
- if (data.sql) {
381
- content += `
382
- <div class="relative group mt-2">
383
- <div class="absolute -top-3 left-3 bg-dark-700 px-2 text-[10px] text-gray-400 rounded border border-dark-600">Generated SQL</div>
384
- <pre class="!m-0 !p-4 !bg-[#0d1117] !text-xs rounded-xl border border-dark-700/50"><code class="language-sql">${data.sql}</code></pre>
385
- </div>`;
386
- }
387
-
388
- if (content) await appendMessage('ai', content, true);
389
- if (data.follow_ups) renderSuggestions(data.follow_ups);
390
-
391
- } catch (err) {
392
- document.getElementById(loadingId)?.remove();
393
- await appendMessage('ai', `<div class="p-4 bg-red-900/20 border border-red-500/30 rounded-lg text-red-400 text-sm">Error: ${err.message}</div>`, true);
394
- }
395
- });
396
-
397
- function openChart(dataString) {
398
- const data = JSON.parse(decodeURIComponent(dataString));
399
- const headers = Object.keys(data[0]);
400
- let labelKey = headers.find(h => isNaN(data[0][h])) || headers[0];
401
- let valueKey = headers.find(h => !isNaN(data[0][h]) && h !== labelKey) || headers[1];
402
- const labels = data.map(row => row[labelKey]);
403
- const values = data.map(row => row[valueKey]);
404
-
405
- modal.classList.remove('hidden');
406
- setTimeout(() => { modal.classList.remove('opacity-0'); modalContent.classList.remove('scale-95'); modalContent.classList.add('scale-100'); }, 10);
407
- if (chartInstance) chartInstance.destroy();
408
-
409
- const ctx = document.getElementById('myChart').getContext('2d');
410
- chartInstance = new Chart(ctx, {
411
- type: labels.length > 8 ? 'bar' : 'doughnut',
412
- data: {
413
- labels: labels,
414
- datasets: [{
415
- label: valueKey.toUpperCase(),
416
- data: values,
417
- backgroundColor: ['#38bdf8', '#a855f7', '#ec4899', '#22c55e', '#eab308'],
418
- borderColor: '#15171E', borderWidth: 2
419
- }]
420
- },
421
- options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8' } } }, scales: { x: { display: false }, y: { display: false } } }
422
- });
423
- }
424
-
425
- function closeChart() {
426
- modal.classList.add('opacity-0'); modalContent.classList.remove('scale-100'); modalContent.classList.add('scale-95'); setTimeout(() => { modal.classList.add('hidden'); }, 300);
427
- }
428
- </script>
429
- </body>
430
- </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PlainSQL | Enterprise Data Assistant</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
+
14
+ <script>
15
+ tailwind.config = {
16
+ theme: {
17
+ extend: {
18
+ fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
19
+ colors: {
20
+ dark: { 900: '#0B0C10', 800: '#15171E', 700: '#1F222E' },
21
+ brand: { 500: '#38bdf8', 400: '#0ea5e9' }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ </script>
27
+ <style>
28
+ body { background-color: #0B0C10; color: #E2E8F0; }
29
+ .scrollbar-custom::-webkit-scrollbar { width: 8px; }
30
+ .scrollbar-custom::-webkit-scrollbar-track { background: #15171E; }
31
+ .scrollbar-custom::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
32
+
33
+ .bubble-user { background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%); color: white; border-radius: 16px 16px 4px 16px; }
34
+ .bubble-ai { background: #1F222E; border: 1px solid #2D3142; border-radius: 16px 16px 16px 4px; }
35
+
36
+ .custom-table th { background: #262A3B; color: #94A3B8; font-size: 0.75rem; padding: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
37
+ .custom-table td { border-bottom: 1px solid #1F222E; color: #CBD5E1; padding: 12px; font-size: 0.9rem; }
38
+ .custom-table tr:last-child td { border-bottom: none; }
39
+
40
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
41
+ .animate-fade { animation: fadeIn 0.3s ease-out forwards; }
42
+ .typing-cursor::after { content: '▋'; animation: blink 1s step-start infinite; color: #38bdf8; margin-left: 2px; }
43
+ @keyframes blink { 50% { opacity: 0; } }
44
+
45
+ .scrollbar-hide::-webkit-scrollbar { display: none; }
46
+ .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
47
+ </style>
48
+ </head>
49
+ <body class="flex h-screen overflow-hidden selection:bg-brand-500 selection:text-white relative font-sans">
50
+
51
+ <audio id="sound-welcome" src="https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3" preload="auto"></audio>
52
+ <audio id="sound-message" src="https://assets.mixkit.co/active_storage/sfx/2346/2346-preview.mp3" preload="auto"></audio>
53
+
54
+ <div id="splash-screen" class="fixed inset-0 z-[100] bg-dark-900 flex flex-col items-center justify-center transition-opacity duration-700">
55
+ <div class="text-center space-y-6 animate-fade">
56
+ <div class="w-20 h-20 bg-brand-500 rounded-2xl flex items-center justify-center shadow-2xl shadow-brand-500/50 mx-auto mb-6">
57
+ <svg class="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.58 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.58 4 8 4s8-1.79 8-4M4 7c0-2.21 3.58-4 8-4s8 1.79 8 4m0 5c0 2.21-3.58 4-8 4s-8-1.79-8-4"/></svg>
58
+ </div>
59
+ <h1 class="text-4xl font-bold text-white tracking-tight">Plain<span class="text-brand-400">SQL</span></h1>
60
+ <p class="text-gray-400 text-sm">Enterprise Text-to-SQL Engine</p>
61
+ <button onclick="enterSystem()" class="mt-8 px-8 py-3 bg-white text-dark-900 font-bold rounded-full hover:bg-gray-100 transition-all shadow-lg hover:scale-105 transform">
62
+ Initialize System
63
+ </button>
64
+ </div>
65
+ </div>
66
+
67
+ <div id="chart-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm hidden opacity-0 transition-opacity duration-300">
68
+ <div class="bg-dark-800 border border-dark-700 w-full max-w-4xl h-[600px] rounded-2xl shadow-2xl flex flex-col p-6 transform scale-95 transition-transform duration-300" id="chart-content">
69
+ <div class="flex justify-between items-center mb-4">
70
+ <h3 class="text-lg font-semibold text-white">Data Visualization</h3>
71
+ <button onclick="closeChart()" class="text-gray-500 hover:text-white p-2 text-xl">✕</button>
72
+ </div>
73
+ <div class="flex-1 relative w-full h-full"><canvas id="myChart"></canvas></div>
74
+ </div>
75
+ </div>
76
+
77
+ <aside class="w-64 bg-dark-800 border-r border-dark-700 flex flex-col hidden md:flex">
78
+ <div class="p-6 flex items-center gap-3">
79
+ <div class="w-8 h-8 bg-brand-500 rounded-lg flex items-center justify-center shadow-lg">
80
+ <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.58 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.58 4 8 4s8-1.79 8-4M4 7c0-2.21 3.58-4 8-4s8 1.79 8 4m0 5c0 2.21-3.58 4-8 4s-8-1.79-8-4"/></svg>
81
+ </div>
82
+ <span class="font-bold text-xl tracking-tight">PlainSQL</span>
83
+ </div>
84
+ <nav class="flex-1 px-4 space-y-2 overflow-y-auto scrollbar-custom">
85
+ <button onclick="window.location.reload()" class="w-full flex items-center gap-3 px-4 py-3 bg-brand-500/10 text-brand-400 rounded-xl border border-brand-500/20 hover:bg-brand-500/20 transition-all group">
86
+ <svg class="w-5 h-5 group-hover:rotate-90 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
87
+ <span class="text-sm font-medium">New Analysis</span>
88
+ </button>
89
+ <div class="mt-8">
90
+ <p class="px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent Queries</p>
91
+ <div id="history-list" class="space-y-1"></div>
92
+ </div>
93
+ </nav>
94
+ <div class="p-4 border-t border-dark-700">
95
+ <div class="flex items-center gap-3 px-3 py-2 bg-green-500/10 rounded-lg border border-green-500/20">
96
+ <div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
97
+ <div class="flex flex-col">
98
+ <span class="text-[11px] font-bold text-green-400 uppercase">System Online</span>
99
+ <span class="text-[10px] text-gray-500">Read-Only Mode</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </aside>
104
+
105
+ <main class="flex-1 flex flex-col relative bg-dark-900">
106
+ <div id="chat-box" class="flex-1 overflow-y-auto p-4 md:p-8 space-y-6 pb-80 scroll-smooth scrollbar-custom">
107
+ <div class="flex gap-4 max-w-3xl mx-auto animate-fade">
108
+ <div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center flex-shrink-0 border border-brand-500/30 text-brand-400">
109
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
110
+ </div>
111
+ <div class="bubble-ai p-5 shadow-sm">
112
+ <p class="text-sm leading-relaxed text-gray-200">
113
+ Hello. I am <strong>PlainSQL</strong>. I can access your database securely to fetch real-time insights.<br><br>
114
+ <em>Try asking: "Show me top 5 employees by salary" or "List all active users."</em>
115
+ </p>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="absolute bottom-0 w-full p-4 md:p-6 bg-gradient-to-t from-dark-900 via-dark-900 to-transparent z-20">
121
+ <div class="max-w-3xl mx-auto">
122
+ <div id="suggestions" class="flex gap-2 mb-3 overflow-x-auto pb-1 scrollbar-hide"></div>
123
+ <form id="chat-form" class="relative group">
124
+ <div class="absolute inset-0 bg-brand-500/20 rounded-2xl blur-lg group-hover:bg-brand-500/30 transition-all opacity-0 group-hover:opacity-100"></div>
125
+ <input type="text" id="user-input"
126
+ class="relative w-full bg-dark-800 text-white border border-dark-700 rounded-2xl py-4 pl-5 pr-14 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all placeholder-gray-500 shadow-xl"
127
+ placeholder="Ask a question in plain English..." autocomplete="off">
128
+ <button type="submit" class="absolute right-2 top-2 p-2 bg-brand-500 hover:bg-brand-400 text-white rounded-xl transition-all shadow-lg active:scale-95">
129
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
130
+ </button>
131
+ </form>
132
+ <div class="text-center mt-2">
133
+ <p class="text-[10px] text-gray-600">AI Generated SQL can be inaccurate. Always verify important data.</p>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </main>
138
+
139
+ <script>
140
+ // --- 🚀 KEY CHANGE: DYNAMIC URL FOR DEPLOYMENT ---
141
+ const API_URL = window.location.origin + "/chat";
142
+
143
+ const form = document.getElementById('chat-form');
144
+ const input = document.getElementById('user-input');
145
+ const chatBox = document.getElementById('chat-box');
146
+ const suggestionsBox = document.getElementById('suggestions');
147
+ const historyList = document.getElementById('history-list');
148
+ const soundWelcome = document.getElementById('sound-welcome');
149
+ const soundMessage = document.getElementById('sound-message');
150
+ const splashScreen = document.getElementById('splash-screen');
151
+ const modal = document.getElementById('chart-modal');
152
+ const modalContent = document.getElementById('chart-content');
153
+
154
+ let chartInstance = null;
155
+ let conversationHistory = [];
156
+
157
+ function unlockAudio() {
158
+ soundMessage.volume = 0;
159
+ soundMessage.play().then(() => {
160
+ soundMessage.pause();
161
+ soundMessage.currentTime = 0;
162
+ }).catch(() => {});
163
+ }
164
+
165
+ function playIncomingSound() {
166
+ soundMessage.volume = 0.4;
167
+ soundMessage.currentTime = 0;
168
+ soundMessage.play().catch(e => console.warn("Audio blocked:", e));
169
+ }
170
+
171
+ function enterSystem() {
172
+ soundWelcome.volume = 0.5;
173
+ soundWelcome.play().catch(e => console.log("Init Audio Error:", e));
174
+ splashScreen.style.opacity = '0';
175
+ setTimeout(() => { splashScreen.style.display = 'none'; }, 700);
176
+ setTimeout(() => input.focus(), 800);
177
+ }
178
+
179
+ function checkGreeting(text) {
180
+ const t = text.toLowerCase();
181
+ const greetings = ['hello', 'hi', 'hey', 'good morning', 'good afternoon', 'hola'];
182
+
183
+ if (greetings.some(g => t === g || t.startsWith(g + ' '))) {
184
+ return "Hello! 👋 I am **PlainSQL**, your data assistant. I'm here to help you query your database without writing code.<br><br>You can ask me things like: <em>'Show me top 5 employees by salary'</em> or <em>'List all active users.'</em>";
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function scrollToBottom() {
190
+ setTimeout(() => {
191
+ chatBox.scrollTop = chatBox.scrollHeight;
192
+ }, 50);
193
+ }
194
+
195
+ async function typeText(element, text) {
196
+ element.classList.add('typing-cursor');
197
+ return new Promise(resolve => {
198
+ setTimeout(() => {
199
+ element.innerHTML = text;
200
+ element.classList.remove('typing-cursor');
201
+ resolve();
202
+ }, 300 + Math.random() * 500);
203
+ });
204
+ }
205
+
206
+ function showLoading() {
207
+ const id = 'loading-' + Date.now();
208
+ const html = `
209
+ <div id="${id}" class="flex gap-4 max-w-3xl mx-auto animate-fade">
210
+ <div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center text-brand-400">
211
+ <svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
212
+ </div>
213
+ <div class="bubble-ai p-4 flex gap-2 items-center">
214
+ <span class="text-xs text-gray-400">Analyzing database schema...</span>
215
+ </div>
216
+ </div>`;
217
+ chatBox.insertAdjacentHTML('beforeend', html);
218
+ scrollToBottom();
219
+ return id;
220
+ }
221
+
222
+ async function appendMessage(role, content, isHtml = false) {
223
+ const wrapper = document.createElement('div');
224
+ wrapper.className = `flex gap-4 max-w-3xl mx-auto animate-fade ${role === 'user' ? 'justify-end' : ''}`;
225
+
226
+ const aiIcon = `<div class="w-8 h-8 rounded-full bg-brand-500/20 flex items-center justify-center flex-shrink-0 border border-brand-500/30 text-brand-400"><svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg></div>`;
227
+ const userIcon = `<div class="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center flex-shrink-0 text-gray-300"><svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg></div>`;
228
+
229
+ const bubble = document.createElement('div');
230
+ bubble.className = role === 'user' ? 'bubble-user p-4 max-w-[85%] shadow-lg text-sm' : 'bubble-ai p-5 max-w-[90%] shadow-sm w-full text-sm leading-relaxed';
231
+
232
+ if (role === 'user') {
233
+ bubble.textContent = content;
234
+ wrapper.innerHTML = bubble.outerHTML + userIcon;
235
+ } else {
236
+ wrapper.innerHTML = aiIcon + bubble.outerHTML;
237
+ }
238
+ chatBox.appendChild(wrapper);
239
+
240
+ if (role === 'ai') {
241
+ playIncomingSound();
242
+ if (isHtml) {
243
+ wrapper.querySelector('.bubble-ai').innerHTML = content;
244
+ Prism.highlightAll();
245
+ } else {
246
+ await typeText(wrapper.querySelector('.bubble-ai'), content);
247
+ }
248
+ }
249
+ scrollToBottom();
250
+ }
251
+
252
+ function renderSuggestions(questions) {
253
+ suggestionsBox.innerHTML = '';
254
+ if (!questions) return;
255
+ questions.forEach((q, i) => {
256
+ const btn = document.createElement('button');
257
+ btn.className = "whitespace-nowrap px-4 py-1.5 bg-dark-800 border border-dark-700 hover:border-brand-500 hover:text-brand-400 text-gray-400 text-xs font-medium rounded-full transition-all animate-fade";
258
+ btn.style.animationDelay = `${i * 0.1}s`;
259
+ btn.textContent = q;
260
+ btn.onclick = () => { input.value = q; form.dispatchEvent(new Event('submit')); };
261
+ suggestionsBox.appendChild(btn);
262
+ });
263
+ scrollToBottom();
264
+ }
265
+
266
+ function addToHistory(question) {
267
+ const btn = document.createElement('button');
268
+ btn.className = "w-full text-left px-4 py-2 text-xs text-gray-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors truncate animate-fade";
269
+ btn.textContent = question;
270
+ btn.onclick = () => { input.value = question; form.dispatchEvent(new Event('submit')); };
271
+ historyList.prepend(btn);
272
+ }
273
+
274
+ function downloadCSV(dataString) {
275
+ try {
276
+ const data = JSON.parse(decodeURIComponent(dataString));
277
+ if (!data || !data.length) return;
278
+
279
+ const headers = Object.keys(data[0]);
280
+ const csvRows = [];
281
+ csvRows.push(headers.join(','));
282
+
283
+ for (const row of data) {
284
+ const values = headers.map(header => {
285
+ const escaped = ('' + row[header]).replace(/"/g, '\\"');
286
+ return `"${escaped}"`;
287
+ });
288
+ csvRows.push(values.join(','));
289
+ }
290
+
291
+ const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
292
+ const url = window.URL.createObjectURL(blob);
293
+ const a = document.createElement('a');
294
+ a.setAttribute('hidden', '');
295
+ a.setAttribute('href', url);
296
+ a.setAttribute('download', 'data_export.csv');
297
+ document.body.appendChild(a);
298
+ a.click();
299
+ document.body.removeChild(a);
300
+ } catch (e) {
301
+ console.error("Export failed", e);
302
+ }
303
+ }
304
+
305
+ form.addEventListener('submit', async (e) => {
306
+ e.preventDefault();
307
+ const question = input.value.trim();
308
+ if (!question) return;
309
+
310
+ unlockAudio();
311
+ suggestionsBox.innerHTML = '';
312
+ input.value = '';
313
+
314
+ await appendMessage('user', question);
315
+ addToHistory(question);
316
+
317
+ const greetingResponse = checkGreeting(question);
318
+ if (greetingResponse) {
319
+ setTimeout(() => appendMessage('ai', greetingResponse, true), 500);
320
+ return;
321
+ }
322
+
323
+ const loadingId = showLoading();
324
+
325
+ try {
326
+ const res = await fetch(`${API_URL}?ts=${Date.now()}`, {
327
+ method: 'POST',
328
+ headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ question, history: conversationHistory })
330
+ });
331
+
332
+ if (!res.ok) throw new Error("Backend connection failed.");
333
+
334
+ const data = await res.json();
335
+ document.getElementById(loadingId).remove();
336
+
337
+ if (data.sql && !data.sql.includes("Error")) {
338
+ conversationHistory.push({ user: question, sql: data.sql });
339
+ if(conversationHistory.length > 5) conversationHistory.shift();
340
+ }
341
+
342
+ let content = "";
343
+ if (data.message) content += `<div class="mb-4">${data.message}</div>`;
344
+
345
+ if (Array.isArray(data.answer) && data.answer.length > 0) {
346
+ const firstRow = data.answer[0];
347
+ if (typeof firstRow === 'string' && (firstRow.toLowerCase().includes("error"))) {
348
+ content += `<div class="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 font-mono text-xs mb-3">⚠️ ${firstRow}</div>`;
349
+ } else {
350
+ const headers = Object.keys(firstRow);
351
+ const dataStr = encodeURIComponent(JSON.stringify(data.answer));
352
+
353
+ content += `
354
+ <div class="overflow-hidden rounded-xl border border-dark-700 shadow-xl mb-3 bg-[#15171E]">
355
+ <div class="overflow-x-auto">
356
+ <table class="w-full text-left custom-table">
357
+ <thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>
358
+ <tbody>${data.answer.map(row => `<tr>${headers.map(h => `<td>${row[h]}</td>`).join('')}</tr>`).join('')}</tbody>
359
+ </table>
360
+ </div>
361
+ </div>
362
+ <div class="flex gap-2 mb-4">
363
+ <button onclick="openChart('${dataStr}')" class="flex items-center gap-2 px-3 py-2 bg-brand-500 text-white text-xs font-bold rounded-lg hover:bg-brand-400 transition-colors">
364
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/></svg>
365
+ Visualize
366
+ </button>
367
+ <button onclick="downloadCSV('${dataStr}')" class="flex items-center gap-2 px-3 py-2 bg-dark-700 border border-dark-600 text-gray-300 text-xs font-bold rounded-lg hover:bg-dark-600 transition-colors">
368
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
369
+ Export CSV
370
+ </button>
371
+ </div>
372
+ `;
373
+ }
374
+ } else if (Array.isArray(data.answer) && data.answer.length === 0) {
375
+ content += `<div class="p-4 bg-yellow-500/5 border border-yellow-500/10 rounded-lg text-yellow-500/80 text-xs mb-3">No records found matching query.</div>`;
376
+ }
377
+
378
+ if (data.sql) {
379
+ content += `
380
+ <div class="relative group mt-2">
381
+ <div class="absolute -top-3 left-3 bg-dark-700 px-2 text-[10px] text-gray-400 rounded border border-dark-600">Generated SQL</div>
382
+ <pre class="!m-0 !p-4 !bg-[#0d1117] !text-xs rounded-xl border border-dark-700/50"><code class="language-sql">${data.sql}</code></pre>
383
+ </div>`;
384
+ }
385
+
386
+ if (content) await appendMessage('ai', content, true);
387
+ if (data.follow_ups) renderSuggestions(data.follow_ups);
388
+
389
+ } catch (err) {
390
+ document.getElementById(loadingId)?.remove();
391
+ await appendMessage('ai', `<div class="p-4 bg-red-900/20 border border-red-500/30 rounded-lg text-red-400 text-sm">Error: ${err.message}</div>`, true);
392
+ }
393
+ });
394
+
395
+ function openChart(dataString) {
396
+ const data = JSON.parse(decodeURIComponent(dataString));
397
+ const headers = Object.keys(data[0]);
398
+ let labelKey = headers.find(h => isNaN(data[0][h])) || headers[0];
399
+ let valueKey = headers.find(h => !isNaN(data[0][h]) && h !== labelKey) || headers[1];
400
+ const labels = data.map(row => row[labelKey]);
401
+ const values = data.map(row => row[valueKey]);
402
+
403
+ modal.classList.remove('hidden');
404
+ setTimeout(() => { modal.classList.remove('opacity-0'); modalContent.classList.remove('scale-95'); modalContent.classList.add('scale-100'); }, 10);
405
+ if (chartInstance) chartInstance.destroy();
406
+
407
+ const ctx = document.getElementById('myChart').getContext('2d');
408
+ chartInstance = new Chart(ctx, {
409
+ type: labels.length > 8 ? 'bar' : 'doughnut',
410
+ data: {
411
+ labels: labels,
412
+ datasets: [{
413
+ label: valueKey.toUpperCase(),
414
+ data: values,
415
+ backgroundColor: ['#38bdf8', '#a855f7', '#ec4899', '#22c55e', '#eab308'],
416
+ borderColor: '#15171E', borderWidth: 2
417
+ }]
418
+ },
419
+ options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8' } } }, scales: { x: { display: false }, y: { display: false } } }
420
+ });
421
+ }
422
+
423
+ function closeChart() {
424
+ modal.classList.add('opacity-0'); modalContent.classList.remove('scale-100'); modalContent.classList.add('scale-95'); setTimeout(() => { modal.classList.add('hidden'); }, 300);
425
+ }
426
+ </script>
427
+ </body>
428
+ </html>