"use client"; import { useEffect, useState, useCallback } from "react"; import { useParams } from "next/navigation"; import Link from "next/link"; import { api } from "@/lib/api"; import { getSocket, joinNegotiation, leaveNegotiation } from "@/lib/socket"; import type { Negotiation, Round, SatisfactionPoint, ConcessionEntry } from "@/lib/types"; import { FEATURE_LABELS, STATUS_COLORS, STATUS_LABELS, PERSONALITY_LABELS, relativeTime, } from "@/lib/utils"; import type { Personality } from "@/lib/types"; import SatisfactionChart from "@/components/SatisfactionChart"; import FairnessScore from "@/components/FairnessScore"; import ConcessionTimeline from "@/components/ConcessionTimeline"; import NegotiationTimeline from "@/components/NegotiationTimeline"; import ResolutionCard from "@/components/ResolutionCard"; function Icon({ name, className = "" }: { name: string; className?: string }) { return {name}; } const FEATURE_ICONS: Record = { scheduling: "calendar_month", expenses: "account_balance_wallet", freelance: "work", roommate: "home", trip: "flight_takeoff", marketplace: "store", collaborative: "groups", conflict: "gavel", generic: "hub", }; export default function NegotiationDetailPage() { const params = useParams<{ id: string }>(); const id = params.id; const [negotiation, setNegotiation] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [connected, setConnected] = useState(false); const [liveRound, setLiveRound] = useState(null); const load = useCallback(async () => { try { const data = await api.negotiation(id); setNegotiation(data); setError(null); } catch (e) { setError(String(e)); } finally { setLoading(false); } }, [id]); useEffect(() => { load(); const socket = getSocket(); const onConnect = () => setConnected(true); const onDisconnect = () => setConnected(false); const onState = (data: Negotiation) => { setNegotiation(data); setLoading(false); }; const onRound = (payload: { negotiation_id: string; round: Round }) => { if (payload.negotiation_id !== id) return; setLiveRound(payload.round.round_number); setNegotiation((prev) => { if (!prev) return prev; const rounds = [...(prev.rounds ?? [])]; const idx = rounds.findIndex((r) => r.id === payload.round.id); if (idx >= 0) rounds[idx] = payload.round; else rounds.push(payload.round); return { ...prev, rounds, status: "active" }; }); }; const onResolved = (payload: { negotiation_id: string }) => { if (payload.negotiation_id !== id) return; setLiveRound(null); load(); }; socket.on("connect", onConnect); socket.on("disconnect", onDisconnect); socket.on("negotiation_state", onState); socket.on("round_update", onRound); socket.on("negotiation_resolved", onResolved); setConnected(socket.connected); joinNegotiation(id); return () => { leaveNegotiation(id); socket.off("connect", onConnect); socket.off("disconnect", onDisconnect); socket.off("negotiation_state", onState); socket.off("round_update", onRound); socket.off("negotiation_resolved", onResolved); }; }, [id, load]); if (loading) return ; if (error) return ; if (!negotiation) return ; const rounds = negotiation.rounds ?? []; const participants = negotiation.participants ?? []; const analytics = negotiation.analytics; const isLive = negotiation.status === "active"; const satTimeline: SatisfactionPoint[] = analytics?.satisfaction_timeline?.length ? analytics.satisfaction_timeline : rounds.map((r) => ({ round: r.round_number, score_a: r.satisfaction_a, score_b: r.satisfaction_b })); const concessions: ConcessionEntry[] = analytics?.concession_log ?? []; const lastSat = satTimeline[satTimeline.length - 1]; const fairness = analytics?.fairness_score ?? (lastSat ? 100 - Math.abs(lastSat.score_a - lastSat.score_b) : null); const userA = participants[0]; const userB = participants[1]; return ( {/* ── Header ── */}

{id}

{STATUS_LABELS[negotiation.status]} {FEATURE_LABELS[negotiation.feature_type]}

Started {relativeTime(negotiation.created_at)} {liveRound && ( · Round {liveRound} processing… )}

{connected ? "Live" : "Offline"}
{isLive && ( Negotiating · Rd {liveRound ?? rounds.length} )}
{/* ── Participants ── */} {participants.length > 0 && (
{[userA, userB].filter(Boolean).map((p, i) => ( ))}
)} {/* ── Main 2-col grid ── */}
{/* Left */}
{fairness !== null && (
)} {(negotiation.status === "resolved" || negotiation.status === "escalated") && (

View Settlement Details

Blockchain record · UPI · Full transcript

)}
{/* Right */}
{concessions.length > 0 && (
)}
); } // ─── Sub-components ─────────────────────────────────────────────────────────── function Shell({ children }: { children: React.ReactNode }) { return (
{/* Top bar */}

AgentMesh

{children}
); } function Section({ title, icon, children }: { title: string; icon: string; children: React.ReactNode }) { return (

{title}

{children}
); } function ParticipantCard({ participant, label, }: { // eslint-disable-next-line @typescript-eslint/no-explicit-any participant: any; label: string; }) { if (!participant) return null; const pers = (participant.personality_used ?? "balanced") as Personality; const name = participant.display_name || participant.username || `Agent ${label}`; const isA = label === "A"; return (
{label}

{name}

{PERSONALITY_LABELS[pers]}
); } function LoadingState() { return (
Loading negotiation…
); } function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) { return (

{message}

); }