profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
landing-hero.tsx 7,617 bytes
1 import { ArrowUpRight, Check, Gavel, UsersRound } from 'lucide-react';
2 import Link from 'next/link';
3 import { DecisionStarter } from '@/components/decision-starter';
4 import { Badge } from '@/components/ui/badge';
5 import type { DebateResult } from '@/core/types';
6 import { truncate } from '@/lib/utils';
7
8 const FALLBACK_QUESTION = 'Should I take the higher-paying startup job or stay in my stable role?';
9 const FALLBACK_OPINIONS = [
10 {
11 name: 'Growth view',
12 content:
13 'Take the startup role if the manager is strong and you have enough savings for the added risk.',
14 },
15 {
16 name: 'Risk view',
17 content:
18 'Stay unless the new role gives you specific skills and ownership that your current job cannot offer.',
19 },
20 {
21 name: 'Values view',
22 content:
23 'Choose the role that fits the life you want for the next two years, not salary alone.',
24 },
25 ];
26 const FALLBACK_VERDICT =
27 'Take the startup role only if it clearly improves your manager, ownership, and learning. Keep the stable role if the move is mainly about salary.';
28
29 export function LandingHero({ result }: { result?: DebateResult | null }) {
30 const question = result?.config.question ?? FALLBACK_QUESTION;
31 const opinions =
32 result?.initialAnswers.slice(0, 3).map((answer) => ({
33 name:
34 result.participants.find((participant) => participant.id === answer.participantId)
35 ?.displayName ?? 'AI view',
36 content: compact(answer.content, 150),
37 })) ?? FALLBACK_OPINIONS;
38 const verdict = compact(result?.synthesis?.finalAnswer ?? FALLBACK_VERDICT, 320);
39
40 return (
41 <section className="container relative py-14 sm:py-20 lg:py-24">
42 <div className="grid items-center gap-14 lg:grid-cols-[minmax(0,1.02fr)_minmax(28rem,0.98fr)] lg:gap-16">
43 <div className="relative z-10">
44 <Badge variant="secondary" className="mb-6 gap-2 rounded-full px-3 py-1.5 font-medium">
45 <UsersRound className="h-3.5 w-3.5 text-primary" />
46 One question, several independent opinions
47 </Badge>
48 <h1 className="max-w-3xl font-display text-5xl font-semibold leading-[0.98] tracking-[-0.045em] sm:text-6xl lg:text-7xl">
49 Ask once. Get the <span className="text-primary">whole picture.</span>
50 </h1>
51 <p className="mt-6 max-w-xl text-lg leading-relaxed text-muted-foreground sm:text-xl">
52 Roundtable asks leading AI models separately, lets them challenge each other, then gives
53 you one clear recommendation and shows where they disagree.
54 </p>
55 <div className="mt-8 max-w-2xl">
56 <DecisionStarter />
57 </div>
58 <div className="mt-5 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-muted-foreground">
59 <span className="flex items-center gap-1.5">
60 <Check className="h-3.5 w-3.5 text-emerald-500" /> Smart defaults included
61 </span>
62 <span className="flex items-center gap-1.5">
63 <Check className="h-3.5 w-3.5 text-emerald-500" /> Every opinion stays visible
64 </span>
65 <Link
66 href="/demo"
67 className="font-medium text-foreground underline-offset-4 hover:underline"
68 >
69 See example verdicts
70 </Link>
71 </div>
72 </div>
73
74 <VerdictStack
75 question={question}
76 opinions={opinions}
77 verdict={verdict}
78 demoId={result?.debateId}
79 />
80 </div>
81 </section>
82 );
83 }
84
85 function VerdictStack({
86 question,
87 opinions,
88 verdict,
89 demoId,
90 }: {
91 question: string;
92 opinions: { name: string; content: string }[];
93 verdict: string;
94 demoId?: string;
95 }) {
96 return (
97 <div className="relative mx-auto w-full max-w-xl">
98 <div className="absolute -inset-8 -z-10 rounded-full bg-primary/10 blur-3xl" />
99 <div className="rounded-[1.75rem] border bg-card/95 p-4 shadow-2xl shadow-foreground/10 ring-1 ring-foreground/[0.04] sm:p-5">
100 <div className="flex items-center justify-between gap-4 border-b pb-4">
101 <div className="min-w-0">
102 <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
103 Question on the table
104 </p>
105 <p className="mt-1.5 text-sm font-semibold leading-snug">{truncate(question, 120)}</p>
106 </div>
107 <div
108 className="flex shrink-0 -space-x-2"
109 aria-label={`${opinions.length} independent opinions`}
110 >
111 {opinions.map((opinion, index) => (
112 <span
113 key={opinion.name}
114 className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card text-[11px] font-bold text-white"
115 style={{
116 backgroundColor: `hsl(var(--stage-${['answer', 'critique', 'revision'][index] ?? 'answer'}))`,
117 }}
118 >
119 {index + 1}
120 </span>
121 ))}
122 </div>
123 </div>
124
125 <div className="relative mt-4 space-y-2.5">
126 {opinions.map((opinion, index) => (
127 <div
128 key={`${opinion.name}-${index}`}
129 className="rounded-xl border bg-background/80 p-3.5 transition-transform duration-300 hover:translate-x-1"
130 style={{
131 marginLeft: `${index * 10}px`,
132 marginRight: `${(opinions.length - index - 1) * 10}px`,
133 }}
134 >
135 <div className="flex items-center gap-2">
136 <span
137 className="h-2 w-2 rounded-full"
138 style={{
139 backgroundColor: `hsl(var(--stage-${['answer', 'critique', 'revision'][index] ?? 'answer'}))`,
140 }}
141 />
142 <span className="text-xs font-semibold">{opinion.name}</span>
143 <span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
144 independent
145 </span>
146 </div>
147 <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
148 {opinion.content}
149 </p>
150 </div>
151 ))}
152 </div>
153
154 <div className="relative mt-4 overflow-hidden rounded-2xl border border-emerald-500/25 bg-emerald-500/[0.07] p-5">
155 <div className="absolute right-0 top-0 h-28 w-28 -translate-y-8 translate-x-8 rounded-full bg-emerald-400/15 blur-2xl" />
156 <div className="relative flex items-start gap-3">
157 <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-emerald-500 text-white shadow-sm">
158 <Gavel className="h-4 w-4" />
159 </span>
160 <div>
161 <p className="text-xs font-bold uppercase tracking-[0.14em] text-emerald-700 dark:text-emerald-300">
162 The verdict
163 </p>
164 <p className="mt-2 text-sm leading-relaxed text-foreground/90">{verdict}</p>
165 </div>
166 </div>
167 </div>
168
169 {demoId && (
170 <Link
171 href={`/demo/${demoId}`}
172 className="mt-4 flex items-center justify-center gap-1.5 rounded-xl py-2 text-xs font-semibold text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
173 >
174 Open this real example
175 <ArrowUpRight className="h-3.5 w-3.5" />
176 </Link>
177 )}
178 </div>
179 </div>
180 );
181 }
182
183 function compact(value: string, length: number): string {
184 return truncate(value.replace(/\s+/g, ' ').trim(), length);
185 }
186