This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import { Round, Participant, Personality } from "@/lib/types";
import { PERSONALITY_LABELS } from "@/lib/utils";
interface Props {
rounds: Round[];
participants: Participant[];
}
function Icon({ name, className = "" }: { name: string; className?: string }) {
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
}
const ACTION_ICON: Record<string, string> = {
propose: "send",
counter: "swap_horiz",
accept: "check_circle",
escalate: "warning",
};
const ACTION_LABEL: Record<string, string> = {
propose: "Proposed",
counter: "Counter",
accept: "Accepted",
escalate: "Escalated",
};
const ACTION_BADGE: Record<string, string> = {
propose: "text-[#B7A6FB] bg-[#B7A6FB]/10 border-[#B7A6FB]/20",
counter: "text-slate-300 bg-white/5 border-white/10",
accept: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
escalate: "text-amber-400 bg-amber-500/10 border-amber-500/20",
};
export default function NegotiationTimeline({ rounds, participants }: Props) {
if (!rounds || rounds.length === 0) {
return (
<div className="text-xs text-slate-600 py-6 text-center font-mono">
Negotiation hasn&apos;t started yet
</div>
);
}
const userA = participants?.[0];
const userB = participants?.[1];
function agentLabel(proposerId: number) {
if (userA && proposerId === userA.user_id) return "A";
if (userB && proposerId === userB.user_id) return "B";
return "?";
}
function agentPersonality(proposerId: number): Personality {
const p = participants?.find((p) => p.user_id === proposerId);
return (p?.personality_used ?? "balanced") as Personality;
}
return (
<div className="relative flex flex-col gap-6">
{/* Vertical data-stream line */}
<div className="absolute left-[18px] top-6 bottom-6 data-stream-line" />
{rounds.map((round, idx) => {
const label = agentLabel(round.proposer_id);
const personality = agentPersonality(round.proposer_id);
const action = round.response_type ?? "propose";
const isA = label === "A";
const isLast = idx === rounds.length - 1;
return (
<div key={round.id} className="flex gap-4 group">
{/* Node */}
<div className="relative z-10 flex flex-col items-center shrink-0">
<div
className={`size-9 rounded-full flex items-center justify-center text-[10px] font-mono font-bold transition-all ${
isLast
? isA
? "bg-[#B7A6FB] text-black shadow-[0_0_12px_#B7A6FB]"
: "bg-cyan-400 text-black shadow-[0_0_12px_#22d3ee]"
: "bg-[#070312] border border-white/10 text-slate-500"
}`}
>
{String(round.round_number).padStart(2, "0")}
</div>
</div>
{/* Content */}
<div className="flex-1 pb-2">
<div className="flex items-center gap-2 mb-2">
<span
className={`text-xs font-bold ${isA ? "text-[#B7A6FB]" : "text-cyan-400"}`}
>
Agent {label}
</span>
<span
className={`text-[9px] font-mono px-1.5 py-0.5 rounded border uppercase tracking-wide ${ACTION_BADGE[action] ?? ACTION_BADGE.propose}`}
>
<Icon name={ACTION_ICON[action] ?? "send"} className="text-[11px] align-middle mr-0.5" />
{ACTION_LABEL[action] ?? action}
</span>
<span className="text-[9px] text-slate-600 font-mono">{PERSONALITY_LABELS[personality]}</span>
</div>
<div
className={`rounded-lg p-3.5 text-sm backdrop-blur-sm border ${
isLast
? isA
? "bg-[#B7A6FB]/5 border-[#B7A6FB]/20"
: "bg-cyan-900/10 border-cyan-500/20"
: "bg-[rgba(7,3,18,0.4)] border-white/5"
}`}
>
{round.reasoning && (
<p className="text-slate-300 text-xs leading-relaxed font-light mb-2">
{round.reasoning}
</p>
)}
{round.proposal && typeof round.proposal === "object" && (
<ProposalSnippet proposal={round.proposal as Record<string, unknown>} />
)}
{/* Satisfaction */}
<div className="flex items-center gap-4 mt-2 pt-2 border-t border-white/5">
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-600 font-mono uppercase">Sat A</span>
<span className={`text-[10px] font-mono font-bold ${(round.satisfaction_a ?? 0) >= 70 ? "text-[#B7A6FB]" : "text-red-400"}`}>
{round.satisfaction_a?.toFixed(0) ?? "—"}%
</span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-600 font-mono uppercase">Sat B</span>
<span className={`text-[10px] font-mono font-bold ${(round.satisfaction_b ?? 0) >= 70 ? "text-cyan-400" : "text-red-400"}`}>
{round.satisfaction_b?.toFixed(0) ?? "—"}%
</span>
</div>
{round.concessions_made?.length > 0 && (
<span className="text-[9px] font-mono text-amber-400 border border-amber-500/20 bg-amber-500/10 px-1.5 py-0.5 rounded uppercase ml-auto">
Concession
</span>
)}
</div>
</div>
</div>
</div>
);
})}
</div>
);
}
function ProposalSnippet({ proposal }: { proposal: Record<string, unknown> }) {
const summary =
(proposal.summary as string) ?? (proposal.for_party_a as string) ?? null;
if (!summary) return null;
return (
<div className="code-snippet p-2 text-[10px] text-slate-400 leading-relaxed mt-1">
{summary}
</div>
);
}