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>
This commit is contained in:
+271
@@ -0,0 +1,271 @@
|
|||||||
|
<!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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
import chromadb
|
||||||
|
from PySide6.QtCore import Qt, QEvent, QUrl
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QComboBox, QTextEdit, QPushButton,
|
||||||
|
)
|
||||||
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
|
||||||
|
from gui.worker import RagWorker
|
||||||
|
|
||||||
|
_HERE = Path(__file__).parent
|
||||||
|
_PROJECT = _HERE.parent
|
||||||
|
_CHROMA_DIR = _PROJECT / "chroma_db"
|
||||||
|
_CHAT_HTML = _HERE / "chat.html"
|
||||||
|
|
||||||
|
# ── Temi ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_THEMES = {
|
||||||
|
"dark": {
|
||||||
|
"topbar": "background:#171717; border-bottom:1px solid #2e2e2e;",
|
||||||
|
"lbl": "color:#aaa; font-size:13px;",
|
||||||
|
"combo": """
|
||||||
|
QComboBox {
|
||||||
|
background:#2a2a2a; color:#ececec;
|
||||||
|
border:1px solid #3a3a3a; border-radius:6px;
|
||||||
|
padding:4px 10px; font-size:13px;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down { border:none; }
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background:#2a2a2a; color:#ececec;
|
||||||
|
selection-background-color:#3a3a3a;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"inputbar": "background:#171717; border-top:1px solid #2e2e2e;",
|
||||||
|
"input": """
|
||||||
|
QTextEdit {
|
||||||
|
background:#2a2a2a; color:#ececec;
|
||||||
|
border:1px solid #3a3a3a; border-radius:10px;
|
||||||
|
padding:8px 12px; font-size:14px;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"theme_btn": "background:#2a2a2a; color:#ececec; border:1px solid #3a3a3a; border-radius:6px; font-size:16px; padding:4px 8px;",
|
||||||
|
"icon": "☀️",
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"topbar": "background:#f5f5f5; border-bottom:1px solid #e0e0e0;",
|
||||||
|
"lbl": "color:#555; font-size:13px;",
|
||||||
|
"combo": """
|
||||||
|
QComboBox {
|
||||||
|
background:#ffffff; color:#1a1a1a;
|
||||||
|
border:1px solid #ddd; border-radius:6px;
|
||||||
|
padding:4px 10px; font-size:13px;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down { border:none; }
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background:#ffffff; color:#1a1a1a;
|
||||||
|
selection-background-color:#e8e8e8;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"inputbar": "background:#f5f5f5; border-top:1px solid #e0e0e0;",
|
||||||
|
"input": """
|
||||||
|
QTextEdit {
|
||||||
|
background:#ffffff; color:#1a1a1a;
|
||||||
|
border:1px solid #ddd; border-radius:10px;
|
||||||
|
padding:8px 12px; font-size:14px;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"theme_btn": "background:#fff; color:#1a1a1a; border:1px solid #ddd; border-radius:6px; font-size:16px; padding:4px 8px;",
|
||||||
|
"icon": "🌙",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_BTN_SEND = """
|
||||||
|
QPushButton {
|
||||||
|
background:#10a37f; color:#fff;
|
||||||
|
border:none; border-radius:8px; font-size:14px; font-weight:600;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background:#1ab58e; }
|
||||||
|
QPushButton:pressed { background:#0d8a6a; }
|
||||||
|
QPushButton:disabled { background:#2a2a2a; color:#555; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ChatWindow(QMainWindow):
|
||||||
|
def __init__(self, default_collection: str = "") -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("RAG Assistant")
|
||||||
|
self.resize(1000, 720)
|
||||||
|
|
||||||
|
self._dark = True
|
||||||
|
self._worker: RagWorker | None = None
|
||||||
|
self._collection: chromadb.Collection | None = None
|
||||||
|
self._chroma = chromadb.PersistentClient(path=str(_CHROMA_DIR))
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._load_collections(default_collection)
|
||||||
|
self._load_page()
|
||||||
|
|
||||||
|
# ── UI ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
root = QWidget()
|
||||||
|
self.setCentralWidget(root)
|
||||||
|
layout = QVBoxLayout(root)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Top bar
|
||||||
|
self._top = QWidget()
|
||||||
|
self._top.setFixedHeight(46)
|
||||||
|
tl = QHBoxLayout(self._top)
|
||||||
|
tl.setContentsMargins(14, 0, 14, 0)
|
||||||
|
tl.setSpacing(8)
|
||||||
|
|
||||||
|
self._lbl = QLabel("Collezione:")
|
||||||
|
tl.addWidget(self._lbl)
|
||||||
|
|
||||||
|
self._combo = QComboBox()
|
||||||
|
self._combo.setFixedWidth(260)
|
||||||
|
self._combo.currentTextChanged.connect(self._on_collection_changed)
|
||||||
|
tl.addWidget(self._combo)
|
||||||
|
|
||||||
|
tl.addStretch()
|
||||||
|
|
||||||
|
self._theme_btn = QPushButton()
|
||||||
|
self._theme_btn.setFixedSize(36, 30)
|
||||||
|
self._theme_btn.setToolTip("Cambia tema")
|
||||||
|
self._theme_btn.clicked.connect(self._toggle_theme)
|
||||||
|
tl.addWidget(self._theme_btn)
|
||||||
|
|
||||||
|
layout.addWidget(self._top)
|
||||||
|
|
||||||
|
# Web view
|
||||||
|
self._web = QWebEngineView()
|
||||||
|
s = self._web.settings()
|
||||||
|
s.setAttribute(s.WebAttribute.JavascriptEnabled, True)
|
||||||
|
s.setAttribute(s.WebAttribute.LocalContentCanAccessRemoteUrls, True)
|
||||||
|
layout.addWidget(self._web, stretch=1)
|
||||||
|
|
||||||
|
# Input bar
|
||||||
|
self._bar = QWidget()
|
||||||
|
bl = QHBoxLayout(self._bar)
|
||||||
|
bl.setContentsMargins(14, 10, 14, 12)
|
||||||
|
bl.setSpacing(10)
|
||||||
|
|
||||||
|
self._input = QTextEdit()
|
||||||
|
self._input.setPlaceholderText("Fai una domanda… (Invio per inviare, Shift+Invio per andare a capo)")
|
||||||
|
self._input.setFixedHeight(68)
|
||||||
|
self._input.installEventFilter(self)
|
||||||
|
bl.addWidget(self._input, stretch=1)
|
||||||
|
|
||||||
|
self._btn = QPushButton("Invia")
|
||||||
|
self._btn.setFixedSize(76, 42)
|
||||||
|
self._btn.setStyleSheet(_BTN_SEND)
|
||||||
|
self._btn.clicked.connect(self._on_send)
|
||||||
|
bl.addWidget(self._btn)
|
||||||
|
|
||||||
|
layout.addWidget(self._bar)
|
||||||
|
|
||||||
|
self._apply_theme()
|
||||||
|
|
||||||
|
def _apply_theme(self) -> None:
|
||||||
|
t = _THEMES["dark" if self._dark else "light"]
|
||||||
|
self._top.setStyleSheet(t["topbar"])
|
||||||
|
self._lbl.setStyleSheet(t["lbl"])
|
||||||
|
self._combo.setStyleSheet(t["combo"])
|
||||||
|
self._bar.setStyleSheet(t["inputbar"])
|
||||||
|
self._input.setStyleSheet(t["input"])
|
||||||
|
self._theme_btn.setStyleSheet(t["theme_btn"])
|
||||||
|
self._theme_btn.setText(t["icon"])
|
||||||
|
|
||||||
|
# ── Init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_collections(self, default: str) -> None:
|
||||||
|
names = sorted(c.name for c in self._chroma.list_collections())
|
||||||
|
self._combo.addItems(names)
|
||||||
|
if default and default in names:
|
||||||
|
self._combo.setCurrentText(default)
|
||||||
|
|
||||||
|
def _load_page(self) -> None:
|
||||||
|
self._web.setUrl(QUrl.fromLocalFile(str(_CHAT_HTML)))
|
||||||
|
|
||||||
|
# ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event) -> bool:
|
||||||
|
if obj is self._input and event.type() == QEvent.Type.KeyPress:
|
||||||
|
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||||
|
if not (event.modifiers() & Qt.KeyboardModifier.ShiftModifier):
|
||||||
|
self._on_send()
|
||||||
|
return True
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
def _on_collection_changed(self, name: str) -> None:
|
||||||
|
if name:
|
||||||
|
self._collection = self._chroma.get_collection(name)
|
||||||
|
|
||||||
|
def _toggle_theme(self) -> None:
|
||||||
|
self._dark = not self._dark
|
||||||
|
self._apply_theme()
|
||||||
|
theme = "dark" if self._dark else "light"
|
||||||
|
self._js(f"setTheme({json.dumps(theme)})")
|
||||||
|
|
||||||
|
def _on_send(self) -> None:
|
||||||
|
question = self._input.toPlainText().strip()
|
||||||
|
if not question:
|
||||||
|
return
|
||||||
|
if not self._collection:
|
||||||
|
self._js("addMessage('assistant', '⚠️ Seleziona una collezione prima di inviare.', [])")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._input.clear()
|
||||||
|
self._set_enabled(False)
|
||||||
|
self._js(f"addMessage('user', {json.dumps(question)}, [])")
|
||||||
|
self._js("addThinking()")
|
||||||
|
|
||||||
|
self._worker = RagWorker(question, self._collection, parent=self)
|
||||||
|
self._worker.finished.connect(self._on_response)
|
||||||
|
self._worker.error.connect(self._on_error)
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
|
def _on_response(self, response: str, chunks: list) -> None:
|
||||||
|
self._js("removeThinking()")
|
||||||
|
sources = [
|
||||||
|
{"source": c.get("source", ""), "header_path": c.get("header_path", "")}
|
||||||
|
for c in chunks
|
||||||
|
]
|
||||||
|
self._js(f"addMessage('assistant', {json.dumps(response)}, {json.dumps(sources)})")
|
||||||
|
self._set_enabled(True)
|
||||||
|
|
||||||
|
def _on_error(self, msg: str) -> None:
|
||||||
|
self._js("removeThinking()")
|
||||||
|
self._js(f"addMessage('assistant', {json.dumps('❌ Errore: ' + msg)}, [])")
|
||||||
|
self._set_enabled(True)
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _js(self, script: str) -> None:
|
||||||
|
self._web.page().runJavaScript(script)
|
||||||
|
|
||||||
|
def _set_enabled(self, enabled: bool) -> None:
|
||||||
|
self._input.setEnabled(enabled)
|
||||||
|
self._btn.setEnabled(enabled)
|
||||||
|
if enabled:
|
||||||
|
self._input.setFocus()
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
RAG Assistant — GUI desktop.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python gui/main.py
|
||||||
|
python gui/main.py --collection matematica
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from PySide6.QtGui import QPalette, QColor
|
||||||
|
from gui.chat_window import ChatWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="RAG Assistant GUI")
|
||||||
|
parser.add_argument("--collection", default="", help="Collection ChromaDB da aprire")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("RAG Assistant")
|
||||||
|
app.setStyle("Fusion")
|
||||||
|
|
||||||
|
# Palette scura per i widget nativi Qt
|
||||||
|
palette = QPalette()
|
||||||
|
palette.setColor(QPalette.ColorRole.Window, QColor("#212121"))
|
||||||
|
palette.setColor(QPalette.ColorRole.WindowText, QColor("#ececec"))
|
||||||
|
palette.setColor(QPalette.ColorRole.Base, QColor("#2a2a2a"))
|
||||||
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#1a1a1a"))
|
||||||
|
palette.setColor(QPalette.ColorRole.Text, QColor("#ececec"))
|
||||||
|
palette.setColor(QPalette.ColorRole.Button, QColor("#2a2a2a"))
|
||||||
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#ececec"))
|
||||||
|
palette.setColor(QPalette.ColorRole.Highlight, QColor("#10a37f"))
|
||||||
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
||||||
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
window = ChatWindow(default_collection=args.collection)
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
import chromadb
|
||||||
|
|
||||||
|
|
||||||
|
class RagWorker(QThread):
|
||||||
|
finished = Signal(str, list) # response, chunks
|
||||||
|
error = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, question: str, collection: chromadb.Collection, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.question = question
|
||||||
|
self.collection = collection
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
try:
|
||||||
|
from rag.rag import retrieve, build_prompt, call_ollama
|
||||||
|
chunks = retrieve(self.collection, self.question)
|
||||||
|
system, prompt = build_prompt(self.question, chunks)
|
||||||
|
response = call_ollama(prompt, system=system)
|
||||||
|
self.finished.emit(response, chunks)
|
||||||
|
except Exception as exc:
|
||||||
|
self.error.emit(str(exc))
|
||||||
Reference in New Issue
Block a user