profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
final-answer.tsx 8,376 bytes
1 'use client';
2
3 import { AlertTriangle, ChevronDown, Gavel } from 'lucide-react';
4 import { RichText } from '@/components/debate/rich-text';
5 import { Badge } from '@/components/ui/badge';
6 import { CopyButton } from '@/components/ui/copy-button';
7 import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
8 import {
9 displayNameForModel,
10 type Participant,
11 type ProvenanceRecord,
12 type SynthesisRecord,
13 } from '@/core/types';
14 import { participantColor, participantTag } from '@/lib/model-visuals';
15
16 export function FinalAnswer({
17 synthesis,
18 participants,
19 chairmanProviderConflict,
20 provenance,
21 }: {
22 synthesis: SynthesisRecord;
23 participants: Participant[];
24 chairmanProviderConflict: boolean;
25 provenance?: ProvenanceRecord | null;
26 }) {
27 const byId = new Map(
28 participants.map((participant, index) => [participant.id, { participant, index }]),
29 );
30 const untracedCount = provenance?.claims.filter((claim) => claim.unsourced).length ?? 0;
31
32 return (
33 <div className="overflow-hidden rounded-[1.25rem] border-2 border-emerald-500/25 bg-card shadow-lg shadow-emerald-500/5">
34 <div className="flex flex-wrap items-center gap-3 border-b border-emerald-500/20 bg-emerald-500/[0.055] p-4 sm:p-5">
35 <span className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500 text-white shadow-sm">
36 <Gavel className="h-4 w-4" />
37 </span>
38 <div>
39 <div className="text-[10px] font-bold uppercase tracking-[0.16em] text-emerald-700 dark:text-emerald-300">
40 Final recommendation
41 </div>
42 <div className="mt-0.5 font-display text-lg font-semibold">The verdict</div>
43 <div className="text-[11px] text-muted-foreground">
44 Combined by {displayNameForModel(synthesis.model)}
45 </div>
46 </div>
47 <div className="ml-auto flex items-center gap-2">
48 {chairmanProviderConflict && (
49 <Tooltip>
50 <TooltipTrigger asChild>
51 <Badge variant="warning" className="cursor-help gap-1">
52 <AlertTriangle className="h-3 w-3" /> model mix
53 </Badge>
54 </TooltipTrigger>
55 <TooltipContent className="max-w-xs">
56 The same AI provider appears in the opinion group and the verdict model. Names were
57 hidden during review to reduce bias.
58 </TooltipContent>
59 </Tooltip>
60 )}
61 <CopyButton text={synthesis.finalAnswer} label="Copy verdict" />
62 </div>
63 </div>
64
65 <div className="p-5 sm:p-6">
66 <RichText text={synthesis.finalAnswer} className="text-[15px] leading-relaxed" />
67
68 {provenance && provenance.claims.length > 0 && (
69 <details className="group mt-6 rounded-xl border bg-background/55">
70 <summary className="flex cursor-pointer list-none items-center gap-2 p-4 text-sm font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
71 How this verdict was checked
72 {untracedCount > 0 ? (
73 <Badge variant="warning">{untracedCount} new in verdict</Badge>
74 ) : (
75 <Badge variant="success">Every claim traced</Badge>
76 )}
77 <ChevronDown className="ml-auto h-4 w-4 text-muted-foreground transition-transform group-open:rotate-180" />
78 </summary>
79 <div className="border-t p-4">
80 <p className="mb-3 text-xs leading-relaxed text-muted-foreground">
81 Each important claim is matched to the opinions that support or challenge it. A new
82 claim is one that appeared only when the final verdict was written.
83 </p>
84 <ul className="space-y-2.5">
85 {provenance.claims.map((claim, claimIndex) => (
86 <li key={claimIndex} className="flex items-start gap-2 text-xs">
87 <span className="mt-1 flex shrink-0 items-center gap-0.5">
88 {claim.unsourced ? (
89 <span
90 className="h-2 w-2 rounded-full bg-amber-500"
91 title="New in the final verdict"
92 />
93 ) : (
94 claim.supportedBy.map((support) => {
95 const entry = byId.get(support.participantId);
96 const index = entry?.index ?? 0;
97 return (
98 <span
99 key={support.participantId}
100 className="flex h-4 w-4 items-center justify-center rounded text-[9px] font-bold text-white"
101 style={{ backgroundColor: participantColor(index) }}
102 title={`Supported by ${entry?.participant.displayName ?? support.model}`}
103 >
104 {participantTag(index)}
105 </span>
106 );
107 })
108 )}
109 </span>
110 <span
111 className={claim.unsourced ? 'text-amber-600 dark:text-amber-400' : undefined}
112 >
113 {claim.text}
114 {claim.unsourced && (
115 <span className="ml-1 font-medium">(new in the verdict)</span>
116 )}
117 {claim.contestedBy.length > 0 && (
118 <span className="text-muted-foreground">
119 {' '}
120 ยท challenged by{' '}
121 {claim.contestedBy
122 .map(
123 (challenge) =>
124 byId.get(challenge.participantId)?.participant.displayName ??
125 displayNameForModel(challenge.model),
126 )
127 .join(', ')}
128 </span>
129 )}
130 </span>
131 </li>
132 ))}
133 </ul>
134 </div>
135 </details>
136 )}
137
138 {synthesis.dissent.length > 0 && (
139 <details className="group mt-3 rounded-xl border bg-background/55">
140 <summary className="flex cursor-pointer list-none items-center gap-2 p-4 text-sm font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
141 Where opinions still differ
142 <Badge variant="secondary">{synthesis.dissent.length}</Badge>
143 <ChevronDown className="ml-auto h-4 w-4 text-muted-foreground transition-transform group-open:rotate-180" />
144 </summary>
145 <div className="space-y-4 border-t p-4">
146 {synthesis.dissent.map((dissent, dissentIndex) => (
147 <div key={dissentIndex}>
148 <div className="text-sm font-medium">{dissent.topic}</div>
149 <ul className="mt-2 space-y-2">
150 {dissent.positions.map((position, positionIndex) => {
151 const entry = byId.get(position.participantId);
152 const index = entry?.index ?? 0;
153 const name =
154 entry?.participant.displayName ?? displayNameForModel(position.model);
155 return (
156 <li key={positionIndex} className="flex items-start gap-2 text-xs">
157 <span
158 className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded text-[9px] font-bold text-white"
159 style={{ backgroundColor: participantColor(index) }}
160 >
161 {participantTag(index)}
162 </span>
163 <span>
164 <span className="font-medium">{name}:</span>{' '}
165 <span className="text-muted-foreground">{position.position}</span>
166 </span>
167 </li>
168 );
169 })}
170 </ul>
171 </div>
172 ))}
173 </div>
174 </details>
175 )}
176 </div>
177 </div>
178 );
179 }
180