| | 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 ( |
| | <span |
| | style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }} |
| | onMouseEnter={() => setOpen(true)} |
| | onMouseLeave={() => setOpen(false)} |
| | onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }} |
| | > |
| | <span style={{ |
| | display: 'inline-flex', |
| | alignItems: 'center', |
| | justifyContent: 'center', |
| | width: '16px', |
| | height: '16px', |
| | borderRadius: '50%', |
| | border: '1px solid var(--border)', |
| | background: 'var(--bg-card)', |
| | color: 'var(--text-muted)', |
| | fontSize: '0.62rem', |
| | fontWeight: 700, |
| | cursor: 'help', |
| | flexShrink: 0, |
| | lineHeight: 1, |
| | }}> |
| | ? |
| | </span> |
| | {open && ( |
| | <div style={{ |
| | position: 'absolute', |
| | top: '100%', |
| | left: '50%', |
| | transform: 'translateX(-50%)', |
| | marginTop: '6px', |
| | padding: '0.6rem 0.75rem', |
| | background: 'var(--bg-card)', |
| | border: '1px solid var(--border)', |
| | borderRadius: '6px', |
| | boxShadow: '0 4px 16px var(--shadow)', |
| | fontSize: '0.72rem', |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | fontWeight: 400, |
| | color: 'var(--text)', |
| | lineHeight: 1.55, |
| | width: '220px', |
| | zIndex: 100, |
| | textTransform: 'none', |
| | letterSpacing: 'normal', |
| | }}> |
| | {text} |
| | </div> |
| | )} |
| | </span> |
| | ); |
| | } |
| |
|
| | function StageHeader({ label, accent, info }: { label: string; accent: string; info: string }) { |
| | return ( |
| | <div style={{ |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.4rem', |
| | marginBottom: '0.75rem', |
| | paddingBottom: '0.5rem', |
| | borderBottom: '1px solid var(--stage-divider)', |
| | }}> |
| | <span style={{ |
| | width: '3px', |
| | height: '14px', |
| | borderRadius: '2px', |
| | background: accent, |
| | flexShrink: 0, |
| | }} /> |
| | <h3 style={{ |
| | margin: 0, |
| | fontSize: '0.78rem', |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | fontWeight: 700, |
| | color: accent, |
| | textTransform: 'uppercase', |
| | letterSpacing: '0.05em', |
| | }}> |
| | {label} |
| | </h3> |
| | <InfoTooltip text={info} /> |
| | </div> |
| | ); |
| | } |
| |
|
| | |
| | function StageFlowHeader() { |
| | return ( |
| | <div style={{ |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.3rem', |
| | padding: '0.55rem 0.85rem', |
| | borderBottom: '1px solid var(--pipeline-border)', |
| | flexWrap: 'wrap', |
| | }}> |
| | {STAGES.map((stage, i) => ( |
| | <Fragment key={stage.label}> |
| | {i > 0 && ( |
| | <span style={{ |
| | color: 'var(--text-muted)', |
| | fontSize: '0.7rem', |
| | opacity: 0.5, |
| | margin: '0 0.15rem', |
| | }}> |
| | {'\u203A'} |
| | </span> |
| | )} |
| | <span style={{ |
| | fontSize: '0.68rem', |
| | fontWeight: 700, |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color: stage.accent, |
| | textTransform: 'uppercase', |
| | letterSpacing: '0.04em', |
| | }}> |
| | {stage.label} |
| | </span> |
| | </Fragment> |
| | ))} |
| | </div> |
| | ); |
| | } |
| |
|
| | function QueryColumn({ query, intent, accent, info }: { query?: string; intent?: string; accent: string; info: string }) { |
| | return ( |
| | <div> |
| | <StageHeader label="User Query" accent={accent} info={info} /> |
| | {query ? ( |
| | <> |
| | <div style={{ |
| | padding: '0.55rem 0.75rem', |
| | background: 'var(--bg-card)', |
| | border: '1px solid var(--border)', |
| | borderRadius: '6px', |
| | fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", |
| | fontSize: '0.82rem', |
| | color: 'var(--text)', |
| | wordBreak: 'break-word', |
| | lineHeight: 1.5, |
| | }}> |
| | {query} |
| | </div> |
| | {intent && ( |
| | <div style={{ |
| | marginTop: '0.35rem', |
| | padding: '0.4rem 0.65rem', |
| | background: 'var(--bg-card)', |
| | border: '1px solid #f57f1730', |
| | borderRadius: '6px', |
| | fontSize: '0.72rem', |
| | lineHeight: 1.4, |
| | }}> |
| | <span style={{ |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | fontWeight: 700, |
| | color: '#f57f17', |
| | textTransform: 'uppercase', |
| | fontSize: '0.62rem', |
| | letterSpacing: '0.04em', |
| | }}> |
| | Intent |
| | </span> |
| | <div style={{ |
| | fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", |
| | color: 'var(--text-secondary)', |
| | marginTop: '0.15rem', |
| | }}> |
| | {intent} |
| | </div> |
| | </div> |
| | )} |
| | </> |
| | ) : ( |
| | <p style={{ |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | fontSize: '0.8rem', |
| | color: 'var(--text-muted)', |
| | margin: 0, |
| | }}> |
| | No query yet. |
| | </p> |
| | )} |
| | </div> |
| | ); |
| | } |
| |
|
| | function FinalResultsPanel({ results }: { results: FinalResult[] }) { |
| | return ( |
| | <div style={{ |
| | padding: '0.85rem', |
| | borderTop: '1px solid var(--pipeline-border)', |
| | }}> |
| | <div style={{ |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.4rem', |
| | marginBottom: '0.6rem', |
| | }}> |
| | <span style={{ |
| | width: '3px', |
| | height: '14px', |
| | borderRadius: '2px', |
| | background: '#1b5e20', |
| | flexShrink: 0, |
| | }} /> |
| | <h3 style={{ |
| | margin: 0, |
| | fontSize: '0.82rem', |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | fontWeight: 700, |
| | color: '#1b5e20', |
| | textTransform: 'uppercase', |
| | letterSpacing: '0.05em', |
| | }}> |
| | Final Results |
| | </h3> |
| | <span style={{ |
| | fontSize: '0.68rem', |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color: 'var(--text-muted)', |
| | }}> |
| | ({results.length} docs) |
| | </span> |
| | </div> |
| | <div style={{ |
| | display: 'grid', |
| | gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', |
| | gap: '0.4rem', |
| | }}> |
| | {results.slice(0, 5).map(r => ( |
| | <ResultCard |
| | key={r.docId} |
| | title={r.title} |
| | score={r.score} |
| | snippet={r.bestChunk} |
| | /> |
| | ))} |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|
| | export default function PipelineView({ state, query, intent }: PipelineViewProps) { |
| | const blendDone = state.blend.status === 'done'; |
| | const finalResults = state.blend.data?.finalResults; |
| |
|
| | return ( |
| | <> |
| | <style>{` |
| | @keyframes spin { |
| | to { transform: rotate(360deg); } |
| | } |
| | `}</style> |
| | |
| | <div style={{ |
| | borderRadius: '10px', |
| | overflow: 'hidden', |
| | border: '1px solid var(--pipeline-border)', |
| | background: 'var(--pipeline-bg)', |
| | boxShadow: '0 2px 12px var(--shadow)', |
| | marginBottom: '1.5rem', |
| | }}> |
| | {/* Flow header band */} |
| | <StageFlowHeader /> |
| | |
| | {/* Process row: 4 stages, align-items: start so columns shrink to content */} |
| | <div |
| | className="pipeline-grid" |
| | style={{ |
| | display: 'grid', |
| | gridTemplateColumns: 'minmax(100px, 0.6fr) minmax(110px, 0.7fr) minmax(200px, 1.5fr) minmax(200px, 1.5fr)', |
| | gap: '0', |
| | alignItems: 'start', |
| | }} |
| | > |
| | {STAGES.map((col, i) => ( |
| | <div |
| | key={col.label} |
| | className="pipeline-cell" |
| | style={{ |
| | padding: '0.85rem', |
| | borderTop: `3px solid ${col.accent}`, |
| | borderRight: i < STAGES.length - 1 ? '1px solid var(--pipeline-border)' : 'none', |
| | }} |
| | > |
| | {i === 0 && <QueryColumn query={query} intent={intent} accent={col.accent} info={col.info} />} |
| | {i === 1 && <ExpansionColumn state={state.expansion} accent={col.accent} info={col.info} />} |
| | {i === 2 && <SearchColumn state={state.search} accent={col.accent} info={col.info} />} |
| | {i === 3 && ( |
| | <FusionColumn state={{ |
| | rrf: state.rrf, |
| | rerank: state.rerank, |
| | finalResults: finalResults, |
| | }} accent={col.accent} info={col.info} /> |
| | )} |
| | </div> |
| | ))} |
| | </div> |
| | |
| | {/* Results row: full-width below the process stages */} |
| | {blendDone && finalResults && finalResults.length > 0 && ( |
| | <FinalResultsPanel results={finalResults} /> |
| | )} |
| | </div> |
| | |
| | <style>{` |
| | @media (max-width: 768px) { |
| | .pipeline-grid { |
| | grid-template-columns: 1fr !important; |
| | } |
| | |
| | .pipeline-cell { |
| | border-right: none !important; |
| | border-bottom: 1px solid var(--pipeline-border); |
| | } |
| | |
| | .pipeline-cell:last-child { |
| | border-bottom: none; |
| | } |
| | } |
| | `}</style> |
| | </> |
| | ); |
| | } |
| |
|
| | export { InfoTooltip, StageHeader }; |
| |
|