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 | |