feat: v0.2.0 — API REST, TTS support, documentazione completa e UI mobile-first

✨ Nuove funzionalità:
- Nuovo endpoint API REST /api/v1/query (GET+POST) per consumo programmatico
- Campo speakable_text ottimizzato per sintesi vocale (Alexa, Google Home)
- Funzione strip_markdown() per conversione Markdown → plain text TTS
- Supporto CORS per accesso cross-origin
- UI responsive mobile-first con micro-interazioni e stati
- Nuovo pulsante 'Nuova ricerca' e metadata bar

📚 Documentazione completa:
- docs/ARCHITECTURE.md — architettura di sistema, stack, componenti
- docs/FLOW.md — diagrammi Mermaid della pipeline e flussi
- docs/API.md — documentazione endpoint REST con esempi curl
- docs/WIKI.md — FAQ, glossario, roadmap
- docs/DEPLOYMENT.md — guida deployment (Gunicorn, Nginx, Docker, systemd)
- docs/CONTRIBUTING.md — guida alla contribuzione

🔧 Migliorie tecniche:
- Pipeline di ricerca condivisa in _run_search_pipeline()
- Gestione errori Gemini migliorata con _clean_error()
- Sanitizzazione HTML con allowlist per prevenzione XSS
- .env.example completo con commenti

🧪 Test: 109/109 passati
This commit is contained in:
Sibilla committed 2026-07-18 10:05:46 +00:00
1 parent f3fe13c60e
commit 76bc667adf
20 files changed
+3385 -245

No files matched your search

+55 -4
View File
@@ -1,5 +1,6 @@
/**
* Sibilla — Frontend search interface
* Mobile-friendly, smooth UX
*/
document.addEventListener('DOMContentLoaded', () => {
@@ -10,19 +11,34 @@ document.addEventListener('DOMContentLoaded', () => {
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;
// Show response area + loading
// 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', {
@@ -38,29 +54,63 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await res.json();
// Render answer as Markdown
answerDiv.innerHTML = renderMarkdown(data.answer);
// 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 += `<span class="meta-badge engine">🔍 ${data.search_engine}</span>`;
}
if (data.model) {
metaHtml += `<span class="meta-badge">🧠 ${data.model}</span>`;
}
if (data.tokens_used) {
metaHtml += `<span class="meta-badge">📊 ~${data.tokens_used} token</span>`;
}
metaBar.innerHTML = metaHtml;
metaBar.style.display = 'flex';
}
// Render sources
if (data.sources && data.sources.length > 0) {
sourcesDiv.innerHTML = '<h3>📚 Fonti</h3>' +
data.sources.map(s =>
`<a href="${escapeHtml(s.url)}" target="_blank" rel="noopener" class="source-item">
<span class="source-index">${s.index}</span>
<span class="source-title">${escapeHtml(s.title)}</span>
<span class="source-url">${escapeHtml(s.url)}</span>
</a>`
).join('');
}
// Show "new search" button and scroll to results
newSearchBtn.classList.add('visible');
answerDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (err) {
answerDiv.innerHTML = `<div class="error-box">⚠️ ${escapeHtml(err.message)}</div>`;
answerDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
} finally {
loading.style.display = 'none';
submitBtn.disabled = false;
submitBtn.querySelector('span').textContent = btnText;
isSearching = false;
}
});
// Allow Ctrl+Enter to submit
// "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'));
@@ -69,6 +119,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
// ── Tiny Markdown renderer ─────────────────────────────────────
function renderMarkdown(text) {
if (!text) return '';