Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/requirements_fixed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
langchain==0.3.0
langchain-community==0.3.0
ollama==0.3.0
chromadb==0.5.3
sentence-transformers==3.0.0
pypdf==4.3.0
python-docx==1.1.2
python-multipart==0.0.9
pydantic==2.9.0
python-dotenv==1.0.1
httpx==0.27.0
pytest==8.3.0
pytest-asyncio==0.24.0
6 changes: 6 additions & 0 deletions backend/routes/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,9 @@ async def rag_stats(session_id: str):
count = 0
return {"session_id": session_id, "indexed_chunks": count}


@router.delete("/")
async def clear_all_sessions():
db_service.clear_all_sessions()
return {"message": "All sessions cleared"}

6 changes: 6 additions & 0 deletions backend/services/db_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ def delete_session(session_id: str):
conn.execute("DELETE FROM sessions WHERE id=?", (session_id,))


def clear_all_sessions():
with get_db() as conn:
conn.execute("DELETE FROM messages")
conn.execute("DELETE FROM sessions")


def get_all_sessions() -> list[dict]:
with get_db() as conn:
rows = conn.execute(
Expand Down
17 changes: 17 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def test_export_txt():
assert r2.status_code == 200
assert b"Plain text export" in r2.content


# ─── Prompt Templates ────────────────────────────────────────
def test_create_prompt_template():
r = client.post("/api/prompt-templates/", json={
Expand Down Expand Up @@ -281,3 +282,19 @@ def test_create_prompt_template_empty_title():
"prompt": "Some prompt"
})
assert r.status_code == 422

def test_clear_all_sessions():
r1 = client.post("/api/sessions/", json={"title": "Session 1"})
r2 = client.post("/api/sessions/", json={"title": "Session 2"})
assert r1.status_code == 200
assert r2.status_code == 200

r_delete = client.delete("/api/sessions/")
assert r_delete.status_code == 200
assert r_delete.json() == {"message": "All sessions cleared"}

r_list = client.get("/api/sessions/")
assert r_list.status_code == 200
assert len(r_list.json()) == 0


29 changes: 23 additions & 6 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,24 @@ export default function App() {
if (settRes.value.default_language) setLanguage(settRes.value.default_language);
}
if (stRes.status === "fulfilled") setOllamaOk(stRes.value.ollama_running);
} catch {}
} catch { }
}

const refreshSessions = useCallback(async () => {
try { const s = await api.getSessions(); setSessions(s || []); } catch {}
try { const s = await api.getSessions(); setSessions(s || []); } catch { }
}, []);

const refreshDocuments = useCallback(async (sid) => {
try { const d = await api.getDocuments(sid); setDocuments(d.documents || []); } catch {}
try { const d = await api.getDocuments(sid); setDocuments(d.documents || []); } catch { }
}, []);

