"use client"; import { Btn } from "@/components/btn"; import { Bar, BarChart, Cell, ErrorBar, LabelList, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import type { ClusterRow } from "@/lib/types"; import { useTheme } from "@/lib/providers"; import { useState } from "react"; import { A, B, FF } from "@/lib/theme"; function Card({ children, title, subtitle }: { children: React.ReactNode; title: string; subtitle: string }) { const { t } = useTheme(); return (

{title}

{subtitle}

{children}
); } export function ForestPlot({ clusters }: { clusters: ClusterRow[] }) { const { t } = useTheme(); const [selected, setSelected] = useState(null); const [hovered, setHovered] = useState(null); const rows = [...clusters] .filter((c) => c.mean_delta !== null) .sort((a, b) => (a.mean_delta ?? 0) - (b.mean_delta ?? 0)) .map((c) => { const x = (c.mean_delta ?? 0) * 100; const lo = (c.ci_low ?? 0) * 100; const hi = (c.ci_high ?? 0) * 100; const color = c.winner === "A" ? A : c.winner === "B" ? B : t.faint; return { label: c.label, x, err: [x - lo, hi - x] as [number, number], color, row: c }; }); return ( {selected ? ( setSelected(null)} t={t} /> ) : ( <>
v.toFixed(0)} stroke={t.border} /> [`${v.toFixed(2)} pts`, "Δ (B − A)"]} /> setSelected(d?.payload?.row ?? null)} onMouseEnter={(_: any, i: number) => setHovered(i)} onMouseLeave={() => setHovered(null)} > { const { x, y, width, height, index, viewBox } = props; if (index !== hovered) return null; const label = "(click to see details)"; const px = 6.2 * label.length; // approx text width at 10.5px mono const barEnd = x + width; // right end for +bars, left end for -bars const rightEdge = (viewBox?.x ?? 0) + (viewBox?.width ?? 9999); const leftEdge = viewBox?.x ?? 0; let tx: number; let anchor: "start" | "end"; let inside = false; if (width >= 0) { if (barEnd + 8 + px <= rightEdge) { tx = barEnd + 8; anchor = "start"; } else { tx = barEnd - 8; anchor = "end"; inside = true; } } else { if (barEnd - 8 - px >= leftEdge) { tx = barEnd - 8; anchor = "end"; } else { tx = barEnd + 8; anchor = "start"; inside = true; } } const wide = Math.abs(width) > px + 20; return ( {label} ); }} /> {rows.map((r, i) => ( ))}
{"\u25C0"} Résumé A matches better Résumé B matches better {"\u25B6"}
)}
); } export function Chips({ items, color, t, empty = "no distinguishing skills in this segment", }: { items: { skill: string; freq: number }[]; color: string; t: any; empty?: string; }) { if (!items.length) return ( {empty} ); const max = Math.max(...items.map((i) => i.freq), 0.01); return (
{items.map((it, idx) => (
{/* frequency bar — structure encodes the % */} {it.skill} {Math.round(it.freq * 100)}%
))}
); } function DiffPanel({ row, onBack, t }: { row: ClusterRow; onBack: () => void; t: any }) { const d = row.differentiators ?? { a_favoring: [], b_favoring: [] }; return (
{"\u2190"} back to all clusters

{row.label}

Skills common in this cluster's postings that appear in only one résumé — what separates A from B here. (The score itself is semantic similarity, so read these as differentiators, not exact causes.)

{"\u25C0"} FAVORS RÉSUMÉ A

FAVORS RÉSUMÉ B {"\u25B6"}

); }