"use client"; import { useEffect, useMemo, useState } from "react"; import { cn } from "@/lib/cn"; export type FieldState = "idle" | "searching" | "answered" | "refused" | "blocked"; interface Props { /** One bar per indexed passage. The corpus, drawn. */ count: number; state: FieldState; /** How many passages the retrievers returned, and how many survived the floor. */ retrieved?: number; kept?: number; className?: string; } /** * The corpus as a field of passages, and what a question does to it. * * This is the hero because it is the thesis. Every retrieval demo can show an answer; * almost none can show you the moment where the system decides it does *not* have one. * Here that moment is the whole picture: passages sit as bars, a question lights the ones * the retrievers found, the cross-encoder pushes the best above a luminous threshold, and * whatever clears it becomes a citation. When nothing clears it, nothing crosses the line * and the line turns amber — the refusal, drawn rather than described. * * The bar count is the real `chunks_indexed` from the API, so the field is the corpus and * not an illustration of one. Heights are derived from the index rather than randomised: * the same corpus always draws the same skyline, which is what makes it read as a picture * of something rather than as decoration. */ export function SpectrumField({ count, state, retrieved = 0, kept = 0, className }: Props) { const bars = Math.min(Math.max(count, 24), 181); // Deterministic pseudo-heights. A hash of the index, not Math.random: a field that // reshuffles on every render is noise, and it would differ between server and client. const heights = useMemo( () => Array.from({ length: bars }, (_, index) => { const hashed = Math.sin(index * 12.9898) * 43758.5453; return 0.34 + (hashed - Math.floor(hashed)) * 0.66; }), [bars], ); // Which bars are "found" for this question. Spread across the field rather than // clustered, because retrieval hits are not adjacent in the corpus. const foundStep = retrieved > 0 ? Math.max(1, Math.floor(bars / retrieved)) : 0; const keptStep = kept > 0 ? Math.max(1, Math.floor(bars / kept)) : 0; // The entrance runs once, so the field assembles on load instead of appearing. const [entered, setEntered] = useState(false); useEffect(() => { const frame = requestAnimationFrame(() => setEntered(true)); return () => cancelAnimationFrame(frame); }, []); const refused = state === "refused" || state === "blocked"; const searching = state === "searching"; return (
{heights.map((height, index) => { const isFound = foundStep > 0 && index % foundStep === 0; const isKept = keptStep > 0 && index % keptStep === 0 && !refused; // At rest the field carries the spectrum itself, so the scale that colour // means is legible before a question is asked rather than only after one. const idleHue = index / bars < 0.34 ? "var(--color-indigo)" : index / bars < 0.67 ? "var(--color-violet)" : "var(--color-ochre)"; const active = isKept ? "var(--color-indigo)" : isFound ? refused ? "var(--color-ochre)" : "var(--color-violet)" : idleHue; return ( ); })}
{/* The threshold. Everything above it is answerable; everything below is a near miss. It is the only element in the interface allowed to glow. */}
{refused ? "nothing cleared the line — Lexora refuses" : state === "answered" ? `${kept} passage${kept === 1 ? "" : "s"} cleared the line and became citations` : searching ? "searching…" : `${count} passages, waiting for a question`} {state === "idle" ? "" : `${retrieved} found`}
); }