@lemonriver/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
  2. package/dist/cli/bin.js +4 -4
  3. package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
  4. package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
  5. package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +4 -3
  8. package/dist/locale/index.d.ts +5 -2
  9. package/dist/locale/index.js +2 -2
  10. package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
  11. package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
  12. package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
  13. package/dist/vite/index.d.ts +1 -1
  14. package/dist/vite/index.js +1 -1
  15. package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
  16. package/package.json +13 -6
  17. package/skills/apply-comments/SKILL.md +10 -6
  18. package/skills/create-slide/SKILL.md +2 -0
  19. package/skills/current-slide/SKILL.md +1 -0
  20. package/skills/slide-authoring/SKILL.md +31 -1
  21. package/src/agent/prompt.ts +141 -0
  22. package/src/agent/provider.ts +123 -0
  23. package/src/agent/types.ts +151 -0
  24. package/src/app/app.tsx +2 -0
  25. package/src/app/assets/open-slide.svg +39 -0
  26. package/src/app/components/agent/agent-dock.tsx +35 -0
  27. package/src/app/components/agent/agent-markdown.tsx +45 -0
  28. package/src/app/components/agent/agent-panel.tsx +692 -0
  29. package/src/app/components/agent/agent-presence.tsx +74 -0
  30. package/src/app/components/agent/composer.tsx +120 -0
  31. package/src/app/components/agent/connect-view.tsx +184 -0
  32. package/src/app/components/agent/history-view.tsx +135 -0
  33. package/src/app/components/agent/provider-logo.tsx +34 -0
  34. package/src/app/components/agent/transcript.tsx +609 -0
  35. package/src/app/components/board/board-canvas.tsx +143 -12
  36. package/src/app/components/board/board.css +51 -0
  37. package/src/app/components/board/layout.test.ts +53 -1
  38. package/src/app/components/board/layout.ts +50 -2
  39. package/src/app/components/board/page-node.tsx +287 -13
  40. package/src/app/components/board/use-board-cards.ts +210 -0
  41. package/src/app/components/board/use-page-order.ts +17 -9
  42. package/src/app/components/command/slide-command-menu.tsx +10 -0
  43. package/src/app/components/icon-switcher-indicator.tsx +1 -1
  44. package/src/app/components/inspector/comment-layer.tsx +1011 -0
  45. package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
  46. package/src/app/components/inspector/inspect-overlay.tsx +2 -39
  47. package/src/app/components/inspector/inspector-panel.tsx +10 -93
  48. package/src/app/components/inspector/inspector-provider.tsx +121 -47
  49. package/src/app/components/panel/editor-sidebar.tsx +85 -16
  50. package/src/app/components/panel/panel-shell.tsx +2 -2
  51. package/src/app/components/panel/resize-handle.tsx +129 -0
  52. package/src/app/components/player.tsx +0 -1
  53. package/src/app/components/preview-step-host.tsx +16 -0
  54. package/src/app/components/sidebar/sidebar.tsx +2 -2
  55. package/src/app/components/style-panel/style-panel.tsx +3 -2
  56. package/src/app/components/thumbnail-rail.tsx +76 -1
  57. package/src/app/favicon.ico +0 -0
  58. package/src/app/favicon.svg +39 -0
  59. package/src/app/index.html +2 -1
  60. package/src/app/lib/agent/use-agent.ts +543 -0
  61. package/src/app/lib/inspector/freeze-motion.ts +61 -0
  62. package/src/app/lib/inspector/use-comments.ts +32 -3
  63. package/src/app/lib/locale-store.ts +6 -1
  64. package/src/app/lib/print-ready.ts +9 -6
  65. package/src/app/lib/use-auto-grow.ts +15 -0
  66. package/src/app/lib/utils.ts +4 -0
  67. package/src/app/routes/board.tsx +48 -11
  68. package/src/app/routes/home.tsx +43 -7
  69. package/src/app/routes/presenter.tsx +1 -16
  70. package/src/app/routes/preview.tsx +169 -0
  71. package/src/app/routes/slide.tsx +221 -173
  72. package/src/app/styles.css +234 -80
  73. package/src/editing/placeholder-id.ts +1 -0
  74. package/src/locale/en.ts +127 -5
  75. package/src/locale/es.ts +692 -0
  76. package/src/locale/index.ts +1 -0
  77. package/src/locale/ja.ts +120 -6
  78. package/src/locale/types.ts +132 -5
  79. package/src/locale/zh-cn.ts +117 -5
  80. package/src/locale/zh-tw.ts +117 -5
  81. package/src/app/assets/open-slide.png +0 -0
  82. package/src/app/components/inspector/comment-widget.tsx +0 -119
@@ -0,0 +1,692 @@
1
+ import {
2
+ Check,
3
+ ChevronDown,
4
+ ChevronLeft,
5
+ History,
6
+ MessageSquare,
7
+ Palette,
8
+ Pencil,
9
+ Plus,
10
+ Presentation,
11
+ Settings2,
12
+ X,
13
+ } from 'lucide-react';
14
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
15
+ import { useNavigate } from 'react-router-dom';
16
+ import { toast } from 'sonner';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
19
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
20
+ import {
21
+ autoConnect,
22
+ clearNewDeck,
23
+ consumeRequest,
24
+ createSession,
25
+ deleteSession,
26
+ readyProviders,
27
+ resolveModelChoice,
28
+ respond,
29
+ sendMessage,
30
+ setActiveSession,
31
+ setAutoApprove,
32
+ setModelChoice,
33
+ stopSession,
34
+ useAgentSession,
35
+ useAgentSessions,
36
+ useAgentState,
37
+ useAgentStatus,
38
+ useCreateDeck,
39
+ } from '@/lib/agent/use-agent';
40
+ import { format, plural, useLocale } from '@/lib/use-locale';
41
+ import { cn } from '@/lib/utils';
42
+ import { applyCommentsPrompt } from '../../../agent/prompt';
43
+ import type {
44
+ AgentEffort,
45
+ AgentModelOption,
46
+ AgentProviderId,
47
+ AgentTurnContext,
48
+ RespondPayload,
49
+ } from '../../../agent/types';
50
+ import { Composer, ContextChip } from './composer';
51
+ import { ConnectView } from './connect-view';
52
+ import { HistoryView } from './history-view';
53
+ import { ProviderLogo } from './provider-logo';
54
+ import { Transcript } from './transcript';
55
+
56
+ const CREATE_THEME_PROMPT =
57
+ 'Use the create-theme skill to create a reusable theme. Ask me what it should look like first.';
58
+
59
+ export function AgentPanel({
60
+ context,
61
+ onClose,
62
+ }: {
63
+ context: AgentTurnContext;
64
+ onClose: () => void;
65
+ }) {
66
+ const t = useLocale();
67
+ const { status, loading, connected, provider, model, effort, modelChoices } = useAgentStatus();
68
+ const { sessions, loaded: sessionsLoaded, activeId } = useAgentSessions();
69
+ const session = useAgentSession(activeId);
70
+ const { request, autoApprove, newDeck } = useAgentState();
71
+ const [draft, setDraft] = useState('');
72
+ const [sending, setSending] = useState(false);
73
+ const sendingRef = useRef(false);
74
+ const [manage, setManage] = useState(false);
75
+ const [view, setView] = useState<'chat' | 'history'>('chat');
76
+ const navigate = useNavigate();
77
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
78
+ const running = session?.status === 'running';
79
+ const showConnect = !loading && (!connected || manage);
80
+
81
+ useEffect(() => {
82
+ if (status) void autoConnect();
83
+ }, [status]);
84
+
85
+ const contextRef = useRef(context);
86
+ contextRef.current = context;
87
+
88
+ // Arriving at another deck starts a new conversation; the previous one stays
89
+ // in history. Picking a chat from history navigates to its own deck, so it
90
+ // survives this check.
91
+ const slideId = context.slideId ?? null;
92
+ const checkedSlideRef = useRef<string | null | undefined>(undefined);
93
+ useEffect(() => {
94
+ if (!sessionsLoaded || checkedSlideRef.current === slideId) return;
95
+ checkedSlideRef.current = slideId;
96
+ const active = activeId ? sessions.find((s) => s.id === activeId) : null;
97
+ if (active?.slideId && active.slideId !== slideId) setActiveSession(null);
98
+ }, [slideId, sessionsLoaded, activeId, sessions]);
99
+ const isNewDeck = Boolean(context.slideId) && newDeck === context.slideId;
100
+
101
+ const ensureSession = useCallback(
102
+ async (fresh: boolean) => {
103
+ const current = activeId ? sessions.find((s) => s.id === activeId) : null;
104
+ if (!provider) throw new Error('no agent connected');
105
+ if (!fresh && current && current.status !== 'running' && current.provider === provider.id) {
106
+ return current.id;
107
+ }
108
+ const created = await createSession(provider.id, contextRef.current.slideId ?? null);
109
+ return created.id;
110
+ },
111
+ [activeId, sessions, provider],
112
+ );
113
+
114
+ const send = useCallback(
115
+ async (text: string, fresh = false) => {
116
+ const trimmed = text.trim();
117
+ // A second agent would edit the same deck alongside the running one.
118
+ if (!trimmed || sendingRef.current || (running && !fresh)) return;
119
+ sendingRef.current = true;
120
+ setSending(true);
121
+ try {
122
+ const id = await ensureSession(fresh || isNewDeck);
123
+ setDraft('');
124
+ await sendMessage(
125
+ id,
126
+ trimmed,
127
+ isNewDeck ? { ...contextRef.current, newDeck: true } : contextRef.current,
128
+ );
129
+ if (isNewDeck) clearNewDeck();
130
+ } catch (err) {
131
+ toast.error(t.agent.toastSendFailed, {
132
+ description: err instanceof Error ? err.message : undefined,
133
+ });
134
+ } finally {
135
+ sendingRef.current = false;
136
+ setSending(false);
137
+ }
138
+ },
139
+ [ensureSession, isNewDeck, running, t],
140
+ );
141
+
142
+ const createDeck = useCreateDeck();
143
+
144
+ useEffect(() => {
145
+ if (isNewDeck && connected) textareaRef.current?.focus();
146
+ }, [isNewDeck, connected]);
147
+
148
+ // A request made while a turn runs waits for it to end, then joins the same
149
+ // conversation.
150
+ useEffect(() => {
151
+ if (!request || loading || !connected || running) return;
152
+ const req = consumeRequest();
153
+ if (!req) return;
154
+ setManage(false);
155
+ void send(req.text);
156
+ }, [request, loading, connected, running, send]);
157
+
158
+ const onRespond = useCallback(
159
+ async (itemId: string, payload: RespondPayload) => {
160
+ if (!session) return;
161
+ try {
162
+ await respond(session.id, itemId, payload, contextRef.current);
163
+ } catch (err) {
164
+ toast.error(err instanceof Error ? err.message : String(err));
165
+ }
166
+ },
167
+ [session],
168
+ );
169
+
170
+ const providerName = provider?.name ?? '';
171
+ const onEdit = useCallback(
172
+ async (itemId: string, text: string) => {
173
+ if (!session) return;
174
+ try {
175
+ await sendMessage(session.id, text, contextRef.current, itemId);
176
+ } catch (err) {
177
+ toast.error(t.agent.toastSendFailed, {
178
+ description: err instanceof Error ? err.message : undefined,
179
+ });
180
+ }
181
+ },
182
+ [session, t],
183
+ );
184
+
185
+ const pendingComments = context.pendingComments ?? 0;
186
+
187
+ const chips: ReactNode = context.slideId ? (
188
+ <>
189
+ <ContextChip title={context.slideId}>
190
+ {context.slideTitle ?? context.slideId}
191
+ {context.pageNumber ? ` · ${format(t.agent.contextPage, { n: context.pageNumber })}` : ''}
192
+ </ContextChip>
193
+ {context.selection && (
194
+ <ContextChip title={context.selection.text || undefined}>
195
+ {`<${context.selection.tagName}>`} {t.agent.contextSelection}
196
+ </ContextChip>
197
+ )}
198
+ {pendingComments > 0 && (
199
+ <ContextChip
200
+ tone="brand"
201
+ title={t.agent.quickApplyHint}
202
+ onClick={
203
+ running
204
+ ? undefined
205
+ : () => context.slideId && send(applyCommentsPrompt(context.slideId))
206
+ }
207
+ >
208
+ {format(plural(pendingComments, t.agent.pendingComments), { count: pendingComments })} ·{' '}
209
+ {t.agent.applyComments}
210
+ </ContextChip>
211
+ )}
212
+ </>
213
+ ) : null;
214
+
215
+ return (
216
+ <div
217
+ data-agent-provider={provider?.id}
218
+ className="relative flex h-full min-h-0 flex-col bg-sidebar"
219
+ >
220
+ {running && <span aria-hidden className="agent-progress z-10" />}
221
+ <header className="relative z-10 flex h-9 shrink-0 items-center justify-between gap-1 bg-sidebar px-2">
222
+ <span
223
+ aria-hidden
224
+ className="pointer-events-none absolute inset-x-0 top-full h-5 bg-gradient-to-b from-sidebar to-transparent"
225
+ />
226
+ <div className="flex min-w-0 items-center gap-0.5">
227
+ {showConnect && connected ? (
228
+ <Button size="xs" variant="ghost" onClick={() => setManage(false)}>
229
+ {t.agent.backToChat}
230
+ </Button>
231
+ ) : view === 'history' ? (
232
+ <Button size="xs" variant="ghost" onClick={() => setView('chat')}>
233
+ <ChevronLeft className="size-3.5" />
234
+ {t.agent.sessions}
235
+ </Button>
236
+ ) : (
237
+ <ModelMenu
238
+ status={status}
239
+ providerId={provider?.id ?? null}
240
+ model={model}
241
+ effort={effort}
242
+ modelChoices={modelChoices}
243
+ autoApprove={autoApprove}
244
+ onManage={() => setManage(true)}
245
+ />
246
+ )}
247
+ <BetaBadge />
248
+ </div>
249
+ <div className="flex shrink-0 items-center gap-0.5">
250
+ {connected && !showConnect && (
251
+ <>
252
+ <Button
253
+ size="icon-sm"
254
+ variant="ghost"
255
+ aria-label={t.agent.sessions}
256
+ aria-pressed={view === 'history'}
257
+ title={t.agent.sessions}
258
+ className="aria-pressed:bg-muted aria-pressed:text-foreground"
259
+ onClick={() => setView((v) => (v === 'history' ? 'chat' : 'history'))}
260
+ >
261
+ <History className="size-3.5" />
262
+ </Button>
263
+ <Button
264
+ size="icon-sm"
265
+ variant="ghost"
266
+ aria-label={t.agent.newSession}
267
+ title={t.agent.newSession}
268
+ onClick={() => {
269
+ setActiveSession(null);
270
+ setView('chat');
271
+ setDraft('');
272
+ queueMicrotask(() => textareaRef.current?.focus());
273
+ }}
274
+ >
275
+ <Plus className="size-3.5" />
276
+ </Button>
277
+ </>
278
+ )}
279
+ <Button
280
+ size="icon-sm"
281
+ variant="ghost"
282
+ aria-label={t.agent.close}
283
+ title={t.agent.close}
284
+ onClick={onClose}
285
+ >
286
+ <X className="size-3.5" />
287
+ </Button>
288
+ </div>
289
+ </header>
290
+
291
+ {loading ? (
292
+ <p className="px-4 py-5 text-[12px] text-muted-foreground">{t.agent.statusLoading}…</p>
293
+ ) : showConnect ? (
294
+ <div className="min-h-0 flex-1 overflow-y-auto">
295
+ <ConnectView />
296
+ </div>
297
+ ) : view === 'history' ? (
298
+ <HistoryView
299
+ sessions={sessions}
300
+ activeId={activeId}
301
+ slideId={context.slideId ?? null}
302
+ onSelect={(s) => {
303
+ setActiveSession(s.id);
304
+ setView('chat');
305
+ if (s.slideId && s.slideId !== context.slideId) navigate(`/s/${s.slideId}`);
306
+ }}
307
+ onDelete={async (id) => {
308
+ try {
309
+ await deleteSession(id);
310
+ } catch (err) {
311
+ toast.error(err instanceof Error ? err.message : String(err));
312
+ }
313
+ }}
314
+ />
315
+ ) : (
316
+ <>
317
+ {session && session.items.length > 0 ? (
318
+ <Transcript session={session} onRespond={onRespond} onEdit={onEdit} />
319
+ ) : isNewDeck ? (
320
+ <NewDeckState providerName={providerName} />
321
+ ) : (
322
+ <EmptyState
323
+ pendingComments={pendingComments}
324
+ hasSlide={Boolean(context.slideId)}
325
+ onCreate={createDeck}
326
+ onApply={() => context.slideId && send(applyCommentsPrompt(context.slideId))}
327
+ onTheme={() => send(CREATE_THEME_PROMPT, true)}
328
+ onEdit={() => textareaRef.current?.focus()}
329
+ />
330
+ )}
331
+ <Composer
332
+ value={draft}
333
+ onChange={setDraft}
334
+ onSend={() => send(draft)}
335
+ onStop={() =>
336
+ session &&
337
+ stopSession(session.id).catch((err) => {
338
+ toast.error(err instanceof Error ? err.message : String(err));
339
+ })
340
+ }
341
+ running={Boolean(running)}
342
+ busy={sending}
343
+ placeholder={
344
+ isNewDeck
345
+ ? t.agent.newDeckPlaceholder
346
+ : format(t.agent.composerPlaceholder, { provider: providerName })
347
+ }
348
+ chips={chips}
349
+ textareaRef={textareaRef}
350
+ />
351
+ </>
352
+ )}
353
+ </div>
354
+ );
355
+ }
356
+
357
+ function modelLabel(model: AgentModelOption, t: ReturnType<typeof useLocale>): string {
358
+ return model.id ? model.label : t.agent.modelDefault;
359
+ }
360
+
361
+ function ModelMenu({
362
+ status,
363
+ providerId,
364
+ model,
365
+ effort,
366
+ modelChoices,
367
+ autoApprove,
368
+ onManage,
369
+ }: {
370
+ status: ReturnType<typeof useAgentStatus>['status'];
371
+ providerId: AgentProviderId | null;
372
+ model: AgentModelOption | null;
373
+ effort: AgentEffort | null;
374
+ modelChoices: ReturnType<typeof useAgentStatus>['modelChoices'];
375
+ autoApprove: boolean;
376
+ onManage: () => void;
377
+ }) {
378
+ const t = useLocale();
379
+ const [open, setOpen] = useState(false);
380
+ const providers = status?.enabled ? status.providers : [];
381
+ const [tab, setTab] = useState<AgentProviderId | null>(providerId);
382
+ const shownId = tab ?? providerId ?? providers[0]?.id ?? null;
383
+ const shown = providers.find((p) => p.id === shownId) ?? null;
384
+ const choice = shown?.ready ? resolveModelChoice(shown.models, modelChoices[shown.id]) : null;
385
+ const selectedModel = shown?.id === providerId ? (model ?? choice?.model) : choice?.model;
386
+ const selectedEffort = shown?.id === providerId ? effort : (choice?.effort ?? null);
387
+
388
+ return (
389
+ <Popover
390
+ open={open}
391
+ onOpenChange={(next) => {
392
+ setOpen(next);
393
+ if (next) setTab(providerId);
394
+ }}
395
+ >
396
+ <PopoverTrigger
397
+ type="button"
398
+ className="flex h-7 min-w-0 items-center gap-1.5 rounded-[5px] px-2 text-[12.5px] font-medium outline-none transition-colors duration-100 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/30 aria-expanded:bg-muted"
399
+ >
400
+ {providerId ? (
401
+ <ProviderLogo provider={providerId} className="size-3.5 shrink-0" />
402
+ ) : (
403
+ <span aria-hidden className="size-1.5 rounded-full bg-muted-foreground/50" />
404
+ )}
405
+ <span className="truncate">{model ? modelLabel(model, t) : t.agent.title}</span>
406
+ {effort && (
407
+ <span className="shrink-0 font-normal text-muted-foreground">
408
+ {t.agent.efforts[effort]}
409
+ </span>
410
+ )}
411
+ <ChevronDown className="size-3 text-muted-foreground" aria-hidden />
412
+ </PopoverTrigger>
413
+ <PopoverContent align="start" className="w-[272px] p-1.5" data-agent-model-menu>
414
+ {providers.length > 1 && (
415
+ <div role="tablist" className="mb-1.5 flex gap-0.5 rounded-[6px] bg-muted p-0.5">
416
+ {providers.map((p) => (
417
+ <button
418
+ key={p.id}
419
+ type="button"
420
+ role="tab"
421
+ aria-selected={p.id === shownId}
422
+ onClick={() => setTab(p.id)}
423
+ className={cn(
424
+ 'flex h-7 flex-1 items-center justify-center gap-1.5 rounded-[5px] text-[12px] font-medium text-muted-foreground outline-none transition-colors duration-100 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30',
425
+ p.id === shownId && 'bg-background text-foreground shadow-edge',
426
+ )}
427
+ >
428
+ <ProviderLogo provider={p.id} className="size-3.5 shrink-0" />
429
+ {p.name}
430
+ </button>
431
+ ))}
432
+ </div>
433
+ )}
434
+
435
+ {shown && !shown.ready ? (
436
+ <div className="flex flex-col items-start gap-2 px-2 py-2.5">
437
+ <p className="text-[12px] leading-relaxed text-muted-foreground">
438
+ {shown.cliFound
439
+ ? format(t.agent.connectInstallHint, {
440
+ pkg: shown.sdkPackage,
441
+ pm: status?.packageManager ?? 'npm',
442
+ })
443
+ : format(t.agent.cliHint, { name: shown.name })}
444
+ </p>
445
+ <Button
446
+ size="xs"
447
+ variant="outline"
448
+ onClick={() => {
449
+ setOpen(false);
450
+ onManage();
451
+ }}
452
+ >
453
+ {t.agent.connect}
454
+ </Button>
455
+ </div>
456
+ ) : shown ? (
457
+ <div className="flex flex-col gap-px">
458
+ {shown.models.map((m) => {
459
+ const checked = m.id === selectedModel?.id;
460
+ return (
461
+ <button
462
+ key={m.id}
463
+ type="button"
464
+ aria-pressed={checked}
465
+ onClick={() =>
466
+ setModelChoice(shown.id, {
467
+ model: m.id,
468
+ effort: modelChoices[shown.id]?.effort ?? null,
469
+ })
470
+ }
471
+ className={cn(
472
+ 'flex h-7 items-center justify-between gap-2 rounded-[5px] px-2 text-left text-[12.5px] outline-none transition-colors duration-100 hover:bg-muted focus-visible:bg-muted',
473
+ checked && 'font-medium',
474
+ )}
475
+ >
476
+ <span className="truncate">{modelLabel(m, t)}</span>
477
+ {checked && <Check className="size-3.5 shrink-0" aria-hidden />}
478
+ </button>
479
+ );
480
+ })}
481
+ </div>
482
+ ) : null}
483
+
484
+ {shown?.ready && selectedModel && selectedModel.efforts.length > 0 && (
485
+ <div className="mt-1.5 border-t border-hairline px-1 pt-2 pb-1">
486
+ <div className="mb-1.5 px-1 text-[11px] font-medium text-muted-foreground">
487
+ {t.agent.reasoning}
488
+ </div>
489
+ <div className="flex gap-0.5 rounded-[6px] bg-muted p-0.5">
490
+ {([null, ...selectedModel.efforts] as (AgentEffort | null)[]).map((e) => {
491
+ const checked = e === selectedEffort;
492
+ return (
493
+ <button
494
+ key={e ?? 'auto'}
495
+ type="button"
496
+ aria-pressed={checked}
497
+ onClick={() => setModelChoice(shown.id, { model: selectedModel.id, effort: e })}
498
+ className={cn(
499
+ 'h-6 min-w-0 flex-1 truncate rounded-[4px] px-1 text-[11.5px] text-muted-foreground outline-none transition-colors duration-100 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30',
500
+ checked && 'bg-background font-medium text-foreground shadow-edge',
501
+ )}
502
+ >
503
+ {e ? t.agent.efforts[e] : t.agent.effortAuto}
504
+ </button>
505
+ );
506
+ })}
507
+ </div>
508
+ </div>
509
+ )}
510
+
511
+ <div className="mt-1.5 flex flex-col gap-px border-t border-hairline pt-1.5">
512
+ {readyProviders(status).length > 0 && (
513
+ <button
514
+ type="button"
515
+ role="switch"
516
+ aria-checked={autoApprove}
517
+ title={t.agent.autoApproveHint}
518
+ onClick={() => setAutoApprove(!autoApprove)}
519
+ className="flex h-7 items-center justify-between gap-2 rounded-[5px] px-2 text-[12.5px] outline-none transition-colors duration-100 hover:bg-muted focus-visible:bg-muted"
520
+ >
521
+ {t.agent.autoApprove}
522
+ <span
523
+ aria-hidden
524
+ className={cn(
525
+ 'relative h-4 w-7 shrink-0 rounded-full transition-colors duration-150',
526
+ autoApprove ? 'bg-foreground' : 'bg-foreground/15',
527
+ )}
528
+ >
529
+ <span
530
+ className={cn(
531
+ 'absolute top-0.5 left-0.5 size-3 rounded-full bg-background shadow-sm transition-transform duration-150',
532
+ autoApprove && 'translate-x-3',
533
+ )}
534
+ />
535
+ </span>
536
+ </button>
537
+ )}
538
+ <button
539
+ type="button"
540
+ onClick={() => {
541
+ setOpen(false);
542
+ onManage();
543
+ }}
544
+ className="flex h-7 items-center gap-2 rounded-[5px] px-2 text-[12.5px] text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted hover:text-foreground focus-visible:bg-muted"
545
+ >
546
+ <Settings2 className="size-3.5" />
547
+ {t.agent.manageConnections}
548
+ </button>
549
+ </div>
550
+ </PopoverContent>
551
+ </Popover>
552
+ );
553
+ }
554
+
555
+ function NewDeckState({ providerName }: { providerName: string }) {
556
+ const t = useLocale();
557
+ return (
558
+ <div className="flex min-h-0 flex-1 flex-col justify-end overflow-y-auto px-4 py-5">
559
+ <h2 className="font-heading text-[15px] font-semibold tracking-tight">
560
+ {t.agent.newDeckTitle}
561
+ </h2>
562
+ <p className="mt-1 text-[12.5px] leading-relaxed text-muted-foreground">
563
+ {format(t.agent.newDeckHint, { provider: providerName })}
564
+ </p>
565
+ </div>
566
+ );
567
+ }
568
+
569
+ function EmptyState({
570
+ pendingComments,
571
+ hasSlide,
572
+ onCreate,
573
+ onApply,
574
+ onTheme,
575
+ onEdit,
576
+ }: {
577
+ pendingComments: number;
578
+ hasSlide: boolean;
579
+ onCreate: () => void;
580
+ onApply: () => void;
581
+ onTheme: () => void;
582
+ onEdit: () => void;
583
+ }) {
584
+ const t = useLocale();
585
+ return (
586
+ <div className="flex min-h-0 flex-1 flex-col justify-end overflow-y-auto px-4 py-5">
587
+ <span className="eyebrow">{t.agent.title}</span>
588
+ <h2 className="mt-1.5 font-heading text-[15px] font-semibold tracking-tight">
589
+ {t.agent.emptyTitle}
590
+ </h2>
591
+ <p className="mt-1 text-[12.5px] text-muted-foreground">{t.agent.emptyHint}</p>
592
+ <div className="mt-4 flex flex-col gap-1.5">
593
+ {hasSlide && pendingComments > 0 && (
594
+ <QuickAction
595
+ icon={<MessageSquare />}
596
+ label={`${t.agent.quickApply} · ${format(plural(pendingComments, t.agent.pendingComments), { count: pendingComments })}`}
597
+ hint={t.agent.quickApplyHint}
598
+ tone="brand"
599
+ onClick={onApply}
600
+ />
601
+ )}
602
+ {hasSlide && (
603
+ <QuickAction
604
+ icon={<Pencil />}
605
+ label={t.agent.quickEdit}
606
+ hint={t.agent.quickEditHint}
607
+ onClick={onEdit}
608
+ />
609
+ )}
610
+ <QuickAction
611
+ icon={<Presentation />}
612
+ label={t.agent.quickCreate}
613
+ hint={t.agent.quickCreateHint}
614
+ onClick={onCreate}
615
+ />
616
+ <QuickAction
617
+ icon={<Palette />}
618
+ label={t.agent.quickTheme}
619
+ hint={t.agent.quickThemeHint}
620
+ onClick={onTheme}
621
+ />
622
+ </div>
623
+ </div>
624
+ );
625
+ }
626
+
627
+ function QuickAction({
628
+ icon,
629
+ label,
630
+ hint,
631
+ tone = 'default',
632
+ onClick,
633
+ }: {
634
+ icon: ReactNode;
635
+ label: string;
636
+ hint: string;
637
+ tone?: 'default' | 'brand';
638
+ onClick: () => void;
639
+ }) {
640
+ return (
641
+ <button
642
+ type="button"
643
+ onClick={onClick}
644
+ className={cn(
645
+ 'flex items-start gap-2.5 rounded-[8px] border px-3 py-2.5 text-left outline-none transition-colors duration-100 focus-visible:ring-2 focus-visible:ring-ring/30',
646
+ tone === 'brand'
647
+ ? 'border-brand/40 bg-brand/[0.06] hover:bg-brand/[0.1]'
648
+ : 'border-border bg-card hover:border-foreground/25 hover:bg-muted/40',
649
+ )}
650
+ >
651
+ <span
652
+ className={cn(
653
+ 'mt-0.5 grid size-5 shrink-0 place-items-center [&_svg]:size-3.5',
654
+ tone === 'brand' ? 'text-brand' : 'text-muted-foreground',
655
+ )}
656
+ >
657
+ {icon}
658
+ </span>
659
+ <span className="min-w-0">
660
+ <span className="block text-[12.5px] font-medium">{label}</span>
661
+ <span className="mt-0.5 block text-[11px] leading-snug text-muted-foreground">{hint}</span>
662
+ </span>
663
+ </button>
664
+ );
665
+ }
666
+
667
+ function BetaBadge() {
668
+ const t = useLocale();
669
+ return (
670
+ <Tooltip>
671
+ <TooltipTrigger
672
+ render={
673
+ <button
674
+ type="button"
675
+ className="ml-1 shrink-0 cursor-default rounded-[3px] border border-border px-1 py-px font-medium text-[9px] text-muted-foreground uppercase leading-none tracking-wider outline-none focus-visible:ring-1 focus-visible:ring-brand"
676
+ >
677
+ {t.agent.beta}
678
+ </button>
679
+ }
680
+ />
681
+ <TooltipContent
682
+ side="bottom"
683
+ align="start"
684
+ sideOffset={6}
685
+ className="w-64 px-3 py-2.5 text-left text-pretty"
686
+ >
687
+ <p className="font-medium text-[12px]">{t.agent.betaTitle}</p>
688
+ <p className="mt-1 text-[11.5px] text-background/70 leading-relaxed">{t.agent.betaHint}</p>
689
+ </TooltipContent>
690
+ </Tooltip>
691
+ );
692
+ }