diff --git a/ui/src/components/features/KnowledgeChatView.tsx b/ui/src/components/features/KnowledgeChatView.tsx index ab7efb6..83bee39 100644 --- a/ui/src/components/features/KnowledgeChatView.tsx +++ b/ui/src/components/features/KnowledgeChatView.tsx @@ -195,6 +195,7 @@ export function KnowledgeChatView({ onGpuActivity }: Props = {}) { } const onSendAgent = async (msg: string) => { + setShowArch(true) setLoading(true) setActiveStage(0) const steps: AgentStep[] = [] @@ -257,6 +258,7 @@ export function KnowledgeChatView({ onGpuActivity }: Props = {}) { const STAGE_INDEX: Record = { question: 0, embed: 1, retrieve: 2, context: 3, llm: 4, answer: 5 } const onSendChatStream = async (msg: string) => { + setShowArch(true) setLoading(true) setActiveStage(0) let answer = '' @@ -576,29 +578,36 @@ export function KnowledgeChatView({ onGpuActivity }: Props = {}) { } 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 }, + { icon: Upload, label: 'Upload', sub: 'PDF·DOCX·MD', color: '#38bdf8', lc: false, tech: 'FastAPI upload', desc: 'Your file (PDF, DOCX, Markdown, CSV, images) is uploaded to the RAG service and saved to disk for processing.' }, + { icon: ScanText, label: 'Docling', sub: 'OCR · tables', color: '#22d3ee', lc: false, tech: 'Docling serve', desc: 'Docling parses the document with layout-aware OCR and table extraction, turning it into clean Markdown text.' }, + { icon: Scissors, label: 'Split', sub: 'TextSplitter', color: '#a78bfa', lc: true, tech: 'LangChain · RecursiveCharacterTextSplitter', desc: 'The text is cut into ~800-character chunks with 120-char overlap, so each piece fits the embedder and keeps surrounding context.' }, + { icon: Binary, label: 'Embed', sub: 'MiniLM 384d', color: '#f472b6', lc: true, tech: 'LangChain · HuggingFaceEmbeddings (MiniLM)', desc: 'Every chunk is converted into a 384-dimension vector that numerically captures its meaning.' }, + { icon: Database, label: 'ChromaDB', sub: 'vector store', color: '#34d399', lc: true, tech: 'LangChain · Chroma vector store', desc: 'The vectors plus metadata (source file, chunk number) are stored in ChromaDB for fast similarity search.' }, ] 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 }, + { icon: MessageSquare, label: 'Question', sub: 'user', color: '#38bdf8', lc: false, tech: 'user input', desc: 'Your question enters the pipeline as plain text.' }, + { icon: Binary, label: 'Embed', sub: 'MiniLM', color: '#f472b6', lc: true, tech: 'LangChain · HuggingFaceEmbeddings (MiniLM)', desc: 'The question is embedded into the exact same 384-dim vector space as the document chunks.' }, + { icon: Search, label: 'Retrieve', sub: 'as_retriever', color: '#34d399', lc: true, tech: 'LangChain · Chroma retriever', desc: 'ChromaDB returns the chunks whose vectors are closest to the question (we fetch top-k×3, then filter out noise).' }, + { icon: Layers, label: 'Context', sub: 'top-5 chunks', color: '#fbbf24', lc: false, tech: 'context builder', desc: 'The best chunks are assembled into a single context block, each tagged with its source file for citations.' }, + { icon: Cpu, label: 'LLM', sub: 'ChatOpenAI', color: '#818cf8', lc: true, tech: 'LangChain · ChatOpenAI (gpt-4o)', desc: 'The context and your question are sent to the LLM, which writes a grounded answer — streamed back token by token.' }, + { icon: Sparkles, label: 'Answer', sub: '+ sources', color: '#34d399', lc: false, tech: 'answer + sources', desc: 'The streamed answer is shown with its source chunks, and the whole run is logged to the Traces viewer.' }, ] const STAGE_LABEL = ['Question', 'Embed', 'Retrieve', 'Context', 'LLM', 'Answer'] function RagFlow({ loading, ingesting, activeStage, smithUrl, project }: { loading: boolean; ingesting: boolean; activeStage: number | null; smithUrl?: string | null; project?: string }) { + const [sel, setSel] = useState<{ row: 'Index' | 'Query'; i: number } | null>(null) + const selStages = sel?.row === 'Index' ? INDEX_STAGES : QUERY_STAGES + const selStage = sel ? selStages[sel.i] : null + const SelIcon = selStage?.icon + const pick = (row: 'Index' | 'Query', i: number) => + setSel((cur) => (cur && cur.row === row && cur.i === i ? null : { row, i })) return (
How it works — RAG pipeline + click a step orchestrated by LangChain @@ -617,13 +626,30 @@ function RagFlow({ loading, ingesting, activeStage, smithUrl, project }: { loadi )}
- - + pick('Index', i)} /> + pick('Query', i)} /> + {selStage && ( +
+
+ {SelIcon && } +
+
+
+ {sel?.row} · {selStage.label} + {selStage.tech} +
+

{selStage.desc}

+
+ +
+ )}
) } -function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: typeof INDEX_STAGES; flowing: boolean; activeIndex?: number | null }) { +function FlowRow({ tag, stages, flowing, activeIndex, selectedIndex, onSelect }: { tag: string; stages: typeof INDEX_STAGES; flowing: boolean; activeIndex?: number | null; selectedIndex?: number | null; onSelect?: (i: number) => void }) { return (
{tag} @@ -633,7 +659,7 @@ function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: t activeIndex == null ? 'idle' : i < activeIndex ? 'done' : i === activeIndex ? 'active' : 'idle' return ( - + onSelect?.(i)} /> {i < stages.length - 1 && } ) @@ -643,13 +669,16 @@ function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: t ) } -function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof Upload; label: string; sub: string; color: string; lc: boolean; state: 'idle' | 'active' | 'done' }) { +function FlowNode({ icon: Icon, label, sub, color, lc, state, selected, onClick }: { icon: typeof Upload; label: string; sub: string; color: string; lc: boolean; state: 'idle' | 'active' | 'done'; selected?: boolean; onClick?: () => void }) { 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 @@ -657,7 +686,7 @@ function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof U {label} {sub} -
+ ) } diff --git a/ui/src/styles/globals.css b/ui/src/styles/globals.css index ecb12b4..9d45686 100644 --- a/ui/src/styles/globals.css +++ b/ui/src/styles/globals.css @@ -211,9 +211,10 @@ /* RAG flow — active/done stage states (real-time per-stage pulsing) */ @keyframes rag-active-pulse { - 0%, 100% { box-shadow: 0 0 0 0 var(--rag-glow); transform: scale(1); } - 50% { box-shadow: 0 0 16px 3px var(--rag-glow); transform: scale(1.08); } + 0% { box-shadow: 0 0 0 0 var(--rag-glow), 0 0 10px 1px var(--rag-glow); transform: scale(1); } + 50% { box-shadow: 0 0 0 6px transparent, 0 0 22px 5px var(--rag-glow); transform: scale(1.12); } + 100% { box-shadow: 0 0 0 0 var(--rag-glow), 0 0 10px 1px var(--rag-glow); transform: scale(1); } } -.rag-node-active { animation: rag-active-pulse 0.8s ease-in-out infinite; border-color: var(--rag-line) !important; z-index: 1; } +.rag-node-active { animation: rag-active-pulse 0.75s ease-in-out infinite; border-color: var(--rag-line) !important; z-index: 1; } .rag-node-done { border-color: var(--rag-line) !important; } @media (prefers-reduced-motion: reduce) { .rag-node-active { animation: none !important; } }