| 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> |
| ); |
| } |
|
|