import { useEffect, useState } from "react"; import { api } from "./api.js"; export default function ResultsView({ jobId, onBack }) { const [data, setData] = useState(null); const [error, setError] = useState(""); useEffect(() => { api.jobResults(jobId).then(setData).catch((e) => setError(e.message)); }, [jobId]); if (error) return (
{error}
); if (!data) return
Loading results…
; const { summary, metadata } = data; // Multi-construct runs summarize per construct + a correlation matrix; // single-construct summaries keep the original flat shape. const multi = Array.isArray(summary.constructs); // Anchor-vector (bipolar) runs score along the axis between two poles: the // score is centered on 0, negative meaning "toward the opposite pole". const anchored = summary.anchored === true; return ( <>

{metadata.construct} × {metadata.corpus_file}

{anchored ? ( <> Anchored (bipolar) score = {summary.metric === "dot" ? "dot product" : "cosine"} of each text with the anchor vector (target centroid minus opposite centroid). Positive = toward {summary.target_name}; negative = toward {summary.opposite_name}. The two per-pole CCR scores are in the export. ) : ( <> CCR score = mean cosine similarity between each text and a construct's scale items.{" "} {/* A construct whose items are ALL reverse-keyed scores in the opposite direction, and the backend says so with CONSTRUCT_ALL_ITEMS_REVERSED. Do not state the direction here when that warning is present - the amber panel names which construct is affected. */} {summary.warnings?.some((w) => w.code === "CONSTRUCT_ALL_ITEMS_REVERSED") ? "Score direction depends on how each construct's items are keyed - see the warning below." : "Higher = the text expresses the construct more strongly."} {multi && " All constructs were scored on the same pass over the corpus, so scores are row-aligned and directly comparable."} )}

{/* Cautionary wording approved by the PI (2026-08-05); source_type comes from the construct snapshot in the run metadata (top-level construct_snapshot on single runs, constructs[].snapshot on multi). */} {[ metadata.construct_snapshot, metadata.target_construct?.snapshot, metadata.opposite_construct?.snapshot, ...(metadata.constructs || []).map((c) => c.snapshot), ].some((s) => s?.source_type === "llm_generated") && (

⚠ This run uses a construct whose items were AI-generated and have not been psychometrically validated. Interpret scores with appropriate caution.

)}
{anchored ? ( <> ) : multi ? ( ) : ( <> )} {summary.n_dropped_empty > 0 && ( )}
{summary.warnings?.length > 0 && (
Data-quality notes
)}
{multi && } {anchored ? ( ) : multi ? ( summary.constructs.map((c, i) => (
{c.construct_name} {" "} mean {c.score_mean.toFixed(3)} · SD {c.score_sd.toFixed(3)} ·{" "} {c.n_items} item{c.n_items === 1 ? "" : "s"} · CSV columns{" "} {c.column_prefix}_*

Score distribution

Per-item mean loadings

Highest-scoring texts

Lowest-scoring texts

)) ) : ( <>

Score distribution

Per-item mean loadings

Mean similarity of the corpus to each scale item - a face-validity check on which items drive the construct signal.

Highest-scoring texts

Lowest-scoring texts

)}
Reproducibility record - model: {metadata.model} (dim{" "} {metadata.embedding_dim}) {!multi && !anchored && ( <> {" "}· items hash: {metadata.items_sha256_16} )} {anchored && ( <> {" "}· metric: {summary.metric} · anchor vector norm{" "} {metadata.anchor_vector_norm} )}{" "} · text column: {metadata.text_column} · run:{" "} {metadata.started_at} → {metadata.finished_at} ({metadata.duration_seconds}s) · numpy {metadata.numpy} {metadata.sentence_transformers && ` · sentence-transformers ${metadata.sentence_transformers}`} {anchored ? (
{metadata.target_construct?.name} (target) - items hash{" "} {metadata.target_items_sha256_16} {metadata.target_construct?.reference ? ` · ${metadata.target_construct.reference}` : ""}
{metadata.opposite_construct?.name} (opposite) - items hash{" "} {metadata.opposite_items_sha256_16} {metadata.opposite_construct?.reference ? ` · ${metadata.opposite_construct.reference}` : ""}
) : multi ? (
{metadata.constructs.map((c) => (
{c.name} - items hash {c.items_sha256_16} {c.reference ? ` · ${c.reference}` : ""}
))}
) : (
Construct reference: {metadata.construct_reference || "-"}
)}
); } // Bipolar (anchor-vector) run body: distribution centered on 0, per-pole item // loadings side by side, and top/bottom texts labeled by pole (spec 0006). function AnchorBody({ summary }) { return ( <>

Score distribution

Centered on zero. Texts to the right lean toward {summary.target_name}; to the left, toward {summary.opposite_name}.

{summary.target_name} items

Mean similarity of the corpus to each target-pole item.

{summary.opposite_name} items

Mean similarity to each opposite-pole item.

Most {summary.target_name}

Most {summary.opposite_name}

); } // Correlation table in the layout psychology papers use: rows "1. Name", // columns numbered. Cell shading encodes sign (accent = positive, blue = // negative) and strength (|r|). function CorrelationCard({ correlations }) { const { constructs: names, matrix, n_texts } = correlations; function cellStyle(r, isDiag) { if (isDiag || r == null) return { color: "#98a2b3" }; const alpha = Math.min(0.85, Math.abs(r)); // Diverging pair: brand teal for positive, copper for negative. return { background: r >= 0 ? `rgba(38, 115, 111, ${alpha})` : `rgba(178, 96, 43, ${alpha})`, color: Math.abs(r) > 0.5 ? "#fff" : undefined, textAlign: "center", }; } return (

Construct interrelations

Pearson correlation between per-text CCR scores ({n_texts.toLocaleString()} texts). Positive r = the constructs rise and fall together in your corpus; negative r = texts high on one tend to be low on the other. The exported CSV contains every per-text score, so these are fully recomputable.

))} {names.map((rowName, i) => ( {matrix[i].map((r, j) => ( ))} ))}
{names.map((n, i) => ( {i + 1}
{i + 1}. {rowName} {i === j ? "-" : r == null ? "n/a" : r.toFixed(2)}
); } function Stat({ k, v }) { return (
{v}
{k}
); } function ItemBars({ itemMeans }) { const maxItemMean = Math.max(...itemMeans.map((m) => Math.abs(m.mean)), 1e-9); return ( <> {itemMeans.map((m, i) => (
{m.item.length > 80 ? m.item.slice(0, 80) + "…" : m.item}
{m.mean.toFixed(3)}
))} ); } function DocTable({ docs }) { return (
{docs.map((d) => ( ))}
Score Text
{d.score.toFixed(3)} {d.text}
); } function Histogram({ histogram }) { const { counts, edges } = histogram; const W = 640; const H = 180; const PAD = { top: 10, right: 10, bottom: 26, left: 34 }; const plotW = W - PAD.left - PAD.right; const plotH = H - PAD.top - PAD.bottom; const maxCount = Math.max(...counts, 1); const barW = plotW / counts.length; return ( {/* y gridlines */} {[0.25, 0.5, 0.75, 1].map((f) => { const y = PAD.top + plotH - f * plotH; return ( {Math.round(f * maxCount)} ); })} {/* bars */} {counts.map((c, i) => { const h = (c / maxCount) * plotH; return ( {edges[i].toFixed(3)} – {edges[i + 1].toFixed(3)}: {c} ); })} {/* x labels: first, middle, last edges */} {[0, Math.floor(counts.length / 2), counts.length].map((i) => ( {edges[i].toFixed(2)} ))} ); }