| async function checkServer() { |
| try { |
| const r = await fetch('/health', {method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({})}); |
| if (!r.ok) throw new Error('no'); |
| const j = await r.json(); |
| const statusEl = document.getElementById('server-status'); |
| if (j.status === 'ok') { |
| statusEl.textContent = '✓ פעיל'; |
| statusEl.style.color = '#ffffff'; |
| statusEl.style.textShadow = '0 1px 3px rgba(0,0,0,0.5), 0 0 8px rgba(76, 175, 80, 0.8)'; |
| statusEl.style.fontWeight = '700'; |
| } else { |
| statusEl.textContent = j.status || 'לא ידוע'; |
| statusEl.style.color = '#ffebee'; |
| statusEl.style.textShadow = '0 1px 3px rgba(0,0,0,0.5)'; |
| statusEl.style.fontWeight = '700'; |
| } |
| } catch (e) { |
| const statusEl = document.getElementById('server-status'); |
| statusEl.textContent = '✗ לא זמין'; |
| statusEl.style.color = '#ffebee'; |
| statusEl.style.textShadow = '0 1px 3px rgba(0,0,0,0.5), 0 0 8px rgba(244, 67, 54, 0.8)'; |
| statusEl.style.fontWeight = '700'; |
| } |
| } |
|
|
| async function refreshHistory() { |
| try { |
| const r = await fetch('/history'); |
| const j = await r.json(); |
| const h = j.history || []; |
| const container = document.getElementById('history'); |
| const historySection = document.getElementById('history-section'); |
| |
| container.innerHTML = ''; |
| |
| if (h.length === 0) { |
| if (historySection) historySection.style.display = 'none'; |
| container.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">אין שאלות קודמות. התחל לשאול שאלות!</div>'; |
| return; |
| } |
| |
| |
| if (historySection) historySection.style.display = 'block'; |
| |
| |
| h.slice().reverse().forEach((entry, idx) => { |
| const el = document.createElement('div'); |
| el.className = 'history-item'; |
| el.style.cursor = 'pointer'; |
| el.onclick = () => restoreHistoryEntry(entry); |
| |
| const q = document.createElement('div'); |
| q.className = 'history-query'; |
| q.innerHTML = `❓ ${escapeHtml(entry.query)}`; |
| |
| const s = document.createElement('div'); |
| s.className = 'history-response'; |
| s.innerHTML = entry.response && entry.response.summary ? formatResponse(entry.response.summary.substring(0, 200) + (entry.response.summary.length > 200 ? '...' : '')) : '<span style="color: #999;">אין תשובה</span>'; |
| |
| const restoreBtn = document.createElement('button'); |
| restoreBtn.className = 'muted'; |
| restoreBtn.style.marginTop = '8px'; |
| restoreBtn.style.fontSize = '13px'; |
| restoreBtn.style.padding = '6px 12px'; |
| restoreBtn.textContent = '↩️ שחזר שאלה ותשובה'; |
| restoreBtn.onclick = (e) => { |
| e.stopPropagation(); |
| restoreHistoryEntry(entry); |
| }; |
| |
| el.appendChild(q); |
| el.appendChild(s); |
| el.appendChild(restoreBtn); |
| container.appendChild(el); |
| }); |
| } catch (e) { |
| console.error('history fetch failed', e); |
| const container = document.getElementById('history'); |
| const historySection = document.getElementById('history-section'); |
| if (historySection) historySection.style.display = 'block'; |
| if (container) { |
| container.innerHTML = '<div style="color: #d32f2f;">שגיאה בטעינת ההיסטוריה: ' + escapeHtml(e.message) + '</div>'; |
| } |
| } |
| } |
|
|
| function restoreHistoryEntry(entry) { |
| |
| document.getElementById('query').value = entry.query; |
| |
| |
| const responseSection = document.getElementById('last-response'); |
| const summaryDiv = document.getElementById('resp-summary'); |
| |
| |
| const existingViz = document.getElementById('resp-visualizations'); |
| if (existingViz) { |
| existingViz.remove(); |
| } |
| |
| if (entry.response && entry.response.summary) { |
| responseSection.style.display = 'block'; |
| summaryDiv.innerHTML = formatResponse(entry.response.summary); |
| |
| |
| const sourcesDiv = document.getElementById('resp-sources'); |
| if (entry.response.query_results && entry.response.query_results.length > 0) { |
| const showSources = document.getElementById('show-sources')?.checked; |
| if (showSources) { |
| sourcesDiv.style.display = 'block'; |
| sourcesDiv.innerHTML = formatSQLResults({ |
| sql_queries: entry.response.sql_queries || [], |
| query_results: entry.response.query_results || [] |
| }); |
| } else { |
| sourcesDiv.style.display = 'none'; |
| } |
| } else { |
| if (sourcesDiv) sourcesDiv.style.display = 'none'; |
| } |
| |
| |
| if (entry.response.visualizations && entry.response.visualizations.length > 0) { |
| showVisualizations(entry.response.visualizations); |
| } |
| |
| |
| responseSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| } |
| } |
|
|
| function escapeHtml(unsafe) { |
| return unsafe |
| .replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">") |
| .replace(/\"/g, """) |
| .replace(/'/g, "'"); |
| } |
|
|
| async function sendQuery() { |
| const q = document.getElementById('query').value.trim(); |
| if (!q) { |
| alert('אנא הזן שאלה'); |
| return; |
| } |
| |
| |
| const sendBtn = document.getElementById('send'); |
| const originalText = sendBtn.textContent; |
| sendBtn.disabled = true; |
| sendBtn.textContent = '⏳ שולח...'; |
| |
| |
| const loadingContainer = document.getElementById('loading-container'); |
| const responseSection = document.getElementById('last-response'); |
| if (loadingContainer) { |
| loadingContainer.classList.add('active'); |
| } |
| if (responseSection) { |
| responseSection.style.display = 'none'; |
| } |
| |
| try { |
| |
| let endpoint = '/query-sql'; |
| const body = { query: q, top_k: 5 }; |
| |
| const r = await fetch(endpoint, { |
| method: 'POST', |
| headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify(body) |
| }); |
| |
| if (!r.ok) { |
| throw new Error(`HTTP ${r.status}: ${r.statusText}`); |
| } |
| |
| const j = await r.json(); |
| |
| |
| if (loadingContainer) { |
| loadingContainer.classList.remove('active'); |
| } |
| |
| |
| const responseSection = document.getElementById('last-response'); |
| const summaryDiv = document.getElementById('resp-summary'); |
| |
| if (j.summary) { |
| responseSection.style.display = 'block'; |
| summaryDiv.innerHTML = formatResponse(j.summary); |
| } else { |
| responseSection.style.display = 'block'; |
| summaryDiv.innerHTML = '<span style="color: #d32f2f;">לא התקבלה תשובה מהשרת</span>'; |
| } |
| |
| |
| const showSources = document.getElementById('show-sources')?.checked; |
| const sourcesDiv = document.getElementById('resp-sources'); |
| |
| if (showSources) { |
| if (j.query_results && j.query_results.length > 0) { |
| sourcesDiv.style.display = 'block'; |
| sourcesDiv.innerHTML = formatSQLResults(j); |
| } else { |
| if (sourcesDiv) sourcesDiv.style.display = 'none'; |
| } |
| } else { |
| if (sourcesDiv) sourcesDiv.style.display = 'none'; |
| } |
| |
| |
| if (j.visualizations && j.visualizations.length > 0) { |
| showVisualizations(j.visualizations); |
| } |
| |
| |
| setTimeout(async () => { |
| await refreshHistory(); |
| }, 1000); |
| |
| |
| responseSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| |
| } catch (e) { |
| console.error('Query error:', e); |
| |
| const loadingContainer = document.getElementById('loading-container'); |
| if (loadingContainer) { |
| loadingContainer.classList.remove('active'); |
| } |
| const responseSection = document.getElementById('last-response'); |
| const summaryDiv = document.getElementById('resp-summary'); |
| responseSection.style.display = 'block'; |
| summaryDiv.innerHTML = `<span style="color: #d32f2f;">שגיאה: ${escapeHtml(e.message)}</span>`; |
| } finally { |
| sendBtn.disabled = false; |
| sendBtn.textContent = originalText; |
| } |
| } |
|
|
| function formatSQLResults(data) { |
| let html = '<div style="margin-top: 20px;"><h4 style="color: #1976d2; margin-bottom: 12px;">שאילתות SQL שבוצעו:</h4>'; |
| |
| if (data.sql_queries && data.sql_queries.length > 0) { |
| html += '<div style="margin-bottom: 20px;">'; |
| data.sql_queries.forEach((query, idx) => { |
| html += `<div style="background: #f5f5f5; padding: 12px; border-radius: 8px; margin-bottom: 12px; font-family: monospace; font-size: 13px; direction: ltr; text-align: left;">`; |
| html += `<strong>שאילתה ${idx + 1}:</strong><br>${escapeHtml(query)}</div>`; |
| }); |
| html += '</div>'; |
| } |
| |
| if (data.query_results && data.query_results.length > 0) { |
| html += '<h4 style="color: #1976d2; margin-bottom: 12px;">תוצאות:</h4>'; |
| data.query_results.forEach((qr, idx) => { |
| html += `<div style="margin-bottom: 20px; padding: 16px; background: #f8f9fa; border-radius: 8px;">`; |
| html += `<strong>תוצאה ${idx + 1}:</strong> `; |
| if (qr.error) { |
| html += `<span style="color: #d32f2f;">שגיאה: ${escapeHtml(qr.error)}</span>`; |
| } else { |
| html += `<span style="color: #4caf50;">${qr.row_count} שורות</span>`; |
| if (qr.result && qr.result.length > 0) { |
| html += '<table style="width: 100%; margin-top: 12px; border-collapse: collapse;">'; |
| |
| html += '<thead><tr style="background: #e3f2fd;">'; |
| Object.keys(qr.result[0]).forEach(col => { |
| html += `<th style="padding: 8px; text-align: right; border: 1px solid #ddd;">${escapeHtml(col)}</th>`; |
| }); |
| html += '</tr></thead><tbody>'; |
| |
| qr.result.slice(0, 10).forEach(row => { |
| html += '<tr>'; |
| Object.values(row).forEach(val => { |
| html += `<td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(String(val))}</td>`; |
| }); |
| html += '</tr>'; |
| }); |
| html += '</tbody></table>'; |
| if (qr.result.length > 10) { |
| html += `<div style="margin-top: 8px; color: #666; font-size: 14px;">...ועוד ${qr.result.length - 10} שורות</div>`; |
| } |
| } |
| } |
| html += '</div>'; |
| }); |
| } |
| |
| html += '</div>'; |
| return html; |
| } |
|
|
| function showVisualizations(visualizations) { |
| |
| let vizContainer = document.getElementById('resp-visualizations'); |
| if (!vizContainer) { |
| vizContainer = document.createElement('div'); |
| vizContainer.id = 'resp-visualizations'; |
| vizContainer.className = 'viz-container'; |
| vizContainer.style.marginTop = '24px'; |
| document.getElementById('last-response').appendChild(vizContainer); |
| } |
| |
| |
| vizContainer.innerHTML = '<h4 class="viz-title">📊 גרפיקות ויזואליזציות</h4>'; |
| vizContainer.style.display = 'block'; |
| |
| visualizations.forEach((viz, idx) => { |
| const vizDiv = document.createElement('div'); |
| vizDiv.style.marginBottom = '32px'; |
| vizDiv.style.padding = '20px'; |
| vizDiv.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'; |
| vizDiv.style.borderRadius = '16px'; |
| vizDiv.style.boxShadow = '0 4px 16px rgba(0,0,0,0.08)'; |
| vizDiv.style.border = '1px solid rgba(25, 118, 210, 0.1)'; |
| |
| |
| let explanation = ''; |
| switch(viz.type) { |
| case 'bar': |
| explanation = '📊 <strong>גרף עמודות:</strong> מציג את הנתונים בצורה ויזואלית ברורה. כל עמודה מייצגת קטגוריה, והגובה שלה מייצג את הערך. זה עוזר להשוות בין קטגוריות שונות ולהבין את ההבדלים ביניהן.'; |
| break; |
| case 'line': |
| explanation = '📈 <strong>גרף קו:</strong> מציג מגמות ושינויים לאורך זמן. הקו עולה כשיש עלייה בערכים ויורד כשיש ירידה. זה עוזר לזהות דפוסים, שינויים תקופתיים, ומגמות ארוכות טווח.'; |
| break; |
| case 'scatter': |
| explanation = '🔵 <strong>גרף פיזור:</strong> מציג את הקשר בין שני משתנים. כל נקודה מייצגת תצפית אחת. זה עוזר לזהות קשרים, מתאמים, וחריגים בנתונים.'; |
| break; |
| case 'histogram': |
| explanation = '📊 <strong>היסטוגרמה:</strong> מציגה את התפלגות הנתונים. כל עמודה מייצגת טווח ערכים, והגובה שלה מייצג כמה תצפיות נפלו בטווח הזה. זה עוזר להבין את הצורה של ההתפלגות - האם היא סימטרית, מוטה, או יש לה כמה פסגות.'; |
| break; |
| default: |
| explanation = '📊 <strong>ויזואליזציה:</strong> מציגה את הנתונים בצורה גרפית כדי להקל על הבנה וניתוח.'; |
| } |
| |
| vizDiv.innerHTML = ` |
| <h5 style="margin-top: 0; color: #1976d2; font-size: 18px; font-weight: 700; margin-bottom: 16px;"> |
| ${escapeHtml(viz.title)} |
| </h5> |
| <div class="viz-explanation">${explanation}</div> |
| `; |
| |
| const canvasDiv = document.createElement('div'); |
| canvasDiv.style.position = 'relative'; |
| canvasDiv.style.height = '450px'; |
| canvasDiv.style.background = '#ffffff'; |
| canvasDiv.style.borderRadius = '12px'; |
| canvasDiv.style.padding = '16px'; |
| canvasDiv.innerHTML = `<canvas id="chart-${idx}"></canvas>`; |
| vizDiv.appendChild(canvasDiv); |
| |
| vizContainer.appendChild(vizDiv); |
| |
| |
| setTimeout(() => { |
| const canvas = document.getElementById(`chart-${idx}`); |
| if (!canvas) return; |
| |
| const ctx = canvas.getContext('2d'); |
| const config = getChartConfig(viz, idx); |
| if (config) { |
| new Chart(ctx, config); |
| } |
| }, 100); |
| }); |
| } |
|
|
| function getChartConfig(viz, idx) { |
| if (!viz.data || viz.data.length === 0) return null; |
| |
| const xLabel = viz.x_label || viz.x || 'X'; |
| const yLabel = viz.y_label || viz.y || 'Y'; |
| |
| |
| const colorPalettes = { |
| bar: [ |
| 'rgba(25, 118, 210, 0.8)', 'rgba(76, 175, 80, 0.8)', 'rgba(255, 152, 0, 0.8)', |
| 'rgba(156, 39, 176, 0.8)', 'rgba(244, 67, 54, 0.8)', 'rgba(0, 188, 212, 0.8)', |
| 'rgba(255, 193, 7, 0.8)', 'rgba(121, 85, 72, 0.8)' |
| ], |
| line: ['rgba(25, 118, 210, 1)', 'rgba(76, 175, 80, 1)', 'rgba(255, 152, 0, 1)'], |
| scatter: ['rgba(25, 118, 210, 0.7)', 'rgba(76, 175, 80, 0.7)', 'rgba(255, 152, 0, 0.7)'] |
| }; |
| |
| switch (viz.type) { |
| case 'bar': |
| const barColors = viz.data.map((_, i) => colorPalettes.bar[i % colorPalettes.bar.length]); |
| return { |
| type: 'bar', |
| data: { |
| labels: viz.data.map(d => String(d[viz.x])), |
| datasets: [{ |
| label: yLabel, |
| data: viz.data.map(d => d[viz.y]), |
| backgroundColor: barColors, |
| borderColor: barColors.map(c => c.replace('0.8', '1')), |
| borderWidth: 2, |
| borderRadius: 8, |
| borderSkipped: false, |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top', |
| labels: { |
| font: { size: 14, weight: 'bold' }, |
| padding: 15, |
| usePointStyle: true |
| } |
| }, |
| title: { |
| display: false |
| }, |
| tooltip: { |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| padding: 12, |
| titleFont: { size: 14, weight: 'bold' }, |
| bodyFont: { size: 13 }, |
| borderColor: 'rgba(25, 118, 210, 0.8)', |
| borderWidth: 2, |
| cornerRadius: 8 |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: yLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: xLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| } |
| } |
| } |
| }; |
| |
| case 'line': |
| return { |
| type: 'line', |
| data: { |
| labels: viz.data.map(d => String(d[viz.x])), |
| datasets: [{ |
| label: yLabel, |
| data: viz.data.map(d => d[viz.y]), |
| borderColor: 'rgba(25, 118, 210, 1)', |
| backgroundColor: 'rgba(25, 118, 210, 0.15)', |
| borderWidth: 3, |
| fill: true, |
| tension: 0.5, |
| pointRadius: 5, |
| pointHoverRadius: 7, |
| pointBackgroundColor: 'rgba(25, 118, 210, 1)', |
| pointBorderColor: '#ffffff', |
| pointBorderWidth: 2, |
| pointHoverBackgroundColor: 'rgba(25, 118, 210, 1)', |
| pointHoverBorderColor: '#ffffff', |
| pointHoverBorderWidth: 3 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top', |
| labels: { |
| font: { size: 14, weight: 'bold' }, |
| padding: 15, |
| usePointStyle: true |
| } |
| }, |
| tooltip: { |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| padding: 12, |
| titleFont: { size: 14, weight: 'bold' }, |
| bodyFont: { size: 13 }, |
| borderColor: 'rgba(25, 118, 210, 0.8)', |
| borderWidth: 2, |
| cornerRadius: 8 |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: yLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: xLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| } |
| } |
| } |
| }; |
| |
| case 'scatter': |
| return { |
| type: 'scatter', |
| data: { |
| datasets: [{ |
| label: `${xLabel} vs ${yLabel}`, |
| data: viz.data.map(d => ({ |
| x: d[viz.x], |
| y: d[viz.y] |
| })), |
| backgroundColor: 'rgba(25, 118, 210, 0.7)', |
| borderColor: 'rgba(25, 118, 210, 1)', |
| borderWidth: 2, |
| pointRadius: 6, |
| pointHoverRadius: 8, |
| pointHoverBackgroundColor: 'rgba(76, 175, 80, 0.8)', |
| pointHoverBorderColor: '#ffffff', |
| pointHoverBorderWidth: 2 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top', |
| labels: { |
| font: { size: 14, weight: 'bold' }, |
| padding: 15, |
| usePointStyle: true |
| } |
| }, |
| tooltip: { |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| padding: 12, |
| titleFont: { size: 14, weight: 'bold' }, |
| bodyFont: { size: 13 }, |
| borderColor: 'rgba(25, 118, 210, 0.8)', |
| borderWidth: 2, |
| cornerRadius: 8 |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: yLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| }, |
| x: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: xLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| } |
| } |
| } |
| }; |
| |
| case 'histogram': |
| |
| const values = viz.data.filter(v => v != null && !isNaN(v)); |
| if (values.length === 0) return null; |
| |
| const min = Math.min(...values); |
| const max = Math.max(...values); |
| const binCount = Math.min(20, Math.ceil(Math.sqrt(values.length))); |
| const binSize = (max - min) / binCount; |
| |
| const bins = Array(binCount).fill(0); |
| const binLabels = []; |
| |
| for (let i = 0; i < binCount; i++) { |
| binLabels.push((min + i * binSize).toFixed(1)); |
| } |
| |
| values.forEach(v => { |
| const binIndex = Math.min(Math.floor((v - min) / binSize), binCount - 1); |
| bins[binIndex]++; |
| }); |
| |
| const histColors = bins.map((_, i) => { |
| const ratio = i / binCount; |
| return `rgba(${25 + Math.floor(ratio * 180)}, ${118 + Math.floor(ratio * 100)}, ${210 - Math.floor(ratio * 100)}, 0.8)`; |
| }); |
| |
| return { |
| type: 'bar', |
| data: { |
| labels: binLabels, |
| datasets: [{ |
| label: xLabel, |
| data: bins, |
| backgroundColor: histColors, |
| borderColor: histColors.map(c => c.replace('0.8', '1')), |
| borderWidth: 2, |
| borderRadius: 4, |
| borderSkipped: false, |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top', |
| labels: { |
| font: { size: 14, weight: 'bold' }, |
| padding: 15, |
| usePointStyle: true |
| } |
| }, |
| tooltip: { |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| padding: 12, |
| titleFont: { size: 14, weight: 'bold' }, |
| bodyFont: { size: 13 }, |
| borderColor: 'rgba(25, 118, 210, 0.8)', |
| borderWidth: 2, |
| cornerRadius: 8 |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: 'תדירות', |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: xLabel, |
| font: { size: 14, weight: 'bold' }, |
| color: '#1976d2' |
| }, |
| grid: { |
| color: 'rgba(25, 118, 210, 0.1)', |
| lineWidth: 1 |
| }, |
| ticks: { |
| font: { size: 12 }, |
| color: '#555' |
| } |
| } |
| } |
| } |
| }; |
| |
| default: |
| return null; |
| } |
| } |
|
|
| function formatResponse(text) { |
| |
| let formatted = escapeHtml(text); |
| |
| formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
| |
| formatted = formatted.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="margin: 8px 0;"><strong>$1.</strong> $2</div>'); |
| |
| formatted = formatted.replace(/^[-•]\s+(.+)$/gm, '<div style="margin: 4px 0; padding-right: 20px;">• $1</div>'); |
| |
| formatted = formatted.replace(/\n/g, '<br>'); |
| return formatted; |
| } |
|
|
| |
|
|
| async function clearHistory() { |
| try { |
| await fetch('/history/clear', {method: 'POST'}); |
| await refreshHistory(); |
| } catch (e) { |
| console.error('clear failed', e); |
| } |
| } |
|
|
| window.addEventListener('load', async () => { |
| document.getElementById('send').addEventListener('click', sendQuery); |
| document.getElementById('clear-history').addEventListener('click', clearHistory); |
| await checkServer(); |
| await refreshHistory(); |
| }); |
|
|