import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { Binary, Bot, BookOpen, ChevronDown, Cpu, Database, FileText, Layers, Loader2, MessageSquare, RefreshCw, RotateCcw, ScanText, Scissors, Search, Send, Sparkles, Upload, Workflow, Wrench } from 'lucide-react' import { cn } from '../../lib/utils' import { subTabActive, subTabIdle } from '../../lib/tabActive' type Collection = { name: string; documents: number; files?: number; filenames?: string[] } type StoredDoc = { id: string filename: string collection: string chunks: number characters?: number ingested_at: string bytes?: number } type Source = { source?: string; chunk?: number; preview?: string } type AgentStep = { tool: string; input?: Record } type ChatMsg = { role: 'user' | 'assistant'; content: string; sources?: Source[]; steps?: AgentStep[] } type Health = { ok: boolean; chroma: boolean; docling: boolean; llm: boolean; embed_model?: string } type Props = { onGpuActivity?: (active: boolean) => void } export function KnowledgeChatView({ onGpuActivity }: Props = {}) { const [health, setHealth] = useState(null) const [collections, setCollections] = useState([]) const [collection, setCollection] = useState('default') const [newCol, setNewCol] = useState('') const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [ingesting, setIngesting] = useState(false) const [error, setError] = useState(null) const [storedDocs, setStoredDocs] = useState([]) const [selectedDocId, setSelectedDocId] = useState(null) const [summarizing, setSummarizing] = useState(false) const [reindexing, setReindexing] = useState(null) const [agentMode, setAgentMode] = useState(false) const [syncing, setSyncing] = useState(false) const [catalogDocs, setCatalogDocs] = useState(null) const [showArch, setShowArch] = useState(true) const [activeStage, setActiveStage] = useState(null) const bottomRef = useRef(null) const loadMeta = useCallback(async () => { try { const [h, c, d] = await Promise.all([ fetch('/rag/health'), fetch('/rag/collections'), fetch('/rag/documents'), ]) if (h.ok) setHealth(await h.json()) if (c.ok) { const j = await c.json() setCollections(j.collections || []) } if (d.ok) { const j = await d.json() setStoredDocs(j.documents || []) } } catch { setHealth(null) } }, []) const loadCatalog = useCallback(async () => { try { const r = await fetch('/rag/catalog/status') if (r.ok) setCatalogDocs((await r.json()).documents ?? 0) } catch { setCatalogDocs(null) } }, []) useEffect(() => { loadMeta() loadCatalog() }, [loadMeta, loadCatalog]) useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, loading]) useEffect(() => { onGpuActivity?.(loading || ingesting || summarizing || reindexing !== null) }, [loading, ingesting, summarizing, reindexing, onGpuActivity]) const onIngest = async (file: File) => { setIngesting(true) setError(null) const fd = new FormData() fd.append('file', file) fd.append('collection', collection) try { const r = await fetch('/rag/ingest', { method: 'POST', body: fd }) const j = await r.json() if (!r.ok || !j.ok) { setError(j.error || 'Ingest failed') return } setMessages((m) => [...m, { role: 'assistant', content: j.duplicate ? `Already indexed: ${j.filename} (${j.chunks} chunks). You can chat immediately — no re-upload needed.` : `Indexed ${j.filename} → collection "${j.collection}" — ${j.chunks} chunks (${j.characters?.toLocaleString()} chars). Stored permanently.`, }]) loadMeta() } catch { setError('RAG API unavailable') } finally { setIngesting(false) } } const onSummarize = async (doc: StoredDoc) => { setSummarizing(true) setError(null) setSelectedDocId(doc.id) setCollection(doc.collection) setMessages((m) => [...m, { role: 'user', content: `Summarize: ${doc.filename}` }]) try { const r = await fetch('/rag/summarize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ collection: doc.collection, doc_id: doc.id }), }) const j = await r.json() if (!r.ok || !j.ok) { setError(j.error || 'Summarize failed') return } setMessages((m) => [...m, { role: 'assistant', content: `Summary of ${j.filename} (${j.characters?.toLocaleString()} chars):\n\n${j.summary}`, }]) } catch { setError('Summarize request failed') } finally { setSummarizing(false) } } const onReindex = async (doc: StoredDoc) => { setReindexing(doc.id) setError(null) try { const r = await fetch(`/rag/documents/${doc.id}/reindex`, { method: 'POST' }) const j = await r.json() if (!r.ok || !j.ok) { setError(j.error || 'Re-index failed') return } setMessages((m) => [...m, { role: 'assistant', content: `Re-indexed ${j.filename}: ${j.chunks} clean text chunks (${j.characters?.toLocaleString()} chars). You can now chat and summarize.`, }]) loadMeta() } catch { setError('Re-index request failed') } finally { setReindexing(null) } } const onSyncCatalog = async () => { setSyncing(true) setError(null) try { const r = await fetch('/rag/catalog/sync', { method: 'POST' }) const j = await r.json() if (!r.ok || !j.ok) { setError(j.error || 'Catalog sync failed') return } const c = j.counts || {} setMessages((m) => [...m, { role: 'assistant', content: `Catalog synced — ${j.documents} entries (${c.tables ?? 0} tables, ${c.pii_datasets ?? 0} PII datasets, ${c.flows ?? 0} lineage edges, ${c.movements ?? 0} movements). Agent mode can now answer catalog/PII/lineage questions.`, }]) loadCatalog() } catch { setError('Catalog sync request failed') } finally { setSyncing(false) } } const onSendAgent = async (msg: string) => { setLoading(true) setActiveStage(0) const steps: AgentStep[] = [] let answer = '' let idx = -1 setMessages((m) => { idx = m.length return [...m, { role: 'assistant', content: '', steps: [] }] }) const patch = () => setMessages((m) => m.map((x, i) => (i === idx ? { ...x, content: answer, steps: [...steps] } : x))) try { const r = await fetch('/rag/agent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg, max_steps: 5 }), }) if (!r.ok || !r.body) { setError('Agent unavailable') return } const reader = r.body.getReader() const decoder = new TextDecoder() let buf = '' for (;;) { const { done, value } = await reader.read() if (done) break buf += decoder.decode(value, { stream: true }) const events = buf.split('\n\n') buf = events.pop() || '' for (const ev of events) { const eMatch = ev.match(/^event: (.+)$/m) const dMatch = ev.match(/^data: (.+)$/m) if (!eMatch || !dMatch) continue const type = eMatch[1].trim() let data: Record = {} try { data = JSON.parse(dMatch[1]) } catch { continue } if (type === 'step') { setActiveStage(2) steps.push({ tool: String(data.tool), input: data.input as Record }) patch() } else if (type === 'token') { setActiveStage(5) answer += String(data.t || '') patch() } else if (type === 'error') { setError(String(data.error || 'Agent error')) } } } } catch { setError('Failed to reach agent') } finally { patch() setLoading(false) setActiveStage(null) } } const STAGE_INDEX: Record = { question: 0, embed: 1, retrieve: 2, context: 3, llm: 4, answer: 5 } const onSendChatStream = async (msg: string) => { setLoading(true) setActiveStage(0) let answer = '' let srcs: Source[] | undefined let idx = -1 setMessages((m) => { idx = m.length return [...m, { role: 'assistant', content: '' }] }) const patch = () => setMessages((m) => m.map((x, i) => (i === idx ? { ...x, content: answer, sources: srcs } : x))) try { const r = await fetch('/rag/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg, collection, top_k: 5 }), }) if (!r.ok || !r.body) { setError('Chat service unavailable') return } const reader = r.body.getReader() const decoder = new TextDecoder() let buf = '' for (;;) { const { done, value } = await reader.read() if (done) break buf += decoder.decode(value, { stream: true }) const events = buf.split('\n\n') buf = events.pop() || '' for (const ev of events) { const eMatch = ev.match(/^event: (.+)$/m) const dMatch = ev.match(/^data: (.+)$/m) if (!eMatch || !dMatch) continue const type = eMatch[1].trim() let data: Record = {} try { data = JSON.parse(dMatch[1]) } catch { continue } if (type === 'stage') { const si = STAGE_INDEX[String(data.stage)] if (si != null) setActiveStage(si) } else if (type === 'sources') { srcs = data.sources as Source[] patch() } else if (type === 'token') { setActiveStage(5) answer += String(data.t || '') patch() } else if (type === 'error') { setError(String(data.error || 'Chat error')) } } } } catch { setError('Failed to reach RAG / LLM service') } finally { patch() setLoading(false) setActiveStage(null) } } const onSend = async () => { const msg = input.trim() if (!msg || loading) return setInput('') setError(null) setMessages((m) => [...m, { role: 'user', content: msg }]) if (agentMode) { await onSendAgent(msg) return } await onSendChatStream(msg) } const createCollection = async () => { if (!newCol.trim()) return const fd = new FormData() fd.append('name', newCol.trim()) await fetch('/rag/collections', { method: 'POST', body: fd }) setCollection(newCol.trim()) setNewCol('') loadMeta() } const doclingUiUrl = `${window.location.protocol}//${window.location.hostname}:5001/ui/` return (

Knowledge Chat (RAG)

LangChain + ChromaDB — chat with your ingested documents via Llama 70B

{health && ( <> )}
{showArch && (
)}
{messages.length === 0 && (

Upload once — documents stay in ChromaDB. Ask anytime without re-uploading.

Example: "What maturity gaps exist in the customer dataset?"

)} {messages.map((m, i) => (
{m.steps && m.steps.length > 0 && (
{m.steps.map((s, j) => ( {s.tool} ))}
)}

{m.content || (loading && i === messages.length - 1 ? '…' : '')}

{m.sources && m.sources.length > 0 && (

Sources

{m.sources.map((s, j) => (

{s.source} · chunk {s.chunk}: {s.preview?.slice(0, 120)}…

))}
)}
))} {loading && (
Retrieving context & generating answer…
)} {error &&

{error}

}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), onSend())} placeholder={agentMode ? 'Ask the platform agent (catalog, PII, CDC, run a movement…)' : 'Ask a question about your ingested data…'} className="min-w-0 flex-1 rounded-lg border border-border bg-surface-overlay px-3 py-2 text-[12px]" disabled={loading} />
) } const INDEX_STAGES = [ { icon: Upload, label: 'Upload', sub: 'PDF·DOCX·MD', color: '#38bdf8', lc: false }, { icon: ScanText, label: 'Docling', sub: 'OCR · tables', color: '#22d3ee', lc: false }, { icon: Scissors, label: 'Split', sub: 'TextSplitter', color: '#a78bfa', lc: true }, { icon: Binary, label: 'Embed', sub: 'MiniLM 384d', color: '#f472b6', lc: true }, { icon: Database, label: 'ChromaDB', sub: 'vector store', color: '#34d399', lc: true }, ] const QUERY_STAGES = [ { icon: MessageSquare, label: 'Question', sub: 'user', color: '#38bdf8', lc: false }, { icon: Binary, label: 'Embed', sub: 'MiniLM', color: '#f472b6', lc: true }, { icon: Search, label: 'Retrieve', sub: 'as_retriever', color: '#34d399', lc: true }, { icon: Layers, label: 'Context', sub: 'top-5 chunks', color: '#fbbf24', lc: false }, { icon: Cpu, label: 'LLM', sub: 'ChatOpenAI', color: '#818cf8', lc: true }, { icon: Sparkles, label: 'Answer', sub: '+ sources', color: '#34d399', lc: false }, ] const STAGE_LABEL = ['Question', 'Embed', 'Retrieve', 'Context', 'LLM', 'Answer'] function RagFlow({ loading, ingesting, activeStage }: { loading: boolean; ingesting: boolean; activeStage: number | null }) { return (
How it works — RAG pipeline orchestrated by LangChain ChromaDB · Docling · sentence-transformers {activeStage != null && ( {STAGE_LABEL[activeStage] || 'working'}… )} {activeStage == null && ingesting && ( ingesting… )}
) } function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: typeof INDEX_STAGES; flowing: boolean; activeIndex?: number | null }) { return (
{tag}
{stages.map((s, i) => { const state: 'idle' | 'active' | 'done' = activeIndex == null ? 'idle' : i < activeIndex ? 'done' : i === activeIndex ? 'active' : 'idle' return ( {i < stages.length - 1 && } ) })}
) } function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof Upload; label: string; sub: string; color: string; lc: boolean; state: 'idle' | 'active' | 'done' }) { const cls = state === 'active' ? 'rag-node-active' : state === 'done' ? 'rag-node-done' : 'rag-node' const glow = state === 'active' ? `${color}cc` : `${color}55` return (
{lc && ( LC )} {label} {sub}
) } function FlowConnector({ color, delay }: { color: string; delay: number }) { return (
) } function StatusPill({ ok, label }: { ok: boolean; label: string }) { return ( {label} {ok ? '●' : '○'} ) }