async function sendMessage(text) {
if (!text.trim() || loading || streaming) return;
let activeSid = sessionId;
if (!activeSid) {
activeSid = uuidv4();
setSessionId(activeSid);
}
const userMsg = { role: "user", content: text, id: Date.now() };
setMessages(prev => [...prev, userMsg]);

Expand All @@ -75,7 +80,7 @@ export default function App() {
setMessages(prev => [...prev, aiMsg]);
try {
await api.streamMessage(
{ message: text, session_id: sessionId, model, use_documents: documents.length > 0, language },
{ message: text, session_id: activeSid, model, use_documents: documents.length > 0, language },
(token) => setMessages(prev => prev.map(m => m.id === aiMsg.id ? { ...m, content: m.content + token } : m)),
(sources) => {
setMessages(prev => prev.map(m => m.id === aiMsg.id ? { ...m, sources, streaming: false } : m));
Expand All @@ -88,7 +93,7 @@ export default function App() {
} else {
setLoading(true);
try {
const data = await api.sendMessage({ message: text, session_id: sessionId, model, use_documents: documents.length > 0, language });
const data = await api.sendMessage({ message: text, session_id: activeSid, model, use_documents: documents.length > 0, language });
setMessages(prev => [...prev, { role: "assistant", content: data.reply, sources: data.sources || [], id: Date.now() + 1 }]);
refreshSessions();
} catch (e) {
Expand Down Expand Up @@ -116,7 +121,7 @@ export default function App() {
const [msgRes, docRes] = await Promise.all([api.getMessages(sid), api.getDocuments(sid)]);
setMessages((msgRes.messages || []).map((m, i) => ({ ...m, id: i })));
setDocuments(docRes.documents || []);
} catch {}
} catch { }
}

async function handleDeleteSession(sid) {
Expand All @@ -125,6 +130,17 @@ export default function App() {
refreshSessions();
}

async function handleClearAllSessions() {
try {
await api.clearAllSessions();
setSessions([]);
setSessionId(null);
setMessages([]);
setDocuments([]);
setPanel(null);
} catch { }
}

async function handleClearChat() {
await api.clearMessages(sessionId);
setMessages([]);
Expand All @@ -138,6 +154,7 @@ export default function App() {
onNewChat={newChat}
onLoadSession={loadSession}
onDeleteSession={handleDeleteSession}
onClearAllSessions={handleClearAllSessions}
model={model}
models={models}
onModelChange={setModel}
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const LANGUAGES = [
{code:"de",label:"Deutsch"},{code:"es",label:"Español"},
];

export default function Sidebar({ sessions, currentSession, onNewChat, onLoadSession, onDeleteSession, model, models, onModelChange, language, onLanguageChange }) {
export default function Sidebar({ sessions, currentSession, onNewChat, onLoadSession, onDeleteSession, onClearAllSessions, model, models, onModelChange, language, onLanguageChange }) {
const [search, setSearch] = useState("");
const modelList = models.length > 0 ? models.map(m=>m.name) : ["llama3","mistral","phi3","gemma2"];
const filtered = sessions.filter(s => s.title?.toLowerCase().includes(search.toLowerCase()));
Expand Down Expand Up @@ -83,6 +83,22 @@ export default function Sidebar({ sessions, currentSession, onNewChat, onLoadSes
))}
</div>

{sessions.length > 0 && (
<div className="px-3 py-2 border-t border-gray-800 shrink-0">
<button
onClick={() => {
if (window.confirm("Delete all sessions? This cannot be undone.")) {
onClearAllSessions();
}
}}
className="w-full text-left text-xs text-gray-500 hover:text-red-400 hover:bg-red-950/20 px-3 py-2 rounded-lg transition inline-flex items-center gap-2 font-medium"
>
<span>🗑</span>
<span>Clear all sessions</span>
</button>
</div>
)}

{/* Footer */}
<div className="px-4 py-3 border-t border-gray-800">
<p className="text-xs text-gray-600 inline-flex items-center gap-1">
Expand Down
35 changes: 18 additions & 17 deletions frontend/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ async function req(path, opts = {}) {
return res.json();
}

export const sendMessage = (b) => req("/chat/", { method: "POST", body: JSON.stringify(b) });
export const getSessions = () => req("/sessions/");
export const createSession = (b) => req("/sessions/", { method: "POST", body: JSON.stringify(b) });
export const updateSession = (id,b) => req(`/sessions/${id}`, { method: "PATCH", body: JSON.stringify(b) });
export const deleteSession = (id) => req(`/sessions/${id}`, { method: "DELETE" });
export const getMessages = (id) => req(`/sessions/${id}/messages`);
export const clearMessages = (id) => req(`/sessions/${id}/messages`, { method: "DELETE" });
export const getDocuments = (id) => req(`/sessions/${id}/documents`);
export const getModels = () => req("/models/");
export const getOllamaStatus= () => req("/models/status");
export const getPlugins = () => req("/plugins/");
export const runPlugin = (b) => req("/plugins/run", { method: "POST", body: JSON.stringify(b) });
export const getSettings = () => req("/settings/");
export const saveSettings = (b) => req("/settings/", { method: "PUT", body: JSON.stringify(b) });
export const exportSession = (id, fmt) => window.open(`${BASE}/export/${id}/${fmt}`, "_blank");
export const sendMessage = (b) => req("/chat/", { method: "POST", body: JSON.stringify(b) });
export const getSessions = () => req("/sessions/");
export const createSession = (b) => req("/sessions/", { method: "POST", body: JSON.stringify(b) });
export const updateSession = (id, b) => req(`/sessions/${id}`, { method: "PATCH", body: JSON.stringify(b) });
export const deleteSession = (id) => req(`/sessions/${id}`, { method: "DELETE" });
export const clearAllSessions = () => req("/sessions/", { method: "DELETE" });
export const getMessages = (id) => req(`/sessions/${id}/messages`);
export const clearMessages = (id) => req(`/sessions/${id}/messages`, { method: "DELETE" });
export const getDocuments = (id) => req(`/sessions/${id}/documents`);
export const getModels = () => req("/models/");
export const getOllamaStatus = () => req("/models/status");
export const getPlugins = () => req("/plugins/");
export const runPlugin = (b) => req("/plugins/run", { method: "POST", body: JSON.stringify(b) });
export const getSettings = () => req("/settings/");
export const saveSettings = (b) => req("/settings/", { method: "PUT", body: JSON.stringify(b) });
export const exportSession = (id, fmt) => window.open(`${BASE}/export/${id}/${fmt}`, "_blank");
export const deleteDocument = (docId) => req(`/upload/${docId}`, { method: "DELETE" });

// Prompt Templates
Expand All @@ -39,7 +40,7 @@ export async function uploadDocument(file, session_id) {
const fd = new FormData();
fd.append("file", file); fd.append("session_id", session_id);
const res = await fetch(`${BASE}/upload/`, { method: "POST", body: fd });
if (!res.ok) { const e = await res.json().catch(()=>({})); throw new Error(e.detail||"Upload failed"); }
if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Upload failed"); }
return res.json();
}

Expand All @@ -54,7 +55,7 @@ export function streamMessage(body, onToken, onDone) {
if (done) return;
decoder.decode(value).split("\n").forEach(line => {
if (line.startsWith("data: ")) {
try { const d = JSON.parse(line.slice(6)); if (d.token) onToken(d.token); if (d.done) onDone(d.sources||[]); } catch {}
try { const d = JSON.parse(line.slice(6)); if (d.token) onToken(d.token); if (d.done) onDone(d.sources || []); } catch { }
}
});
return pump();
Expand Down