galbendavids's picture
עדכון: הסרת RAG, הוספת ארכיטקטורה מפורטת, תיקון לינקים, שינוי שם פרויקט ל-SQL-based
f073efc
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;
}
// Show history section if there are entries
if (historySection) historySection.style.display = 'block';
// Show history in reverse order (newest first)
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) {
// Restore query to input
document.getElementById('query').value = entry.query;
// Restore response
const responseSection = document.getElementById('last-response');
const summaryDiv = document.getElementById('resp-summary');
// Clear existing visualizations
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);
// Restore sources/query results if available
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';
}
// Restore visualizations if available
if (entry.response.visualizations && entry.response.visualizations.length > 0) {
showVisualizations(entry.response.visualizations);
}
// Scroll to response
responseSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async function sendQuery() {
const q = document.getElementById('query').value.trim();
if (!q) {
alert('אנא הזן שאלה');
return;
}
// Show loading state
const sendBtn = document.getElementById('send');
const originalText = sendBtn.textContent;
sendBtn.disabled = true;
sendBtn.textContent = '⏳ שולח...';
// Show loading animation
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 {
// Always use SQL-based approach
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();
// Hide loading animation
if (loadingContainer) {
loadingContainer.classList.remove('active');
}
// Show response
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>';
}
// Show sources/query results if checkbox is checked
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';
}
// Show visualizations if available
if (j.visualizations && j.visualizations.length > 0) {
showVisualizations(j.visualizations);
}
// Refresh history after a short delay to ensure server has saved it
setTimeout(async () => {
await refreshHistory();
}, 1000); // Increased delay to 1 second to ensure history is saved
// Scroll to response
responseSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (e) {
console.error('Query error:', e);
// Hide loading animation
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;">';
// Header
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>';
// Rows (limit to 10 for display)
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) {
// Create or get visualizations container
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);
}
// Clear previous visualizations
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)';
// Add explanation based on chart type
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);
// Create chart using Chart.js
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';
// Color palettes for different chart types
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':
// For histogram, we need to create bins
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) {
// Format markdown-like text (bold, lists, etc.)
let formatted = escapeHtml(text);
// Convert **text** to <strong>text</strong>
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Convert numbered lists
formatted = formatted.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="margin: 8px 0;"><strong>$1.</strong> $2</div>');
// Convert bullet points
formatted = formatted.replace(/^[-•]\s+(.+)$/gm, '<div style="margin: 4px 0; padding-right: 20px;">• $1</div>');
// Convert line breaks
formatted = formatted.replace(/\n/g, '<br>');
return formatted;
}
// formatSources function removed - no longer needed (SQL-based approach)
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();
});