feat(rag): PDF/docx ingest + chat citations

- Upload endpoint (base64 JSON, no multipart dep): extracts text from
  pdf/docx/txt/md via pypdf + python-docx, then runs the existing
  chunk/embed pipeline. Documents page uploads files straight through.
- Citations: the chat stream emits an SSE `sources` event listing the
  documents that fed the answer; the UI shows them as chips under the reply.
- Deps: pypdf, python-docx (both pure-Python, Windows-safe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-23 13:48:32 -05:00
co-authored by Claude Opus 4.8
parent f509034fdd
commit ac0eb1e5f6
6 changed files with 123 additions and 9 deletions
+22
View File
@@ -215,6 +215,18 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
pendingEventType = null;
continue;
}
if (pendingEventType === "sources") {
try {
const src = JSON.parse(payload).sources;
setMessages(prev => {
const updated = [...prev];
updated[assistantIndex] = { ...updated[assistantIndex], sources: src };
return updated;
});
} catch { /* ignore */ }
pendingEventType = null;
continue;
}
if (pendingEventType === "done") {
// Answer is complete; re-enable input while the backend finishes
// slow post-processing (title, memory) on the still-open stream.
@@ -526,6 +538,16 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
: <span style={{ color: "#aaa" }}>Thinking...</span>
}
</div>
{msg.sources && msg.sources.length > 0 && (
<div style={{ marginTop: "0.35rem", display: "flex", flexWrap: "wrap", gap: "0.35rem", alignItems: "center" }}>
<span style={{ fontSize: "0.7rem", color: "#777" }}>Sources:</span>
{msg.sources.map((s, i) => (
<span key={i} style={{ fontSize: "0.72rem", color: "#8ab4ff", background: "#1a2433", border: "1px solid #2a3a52", borderRadius: "6px", padding: "0.1rem 0.45rem" }}>
📄 {s}
</span>
))}
</div>
)}
{msg.content && editingIdx !== idx && (
<div style={{ display: "flex", gap: "0.25rem", marginTop: "0.25rem" }}>
<button