| | import type { RRFResult, RerankedResult, FinalResult } from '../types'; |
| | import { InfoTooltip } from './PipelineView'; |
| |
|
| | interface FusionColumnState { |
| | rrf: { status: 'idle' | 'done'; data?: { merged: RRFResult[] } }; |
| | rerank: { status: 'idle' | 'running' | 'done'; data?: { before: RRFResult[]; after: RerankedResult[] } }; |
| | finalResults?: FinalResult[]; |
| | } |
| |
|
| | interface FusionColumnProps { |
| | state: FusionColumnState; |
| | accent: string; |
| | info: string; |
| | } |
| |
|
| | function Spinner({ color }: { color: string }) { |
| | return ( |
| | <span style={{ |
| | display: 'inline-block', |
| | width: '14px', |
| | height: '14px', |
| | border: '2px solid var(--border)', |
| | borderTopColor: color, |
| | borderRadius: '50%', |
| | animation: 'spin 0.7s linear infinite', |
| | }} /> |
| | ); |
| | } |
| |
|
| | function SectionHeader({ label, color, badge }: { label: string; color: string; badge?: string }) { |
| | return ( |
| | <div style={{ |
| | fontSize: '0.68rem', |
| | fontWeight: 700, |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color, |
| | textTransform: 'uppercase', |
| | letterSpacing: '0.06em', |
| | marginBottom: '0.35rem', |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.4rem', |
| | }}> |
| | {label} |
| | {badge && ( |
| | <span style={{ color: 'var(--text-muted)', fontWeight: 400, fontSize: '0.65rem' }}>{badge}</span> |
| | )} |
| | </div> |
| | ); |
| | } |
| |
|
| | function RRFRow({ result, rank }: { result: RRFResult; rank: number }) { |
| | return ( |
| | <div style={{ |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.4rem', |
| | padding: '0.3rem 0.5rem', |
| | background: 'var(--bg-card)', |
| | border: '1px solid var(--border)', |
| | borderRadius: '5px', |
| | marginBottom: '0.2rem', |
| | fontSize: '0.72rem', |
| | }}> |
| | <span style={{ |
| | fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", |
| | color: 'var(--text-muted)', |
| | fontSize: '0.65rem', |
| | minWidth: '18px', |
| | }}> |
| | #{rank} |
| | </span> |
| | <span style={{ |
| | flex: 1, |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color: 'var(--text)', |
| | fontWeight: 500, |
| | overflow: 'hidden', |
| | textOverflow: 'ellipsis', |
| | whiteSpace: 'nowrap', |
| | }}> |
| | {result.title} |
| | </span> |
| | <span style={{ |
| | fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", |
| | fontSize: '0.65rem', |
| | color: '#2e7d32', |
| | fontWeight: 700, |
| | flexShrink: 0, |
| | }}> |
| | {result.score.toFixed(4)} |
| | </span> |
| | </div> |
| | ); |
| | } |
| |
|
| | |
| | function RankBadge({ label, rank, color }: { label: string; rank: number; color: string }) { |
| | return ( |
| | <span style={{ |
| | display: 'inline-flex', |
| | alignItems: 'center', |
| | gap: '0.15rem', |
| | fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", |
| | fontSize: '0.6rem', |
| | color, |
| | fontWeight: 600, |
| | }}> |
| | <span style={{ color: 'var(--text-muted)', fontWeight: 400, fontSize: '0.55rem' }}>{label}</span> |
| | #{rank} |
| | </span> |
| | ); |
| | } |
| |
|
| | interface RankJourneyRow { |
| | docId: string; |
| | title: string; |
| | rrfRank?: number; |
| | rerankRank?: number; |
| | finalRank?: number; |
| | } |
| |
|
| | |
| | function RankJourney({ before, after, finalResults }: { |
| | before: RRFResult[]; |
| | after: RerankedResult[]; |
| | finalResults?: FinalResult[]; |
| | }) { |
| | const topLimit = 5; |
| | const topBefore = before.slice(0, topLimit); |
| | const topFinal = (finalResults ?? []).slice(0, topLimit); |
| | const rerankOrder = [...after].sort((a, b) => b.rerankScore - a.rerankScore); |
| | const titleMap = new Map<string, string>([ |
| | ...before.map((result) => [result.docId, result.title] as const), |
| | ...after.map((result) => [result.docId, result.title] as const), |
| | ...topFinal.map((result) => [result.docId, result.title] as const), |
| | ]); |
| | const rrfRankMap = new Map(before.map((result, index) => [result.docId, index + 1])); |
| | const rerankRankMap = new Map(rerankOrder.map((r, i) => [r.docId, i + 1])); |
| | const finalRankMap = new Map((finalResults ?? []).map((result, index) => [result.docId, index + 1])); |
| | const rowMap = new Map<string, RankJourneyRow>(); |
| |
|
| | for (const result of [...topBefore, ...topFinal]) { |
| | const existing = rowMap.get(result.docId); |
| | rowMap.set(result.docId, { |
| | docId: result.docId, |
| | title: titleMap.get(result.docId) ?? result.title, |
| | rrfRank: rrfRankMap.get(result.docId), |
| | rerankRank: rerankRankMap.get(result.docId), |
| | finalRank: finalRankMap.get(result.docId), |
| | ...existing, |
| | }); |
| | } |
| |
|
| | const rows = [...rowMap.values()] |
| | .sort((a, b) => |
| | (a.finalRank ?? Number.POSITIVE_INFINITY) - (b.finalRank ?? Number.POSITIVE_INFINITY) || |
| | (a.rrfRank ?? Number.POSITIVE_INFINITY) - (b.rrfRank ?? Number.POSITIVE_INFINITY) || |
| | (a.rerankRank ?? Number.POSITIVE_INFINITY) - (b.rerankRank ?? Number.POSITIVE_INFINITY), |
| | ) |
| | .slice(0, topLimit); |
| |
|
| | return ( |
| | <div> |
| | {rows.map((row) => { |
| | return ( |
| | <div key={row.docId} style={{ |
| | padding: '0.3rem 0.5rem', |
| | background: 'var(--bg-card)', |
| | border: '1px solid var(--border)', |
| | borderRadius: '5px', |
| | marginBottom: '0.2rem', |
| | fontSize: '0.7rem', |
| | }}> |
| | <div style={{ |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color: 'var(--text)', |
| | fontWeight: 500, |
| | marginBottom: '0.2rem', |
| | }}> |
| | {row.title} |
| | </div> |
| | <div style={{ |
| | display: 'flex', |
| | alignItems: 'center', |
| | gap: '0.3rem', |
| | }}> |
| | {row.rrfRank !== undefined && ( |
| | <> |
| | <RankBadge label="RRF" rank={row.rrfRank} color="var(--text-secondary)" /> |
| | <span style={{ color: 'var(--text-muted)', fontSize: '0.55rem' }}>{'\u2192'}</span> |
| | </> |
| | )} |
| | {row.rerankRank !== undefined && ( |
| | <> |
| | <RankBadge label="Reranker" rank={row.rerankRank} color="#f57f17" /> |
| | <span style={{ color: 'var(--text-muted)', fontSize: '0.55rem' }}>{'\u2192'}</span> |
| | </> |
| | )} |
| | {row.finalRank !== undefined ? ( |
| | <RankBadge label="Final" rank={row.finalRank} color="#1b5e20" /> |
| | ) : ( |
| | <span style={{ |
| | fontSize: '0.55rem', |
| | color: 'var(--text-muted)', |
| | fontStyle: 'italic', |
| | }}> |
| | blending... |
| | </span> |
| | )} |
| | </div> |
| | </div> |
| | ); |
| | })} |
| | <div style={{ |
| | fontSize: '0.62rem', |
| | fontFamily: 'system-ui, -apple-system, sans-serif', |
| | color: 'var(--text-muted)', |
| | marginTop: '0.3rem', |
| | fontStyle: 'italic', |
| | lineHeight: 1.4, |
| | }}> |
| | Final ranking blends reranker scores with retrieval position. |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|
| | export default function FusionColumn({ state, accent, info }: FusionColumnProps) { |
| | const rrfDone = state.rrf.status === 'done'; |
| | const rerankRunning = state.rerank.status === 'running'; |
| | const rerankDone = state.rerank.status === 'done'; |
| | const isIdle = !rrfDone && !rerankRunning && !rerankDone; |
| |
|
| | return ( |
| | <div style={{ opacity: isIdle ? 0.45 : 1, transition: 'opacity 0.3s' }}> |
| | <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', |
| | }}> |
| | Fusion & Reranking |
| | </h3> |
| | {rerankRunning && <Spinner color={accent} />} |
| | <InfoTooltip text={info} /> |
| | </div> |
| | |
| | {isIdle && ( |
| | <p style={{ fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: '0.75rem', color: 'var(--text-muted)', margin: 0 }}> |
| | Awaiting search... |
| | </p> |
| | )} |
| | |
| | {rrfDone && state.rrf.data && ( |
| | <div style={{ marginBottom: '0.7rem' }}> |
| | <SectionHeader |
| | label="RRF Fusion" |
| | color="#558b2f" |
| | badge={`(${state.rrf.data.merged.length} docs)`} |
| | /> |
| | {state.rrf.data.merged.slice(0, 5).map((r, i) => ( |
| | <RRFRow key={r.docId} result={r} rank={i + 1} /> |
| | ))} |
| | {state.rrf.data.merged.length > 5 && ( |
| | <div style={{ fontSize: '0.68rem', color: 'var(--text-muted)', fontFamily: 'system-ui, -apple-system, sans-serif', paddingLeft: '0.25rem' }}> |
| | +{state.rrf.data.merged.length - 5} more |
| | </div> |
| | )} |
| | </div> |
| | )} |
| | |
| | {rerankRunning && !rerankDone && ( |
| | <p style={{ fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: '0.75rem', color: 'var(--text-secondary)', margin: '0 0 0.6rem 0', fontStyle: 'italic' }}> |
| | Reranking with cross-encoder... |
| | </p> |
| | )} |
| | |
| | {rerankDone && state.rerank.data && ( |
| | <div> |
| | <SectionHeader label="Rank Journey" color="#33691e" /> |
| | <RankJourney |
| | before={state.rerank.data.before} |
| | after={state.rerank.data.after} |
| | finalResults={state.finalResults} |
| | /> |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | } |
| |
|