/** * Sibilla — Frontend search interface * Mobile-friendly, smooth UX */ document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('search-form'); const queryInput = document.getElementById('query'); const submitBtn = document.getElementById('submit-btn'); const responseArea = document.getElementById('response-area'); const loading = document.getElementById('loading'); const answerDiv = document.getElementById('answer'); const sourcesDiv = document.getElementById('sources'); const metaBar = document.getElementById('meta-bar'); const newSearchBtn = document.getElementById('new-search-btn'); const btnText = submitBtn.querySelector('span').textContent; let isSearching = false; form.addEventListener('submit', async (e) => { e.preventDefault(); if (isSearching) return; const query = queryInput.value.trim(); if (!query) return; // Reset and show UI isSearching = true; responseArea.style.display = 'block'; loading.style.display = 'flex'; answerDiv.innerHTML = ''; sourcesDiv.innerHTML = ''; metaBar.innerHTML = ''; metaBar.style.display = 'none'; newSearchBtn.classList.remove('visible'); submitBtn.disabled = true; submitBtn.querySelector('span').textContent = 'Consultando...'; // Scroll to loading indicator on mobile loading.scrollIntoView({ behavior: 'smooth', block: 'start' }); try { const res = await fetch('/api/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Errore sconosciuto'); } const data = await res.json(); // Render answer — prefer server-rendered HTML, fall back to client-side markdown if (data.html_answer) { answerDiv.innerHTML = data.html_answer; } else { answerDiv.innerHTML = renderMarkdown(data.answer); } // Render metadata bar if (data.search_engine || data.tokens_used) { let metaHtml = ''; if (data.search_engine && data.search_engine !== 'none') { metaHtml += `🔍 ${data.search_engine}`; } if (data.model) { metaHtml += `🧠 ${data.model}`; } if (data.tokens_used) { metaHtml += `📊 ~${data.tokens_used} token`; } metaBar.innerHTML = metaHtml; metaBar.style.display = 'flex'; } // Render sources if (data.sources && data.sources.length > 0) { sourcesDiv.innerHTML = '

📚 Fonti

' + data.sources.map(s => ` ${s.index} ${escapeHtml(s.title)} ${escapeHtml(s.url)} ` ).join(''); } // Show "new search" button and scroll to results newSearchBtn.classList.add('visible'); answerDiv.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (err) { answerDiv.innerHTML = `
⚠️ ${escapeHtml(err.message)}
`; answerDiv.scrollIntoView({ behavior: 'smooth', block: 'start' }); } finally { loading.style.display = 'none'; submitBtn.disabled = false; submitBtn.querySelector('span').textContent = btnText; isSearching = false; } }); // "New search" button — scroll back to query input newSearchBtn.addEventListener('click', () => { queryInput.focus(); queryInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); // Allow Ctrl+Enter or Cmd+Enter to submit queryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { form.dispatchEvent(new Event('submit')); } }); }); // ── Tiny Markdown renderer ───────────────────────────────────── function renderMarkdown(text) { if (!text) return ''; let html = escapeHtml(text); // Headers html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Bold + italic html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/\*(.+?)\*/g, '$1'); // Inline code html = html.replace(/`(.+?)`/g, '$1'); // Links html = html.replace(/\[(.+?)\]\((.+?)\)/g, '$1'); // Paragraphs (double newlines) html = html.replace(/\n\n/g, '

'); html = '

' + html + '

'; html = html.replace(/

\s*<\/p>/g, ''); // Line breaks html = html.replace(/\n/g, '
'); return html; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }