|
|
@@ -1,7 +1,8 @@ |
| 1 |
1 |
'use client'; |
| 2 |
2 |
|
| 3 |
|
-import { AlertTriangle, Loader2, Play, RotateCcw, Save, Sparkles, Square } from 'lucide-react'; |
|
3 |
+import { AlertTriangle, KeyRound, Loader2, Play, RotateCcw, Save, Sparkles, Square } from 'lucide-react'; |
| 4 |
4 |
import { useEffect, useMemo, useState } from 'react'; |
|
5 |
+import { ApiKeyDialog } from '@/components/api-key-dialog'; |
| 5 |
6 |
import { DebateConsole } from '@/components/debate/debate-console'; |
| 6 |
7 |
import { ModelCombobox } from '@/components/debate/model-combobox'; |
| 7 |
8 |
import { ModelPicker } from '@/components/debate/model-picker'; |
|
|
@@ -13,6 +14,7 @@import { Label } from '@/components/ui/label'; |
| 13 |
14 |
import { Slider } from '@/components/ui/slider'; |
| 14 |
15 |
import { toast } from '@/components/ui/sonner'; |
| 15 |
16 |
import { chairmanSharesProvider, DEFAULT_CONVERGENCE_MODEL } from '@/core/models'; |
|
17 |
+import type { DebateResult } from '@/core/types'; |
| 16 |
18 |
import { useDebateStream } from '@/hooks/use-debate-stream'; |
| 17 |
19 |
import { useModels } from '@/hooks/use-models'; |
| 18 |
20 |
import { estimateDebateCostUsd } from '@/lib/cost-estimate'; |
|
|
@@ -31,8 +33,8 @@interface Preset { |
| 31 |
33 |
|
| 32 |
34 |
const EXAMPLE = 'Should a small startup build on a monolith or microservices? Give a decisive recommendation.'; |
| 33 |
35 |
|
| 34 |
|
-export function NewDebate() { |
| 35 |
|
- const { view, phase, errorMsg, start, cancel, reset } = useDebateStream(); |
|
36 |
+export function NewDebate({ fromDebateId }: { fromDebateId?: string }) { |
|
37 |
+ const { view, phase, errorMsg, errorCode, start, cancel, reset } = useDebateStream(); |
| 36 |
38 |
const { models } = useModels(); |
| 37 |
39 |
|
| 38 |
40 |
const [question, setQuestion] = useState(''); |
|
|
@@ -44,6 +46,42 @@export function NewDebate() { |
| 44 |
46 |
const [temperature, setTemperature] = useState(0.7); |
| 45 |
47 |
const [presets, setPresets] = useState<Preset[]>([]); |
| 46 |
48 |
const [presetName, setPresetName] = useState(''); |
|
49 |
+ const [keyDialogOpen, setKeyDialogOpen] = useState(false); |
|
50 |
+ const [needsKey, setNeedsKey] = useState(false); |
|
51 |
+ const [prefilled, setPrefilled] = useState(false); |
|
52 |
+ |
|
53 |
+ // Prefill the whole form from a previous debate (/debate?from=<id>). |
|
54 |
+ useEffect(() => { |
|
55 |
+ if (!fromDebateId || prefilled) return; |
|
56 |
+ let cancelled = false; |
|
57 |
+ fetch(`/api/debates/${fromDebateId}`) |
|
58 |
+ .then((r) => (r.ok ? (r.json() as Promise<DebateResult>) : null)) |
|
59 |
+ .then((result) => { |
|
60 |
+ if (!result || cancelled) return; |
|
61 |
+ setQuestion(result.config.question); |
|
62 |
+ setCouncil(result.config.models); |
|
63 |
+ setChairman(result.config.chairmanModel); |
|
64 |
+ setConvergenceModel(result.config.convergenceModel); |
|
65 |
+ setMaxRounds(result.config.maxRounds); |
|
66 |
+ setThreshold(result.config.convergenceThreshold); |
|
67 |
+ setTemperature(result.config.temperature); |
|
68 |
+ setPrefilled(true); |
|
69 |
+ toast.message('Loaded question and council from the previous debate'); |
|
70 |
+ }) |
|
71 |
+ .catch(() => {}); |
|
72 |
+ return () => { |
|
73 |
+ cancelled = true; |
|
74 |
+ }; |
|
75 |
+ }, [fromDebateId, prefilled]); |
|
76 |
+ |
|
77 |
+ // A NO_KEY failure is a setup problem, not a debate failure: send the user |
|
78 |
+ // straight to the key dialog with their composed debate intact. |
|
79 |
+ useEffect(() => { |
|
80 |
+ if (errorCode !== 'NO_KEY') return; |
|
81 |
+ setNeedsKey(true); |
|
82 |
+ setKeyDialogOpen(true); |
|
83 |
+ reset(); |
|
84 |
+ }, [errorCode, reset]); |
| 47 |
85 |
|
| 48 |
86 |
// Seed a sensible default council + chairman once the catalog arrives. |
| 49 |
87 |
useEffect(() => { |
|
|
@@ -70,7 +108,17 @@export function NewDebate() { |
| 70 |
108 |
}, []); |
| 71 |
109 |
|
| 72 |
110 |
const providerConflict = chairman && council.length > 0 && chairmanSharesProvider(chairman, council); |
| 73 |
|
- const canStart = question.trim().length >= 3 && council.length >= 3 && council.length <= 6 && Boolean(chairman); |
|
111 |
+ const startBlocker = |
|
112 |
+ question.trim().length < 3 |
|
113 |
+ ? 'Write a question to get started' |
|
114 |
+ : council.length < 3 |
|
115 |
+ ? `Pick at least 3 council models (${council.length} selected)` |
|
116 |
+ : council.length > 6 |
|
117 |
+ ? 'A council holds at most 6 models' |
|
118 |
+ : !chairman |
|
119 |
+ ? 'Pick a chairman to write the final verdict' |
|
120 |
+ : null; |
|
121 |
+ const canStart = startBlocker === null; |
| 74 |
122 |
const running = phase === 'connecting' || phase === 'streaming'; |
| 75 |
123 |
|
| 76 |
124 |
const priceMap = useMemo( |
|
|
@@ -287,22 +335,41 @@export function NewDebate() { |
| 287 |
335 |
</CardContent> |
| 288 |
336 |
</Card> |
| 289 |
337 |
|
|
338 |
+ {needsKey && ( |
|
339 |
+ <div className="flex flex-wrap items-center gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-sm"> |
|
340 |
+ <KeyRound className="h-4 w-4 text-amber-500" /> |
|
341 |
+ <span>Running a real debate needs an OpenRouter key. Add one, then press Start again.</span> |
|
342 |
+ <Button variant="outline" size="sm" className="ml-auto" onClick={() => setKeyDialogOpen(true)}> |
|
343 |
+ Add key |
|
344 |
+ </Button> |
|
345 |
+ </div> |
|
346 |
+ )} |
|
347 |
+ |
| 290 |
348 |
{errorMsg && ( |
| 291 |
349 |
<div className="flex items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive"> |
| 292 |
350 |
<AlertTriangle className="h-4 w-4" /> {errorMsg} |
| 293 |
351 |
</div> |
| 294 |
352 |
)} |
| 295 |
353 |
|
| 296 |
354 |
<div className="sticky bottom-4 flex items-center justify-end gap-3"> |
| 297 |
|
- {canStart && ( |
| 298 |
|
- <span className="rounded-md bg-background/80 px-2.5 py-1.5 text-xs text-muted-foreground shadow-sm backdrop-blur"> |
| 299 |
|
- ~{formatUsd(estimate)} est. · up to {maxRounds} {maxRounds === 1 ? 'round' : 'rounds'} |
| 300 |
|
- </span> |
| 301 |
|
- )} |
|
355 |
+ <span className="rounded-md bg-background/80 px-2.5 py-1.5 text-xs text-muted-foreground shadow-sm backdrop-blur"> |
|
356 |
+ {canStart |
|
357 |
+ ? `~${formatUsd(estimate)} est. · up to ${maxRounds} ${maxRounds === 1 ? 'round' : 'rounds'}` |
|
358 |
+ : startBlocker} |
|
359 |
+ </span> |
| 302 |
360 |
<Button size="lg" disabled={!canStart} onClick={launch} className="shadow-lg"> |
| 303 |
361 |
<Play className="h-4 w-4" /> Start debate |
| 304 |
362 |
</Button> |
| 305 |
363 |
</div> |
|
364 |
+ |
|
365 |
+ <ApiKeyDialog |
|
366 |
+ open={keyDialogOpen} |
|
367 |
+ onOpenChange={setKeyDialogOpen} |
|
368 |
+ onChanged={() => { |
|
369 |
+ setNeedsKey(false); |
|
370 |
+ setKeyDialogOpen(false); |
|
371 |
+ }} |
|
372 |
+ /> |
| 306 |
373 |
</div> |
| 307 |
374 |
); |
| 308 |
375 |
} |