Knowledge Chat: clickable pipeline steps with explanations + stronger live pulsing
- Each pipeline node is now a button; clicking shows a detail card (what the step does + the LangChain/tech component) - Stronger active-stage pulse (expanding ring + brighter glow) - Chat auto-opens the How-it-works panel so live per-stage pulsing is always visible while answering
This commit is contained in:
@@ -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<string, number> = { 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 (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[9px] font-semibold uppercase tracking-wider text-foreground-faint">
|
||||
<Workflow className="h-3 w-3 text-docker" /> How it works — RAG pipeline
|
||||
<span className="rounded bg-surface-overlay px-1.5 py-0.5 font-mono text-[8px] normal-case text-foreground-faint">click a step</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-emerald-400/40 bg-emerald-500/15 px-1.5 py-0.5 font-mono text-[8px] normal-case text-emerald-300">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" /> orchestrated by LangChain
|
||||
</span>
|
||||
@@ -617,13 +626,30 @@ function RagFlow({ loading, ingesting, activeStage, smithUrl, project }: { loadi
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<FlowRow tag="Index" stages={INDEX_STAGES} flowing={ingesting} />
|
||||
<FlowRow tag="Query" stages={QUERY_STAGES} flowing={loading} activeIndex={activeStage} />
|
||||
<FlowRow tag="Index" stages={INDEX_STAGES} flowing={ingesting} selectedIndex={sel?.row === 'Index' ? sel.i : null} onSelect={(i) => pick('Index', i)} />
|
||||
<FlowRow tag="Query" stages={QUERY_STAGES} flowing={loading} activeIndex={activeStage} selectedIndex={sel?.row === 'Query' ? sel.i : null} onSelect={(i) => pick('Query', i)} />
|
||||
{selStage && (
|
||||
<div className="mt-1 flex items-start gap-2 rounded-lg border border-border bg-surface-raised/80 p-2.5">
|
||||
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md" style={{ background: `${selStage.color}22` }}>
|
||||
{SelIcon && <SelIcon className="h-4 w-4" style={{ color: selStage.color }} />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold text-foreground">{sel?.row} · {selStage.label}</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 font-mono text-[8px]', selStage.lc ? 'bg-emerald-500/15 text-emerald-300' : 'bg-surface-overlay text-foreground-muted')}>{selStage.tech}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-snug text-foreground-muted">{selStage.desc}</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => setSel(null)} className="shrink-0 rounded p-0.5 text-foreground-faint hover:bg-surface-overlay" title="Close">
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-10 shrink-0 text-[8px] font-semibold uppercase text-foreground-faint">{tag}</span>
|
||||
@@ -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 (
|
||||
<Fragment key={`${tag}-${s.label}-${i}`}>
|
||||
<FlowNode icon={s.icon} label={s.label} sub={s.sub} color={s.color} lc={s.lc} state={state} />
|
||||
<FlowNode icon={s.icon} label={s.label} sub={s.sub} color={s.color} lc={s.lc} state={state} selected={selectedIndex === i} onClick={() => onSelect?.(i)} />
|
||||
{i < stages.length - 1 && <FlowConnector color={s.color} delay={i * 0.18} />}
|
||||
</Fragment>
|
||||
)
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn('relative flex min-w-[64px] shrink-0 flex-col items-center gap-0.5 rounded-lg border border-border bg-surface-raised px-2 py-1.5 text-center transition-colors', cls)}
|
||||
style={{ '--rag-glow': glow, '--rag-line': color } as React.CSSProperties}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
title={`${label} — click for details`}
|
||||
className={cn('relative flex min-w-[64px] shrink-0 cursor-pointer flex-col items-center gap-0.5 rounded-lg border border-border bg-surface-raised px-2 py-1.5 text-center transition-all hover:-translate-y-0.5 hover:border-foreground-faint', cls)}
|
||||
style={{ '--rag-glow': glow, '--rag-line': color, outline: selected ? `2px solid ${color}` : undefined, outlineOffset: '2px' } as React.CSSProperties}
|
||||
>
|
||||
{lc && (
|
||||
<span className="absolute -right-1.5 -top-1.5 rounded bg-emerald-500/90 px-1 text-[7px] font-bold leading-none text-white shadow" title="LangChain component">LC</span>
|
||||
@@ -657,7 +686,7 @@ function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof U
|
||||
<Icon className="h-4 w-4" style={{ color }} />
|
||||
<span className="text-[9px] font-medium leading-tight text-foreground">{label}</span>
|
||||
<span className="text-[8px] leading-tight text-foreground-faint">{sub}</span>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
Reference in New Issue
Block a user