From f080c242de1fc934c59f5b97f20d4d17f9c6e3c1 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 28 Jun 2026 12:48:52 +0000 Subject: [PATCH] Knowledge Chat: spread pipeline nodes full-width, add self-hosted Trace Viewer + favicon - RAG pipeline nodes now distribute across the row with flex-grow connectors (longer flowing lines) - New Traces modal: list recent runs with stage timeline, retrieved chunks, prompt, answer (local LangSmith-style observability) - Add topology-style SVG favicon --- ui/index.html | 1 + ui/public/favicon.svg | 18 ++ .../components/features/KnowledgeChatView.tsx | 234 +++++++++++++++++- 3 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 ui/public/favicon.svg diff --git a/ui/index.html b/ui/index.html index d4b924e..5c92cac 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,6 +3,7 @@ + ATC Data & AI Command Center diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 0000000..90d9378 --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui/src/components/features/KnowledgeChatView.tsx b/ui/src/components/features/KnowledgeChatView.tsx index 04e08aa..ab7efb6 100644 --- a/ui/src/components/features/KnowledgeChatView.tsx +++ b/ui/src/components/features/KnowledgeChatView.tsx @@ -1,5 +1,5 @@ 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 { Activity, Binary, Bot, BookOpen, ChevronDown, Cpu, Database, FileText, Layers, Loader2, MessageSquare, RefreshCw, RotateCcw, ScanText, Scissors, Search, Send, Sparkles, Trash2, Upload, Workflow, Wrench, X } from 'lucide-react' import { cn } from '../../lib/utils' import { subTabActive, subTabIdle } from '../../lib/tabActive' @@ -41,6 +41,7 @@ export function KnowledgeChatView({ onGpuActivity }: Props = {}) { const [showArch, setShowArch] = useState(true) const [activeStage, setActiveStage] = useState(null) const [traceCfg, setTraceCfg] = useState<{ tracing: boolean; project: string; smith_url: string } | null>(null) + const [showTraces, setShowTraces] = useState(false) const bottomRef = useRef(null) const loadMeta = useCallback(async () => { @@ -359,6 +360,14 @@ export function KnowledgeChatView({ onGpuActivity }: Props = {}) { )} + {traceCfg?.tracing && ( + setShowTraces(false)} smith={traceCfg?.tracing ? traceCfg.smith_url : null} /> {showArch && (
@@ -617,7 +627,7 @@ function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: t return (
{tag} -
+
{stages.map((s, i) => { const state: 'idle' | 'active' | 'done' = activeIndex == null ? 'idle' : i < activeIndex ? 'done' : i === activeIndex ? 'active' : 'idle' @@ -653,7 +663,7 @@ function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof U function FlowConnector({ color, delay }: { color: string; delay: number }) { return ( -
+
= { + embed: '#f472b6', + retrieve: '#34d399', + context: '#fbbf24', + llm: '#818cf8', + answer: '#22d3ee', +} + +function TraceViewer({ open, onClose, smith }: { open: boolean; onClose: () => void; smith?: string | null }) { + const [items, setItems] = useState([]) + const [selId, setSelId] = useState(null) + const [sel, setSel] = useState(null) + const [loading, setLoading] = useState(false) + + const load = useCallback(async () => { + setLoading(true) + try { + const r = await fetch('/rag/traces?limit=80') + const j = await r.json() + setItems((j.traces || []) as TraceSummary[]) + } catch { + /* ignore */ + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + if (!open) return + load() + const id = setInterval(load, 5000) + return () => clearInterval(id) + }, [open, load]) + + useEffect(() => { + if (!open) return + if (!selId && items.length) setSelId(items[0].id) + }, [open, items, selId]) + + useEffect(() => { + if (!selId) { + setSel(null) + return + } + fetch(`/rag/traces/${selId}`) + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { if (d && !d.error) setSel(d as TraceDetail) }) + .catch(() => {}) + }, [selId]) + + const clearAll = async () => { + await fetch('/rag/traces', { method: 'DELETE' }).catch(() => {}) + setSelId(null) + setSel(null) + load() + } + + if (!open) return null + const total = sel ? Math.max(sel.latency_ms, 1) : 1 + return ( +
+
e.stopPropagation()} + > +
+ +
+ Run Traces + Self-hosted RAG observability · {items.length} recent runs +
+ {smith && ( + + also in LangSmith ↗ + + )} +
+ + + +
+
+ +
+
+ {items.length === 0 && ( +
No runs yet. Ask a question in the chat.
+ )} + {items.map((it) => ( + + ))} +
+ +
+ {!sel &&
Select a run to inspect.
} + {sel && ( +
+
+
+ {sel.status} + {sel.model} + {sel.latency_ms} ms + {sel.collection} + {sel.langsmith && → LangSmith} + {sel.id} +
+

{sel.question}

+ {sel.error &&

{sel.error}

} +
+ +
+
Pipeline timeline
+
+ {sel.stages.map((st) => ( +
+ {st.dur_ms > total * 0.06 ? st.name : ''} +
+ ))} +
+
+ {sel.stages.map((st) => ( + + {st.name} {st.dur_ms}ms + + ))} +
+
+ +
+
Retrieved context · {sel.sources.length} chunks · {sel.context_chars} chars
+
+ {sel.sources.map((s, i) => ( +
+
+ {s.source} · chunk {s.chunk} +
+

{s.preview}

+
+ ))} +
+
+ +
+ System prompt +
{sel.system_prompt}
+
+ +
+
Answer · {sel.answer_chars} chars
+
{sel.answer || '(no answer)'}
+
+
+ )} +
+
+
+
+ ) +} + function StatusPill({ ok, label }: { ok: boolean; label: string }) { return (