Files
rag-from-scratch/gui/chat.html
T
davide f947131f16 feat(gui): aggiunge interfaccia desktop PySide6 con tema light/dark
- chat con Markdown e formule KaTeX (via QWebEngineView + CDN)
- top bar con selezione collezione ChromaDB e bottone ☀️/🌙
- input multiriga con Enter per inviare e Shift+Enter per andare a capo
- animazione thinking durante la generazione
- fonti dei chunk mostrate sotto ogni risposta

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:50:37 +02:00

272 lines
8.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body, {delimiters: KATEX_DELIMITERS, throwOnError: false})"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@13.0.2/marked.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Temi ─────────────────────────────────────────────────────── */
:root {
--bg: #212121;
--text: #ececec;
--bubble-user: #2f2f2f;
--code-bg: #1a1a1a;
--code-border: #333;
--inline-code-bg: #2a2a2a;
--table-border: #3a3a3a;
--table-th: #2a2a2a;
--table-even: #252525;
--source-border: #2e2e2e;
--source-color: #6e6e80;
--chip-bg: #2a2a2a;
--chip-border: #333;
--src-doc: #888;
--src-path: #aaa;
--dot-color: #6e6e80;
--scrollbar-track: #1a1a1a;
--scrollbar-thumb: #3a3a3a;
}
body.light {
--bg: #ffffff;
--text: #1a1a1a;
--bubble-user: #efefef;
--code-bg: #f5f5f5;
--code-border: #ddd;
--inline-code-bg: #ebebeb;
--table-border: #e0e0e0;
--table-th: #f0f0f0;
--table-even: #fafafa;
--source-border: #e8e8e8;
--source-color: #888;
--chip-bg: #f5f5f5;
--chip-border: #ddd;
--src-doc: #666;
--src-path: #444;
--dot-color: #aaa;
--scrollbar-track: #f0f0f0;
--scrollbar-thumb: #ccc;
}
/* ── Base ─────────────────────────────────────────────────────── */
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 15px;
line-height: 1.65;
transition: background 0.2s, color 0.2s;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
#chat {
max-width: 820px;
margin: 0 auto;
padding: 24px 20px 40px;
}
.message { margin: 20px 0; }
/* ── Utente ────────────────────────────────────────────────────── */
.user { display: flex; justify-content: flex-end; }
.user .bubble {
background: var(--bubble-user);
border-radius: 18px 18px 4px 18px;
padding: 10px 16px;
max-width: 72%;
white-space: pre-wrap;
word-break: break-word;
}
/* ── Assistente ────────────────────────────────────────────────── */
.assistant .bubble { max-width: 100%; }
.assistant .bubble p { margin: 0.5em 0; }
.assistant .bubble p:first-child { margin-top: 0; }
.assistant .bubble p:last-child { margin-bottom: 0; }
.assistant .bubble h1,
.assistant .bubble h2,
.assistant .bubble h3 { margin: 1em 0 0.4em; font-weight: 600; }
.assistant .bubble ol,
.assistant .bubble ul { padding-left: 1.4em; margin: 0.5em 0; }
.assistant .bubble li { margin: 0.25em 0; }
.assistant .bubble pre {
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
margin: 0.8em 0;
}
.assistant .bubble code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.88em;
}
.assistant .bubble :not(pre) > code {
background: var(--inline-code-bg);
border-radius: 4px;
padding: 2px 5px;
}
.assistant .bubble table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
font-size: 0.92em;
}
.assistant .bubble th,
.assistant .bubble td {
border: 1px solid var(--table-border);
padding: 7px 12px;
text-align: left;
}
.assistant .bubble th { background: var(--table-th); font-weight: 600; }
.assistant .bubble tr:nth-child(even) td { background: var(--table-even); }
.assistant .bubble .katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 6px 0;
}
/* ── Fonti ─────────────────────────────────────────────────────── */
.sources {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--source-border);
font-size: 0.78em;
color: var(--source-color);
}
.source-item {
display: inline-block;
background: var(--chip-bg);
border: 1px solid var(--chip-border);
border-radius: 6px;
padding: 2px 8px;
margin: 2px 4px 2px 0;
cursor: default;
}
.source-item .src-doc { color: var(--src-doc); }
.source-item .src-path { color: var(--src-path); }
/* ── Thinking ──────────────────────────────────────────────────── */
.thinking .bubble { display: flex; align-items: center; gap: 5px; height: 28px; }
.thinking .dot {
width: 7px; height: 7px;
background: var(--dot-color);
border-radius: 50%;
animation: bounce 1.3s infinite ease-in-out;
}
.thinking .dot:nth-child(2) { animation-delay: 0.2s; }
.thinking .dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
40% { transform: translateY(-6px); opacity: 1; }
}
</style>
</head>
<body>
<div id="chat"></div>
<script>
const KATEX_DELIMITERS = [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false}
];
marked.setOptions({ breaks: true, gfm: true });
function setTheme(theme) {
document.body.classList.toggle('light', theme === 'light');
}
function addMessage(role, content, sources) {
const chat = document.getElementById('chat');
const wrap = document.createElement('div');
wrap.className = 'message ' + role;
const bubble = document.createElement('div');
bubble.className = 'bubble';
if (role === 'user') {
bubble.textContent = content;
} else {
bubble.innerHTML = marked.parse(content);
if (sources && sources.length > 0) {
const srcDiv = document.createElement('div');
srcDiv.className = 'sources';
const seen = new Set();
sources.forEach(s => {
const key = s.source + '|' + s.header_path;
if (seen.has(key)) return;
seen.add(key);
const chip = document.createElement('span');
chip.className = 'source-item';
const doc = s.source || '';
const path = s.header_path || '';
chip.innerHTML = path
? `<span class="src-doc">${esc(doc)}</span> <span class="src-path">${esc(path)}</span>`
: `<span class="src-doc">${esc(doc)}</span>`;
srcDiv.appendChild(chip);
});
bubble.appendChild(srcDiv);
}
renderMathInElement(bubble, { delimiters: KATEX_DELIMITERS, throwOnError: false });
}
wrap.appendChild(bubble);
chat.appendChild(wrap);
window.scrollTo(0, document.body.scrollHeight);
}
function addThinking() {
const chat = document.getElementById('chat');
const wrap = document.createElement('div');
wrap.id = 'thinking-msg';
wrap.className = 'message assistant thinking';
const bubble = document.createElement('div');
bubble.className = 'bubble';
[1,2,3].forEach(() => {
const d = document.createElement('div');
d.className = 'dot';
bubble.appendChild(d);
});
wrap.appendChild(bubble);
chat.appendChild(wrap);
window.scrollTo(0, document.body.scrollHeight);
}
function removeThinking() {
const el = document.getElementById('thinking-msg');
if (el) el.remove();
}
function esc(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
</body>
</html>