import { useEffect, useState } from "react"; import { API_BASE } from "./config"; // RAG document manager: upload/paste text, chunked + embedded server-side, then // retrieved into the chat system prompt. See synapse/memory/store.py. export function Documents() { const [docs, setDocs] = useState([]); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [viewing, setViewing] = useState(null); // {title, chunks} being previewed const openDoc = async (doc) => { try { const r = await fetch(`${API_BASE}/documents/${encodeURIComponent(doc.doc_id)}`); if (r.ok) setViewing({ title: doc.title, chunks: (await r.json()).chunks || [] }); } catch { /* ignore */ } }; const load = async () => { try { const r = await fetch(`${API_BASE}/documents`); if (r.ok) setDocs((await r.json()).documents || []); } catch { /* offline — leave list as-is */ } }; useEffect(() => { load(); }, []); // Files (pdf/docx/txt/md) upload straight to the server, which extracts the // text. Base64 in JSON — no multipart dependency. const onFile = (e) => { const file = e.target.files?.[0]; e.target.value = ""; // allow re-selecting the same file if (!file) return; setBusy(true); setMessage(`Reading ${file.name}…`); const reader = new FileReader(); reader.onload = async () => { const b64 = String(reader.result || "").split(",")[1]; try { const r = await fetch(`${API_BASE}/documents/upload`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: file.name, data: b64 }), }); if (r.ok) { const d = await r.json(); setMessage(`Indexed "${d.title}" (${d.chunks} chunk${d.chunks === 1 ? "" : "s"}).`); load(); } else { setMessage((await r.json().catch(() => ({}))).detail || "Failed to index file."); } } catch (err) { setMessage(`Error: ${err.message}`); } finally { setBusy(false); } }; reader.readAsDataURL(file); }; const addDoc = async () => { if (!title.trim() || !content.trim()) { setMessage("Title and content are required."); return; } setBusy(true); setMessage("Chunking and embedding…"); try { const r = await fetch(`${API_BASE}/documents`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: title.trim(), content }), }); if (r.ok) { const d = await r.json(); setMessage(`Indexed "${d.title}" (${d.chunks} chunk${d.chunks === 1 ? "" : "s"}).`); setTitle(""); setContent(""); load(); } else { setMessage((await r.json().catch(() => ({}))).detail || "Failed to index."); } } catch (err) { setMessage(`Error: ${err.message}`); } finally { setBusy(false); } }; const removeDoc = async (docId) => { try { await fetch(`${API_BASE}/documents/${encodeURIComponent(docId)}`, { method: "DELETE" }); load(); } catch { /* ignore */ } }; const input = { padding: "0.9rem", background: "#222", color: "#eee", border: "1px solid #333", borderRadius: "10px", width: "100%", boxSizing: "border-box" }; return (

📄 Documents

Upload or paste text. It's chunked, embedded, and pulled into chat as source material when a message is relevant.

setTitle(e.target.value)} style={input} />