import { Fragment, useState } from 'react'; import type { ExpandedQuery, ScoredChunk, RRFResult, RerankedResult, FinalResult } from '../types'; import ExpansionColumn from './ExpansionColumn'; import SearchColumn from './SearchColumn'; import FusionColumn from './FusionColumn'; import ResultCard from './ResultCard'; export interface PipelineState { expansion: { status: 'idle' | 'running' | 'done' | 'error'; data?: ExpandedQuery; error?: string }; search: { status: 'idle' | 'running' | 'done'; data?: { bm25Hits: ScoredChunk[]; vectorHits: ScoredChunk[] } }; rrf: { status: 'idle' | 'done'; data?: { merged: RRFResult[] } }; rerank: { status: 'idle' | 'running' | 'done'; data?: { before: RRFResult[]; after: RerankedResult[] } }; blend: { status: 'idle' | 'done'; data?: { finalResults: FinalResult[] } }; } interface PipelineViewProps { state: PipelineState; query?: string; intent?: string; } const STAGES = [ { label: 'User Query', accent: '#5c6bc0', info: 'The original search query you typed. This is the starting point for the entire pipeline.', }, { label: 'Query Expansion', accent: '#f57f17', info: 'A compact 1.7B LLM (cached locally) generates lexical keywords (lex), semantic sentences (vec), and a hypothetical document (HyDE). When BM25 already has a strong exact match, expansion is skipped.', }, { label: 'Parallel Search', accent: '#00897b', info: 'The original query always runs through BM25 and vector search. Lex variants route only to BM25, while vec and HyDE variants route to vector search, mirroring qmd\'s typed retrieval flow.', }, { label: 'Fusion & Reranking', accent: '#388e3c', info: 'Results are merged via Reciprocal Rank Fusion (RRF), then a cross-encoder reranker (Qwen3-Reranker-0.6B) re-scores the top candidates. Final ranking blends reranker confidence with RRF position.', }, ]; function InfoTooltip({ text }: { text: string }) { const [open, setOpen] = useState(false); return ( setOpen(true)} onMouseLeave={() => setOpen(false)} onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }} > ? {open && (
{text}
)}
); } function StageHeader({ label, accent, info }: { label: string; accent: string; info: string }) { return (

{label}

); } // Horizontal header band showing stage flow: Stage 1 › Stage 2 › Stage 3 › Stage 4 function StageFlowHeader() { return (
{STAGES.map((stage, i) => ( {i > 0 && ( {'\u203A'} )} {stage.label} ))}
); } function QueryColumn({ query, intent, accent, info }: { query?: string; intent?: string; accent: string; info: string }) { return (
{query ? ( <>
{query}
{intent && (
Intent
{intent}
)} ) : (

No query yet.

)}
); } function FinalResultsPanel({ results }: { results: FinalResult[] }) { return (

Final Results

({results.length} docs)
{results.slice(0, 5).map(r => ( ))}
); } export default function PipelineView({ state, query, intent }: PipelineViewProps) { const blendDone = state.blend.status === 'done'; const finalResults = state.blend.data?.finalResults; return ( <>
{/* Flow header band */} {/* Process row: 4 stages, align-items: start so columns shrink to content */}
{STAGES.map((col, i) => (
{i === 0 && } {i === 1 && } {i === 2 && } {i === 3 && ( )}
))}
{/* Results row: full-width below the process stages */} {blendDone && finalResults && finalResults.length > 0 && ( )}
); } export { InfoTooltip, StageHeader };