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:
1 parent
f3fe13c60e
commit
76bc667adf
20 files changed
+3385
-245
No files matched your search
+55
-4
@@ -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 '';
|
||||
|
||||
|
||||
Reference in new issue
Block a user