@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.
- package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
- package/dist/cli/bin.js +4 -4
- package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
- package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
- package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/locale/index.d.ts +5 -2
- package/dist/locale/index.js +2 -2
- package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
- package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
- package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
- package/package.json +13 -6
- package/skills/apply-comments/SKILL.md +10 -6
- package/skills/create-slide/SKILL.md +2 -0
- package/skills/current-slide/SKILL.md +1 -0
- package/skills/slide-authoring/SKILL.md +31 -1
- package/src/agent/prompt.ts +141 -0
- package/src/agent/provider.ts +123 -0
- package/src/agent/types.ts +151 -0
- package/src/app/app.tsx +2 -0
- package/src/app/assets/open-slide.svg +39 -0
- package/src/app/components/agent/agent-dock.tsx +35 -0
- package/src/app/components/agent/agent-markdown.tsx +45 -0
- package/src/app/components/agent/agent-panel.tsx +692 -0
- package/src/app/components/agent/agent-presence.tsx +74 -0
- package/src/app/components/agent/composer.tsx +120 -0
- package/src/app/components/agent/connect-view.tsx +184 -0
- package/src/app/components/agent/history-view.tsx +135 -0
- package/src/app/components/agent/provider-logo.tsx +34 -0
- package/src/app/components/agent/transcript.tsx +609 -0
- package/src/app/components/board/board-canvas.tsx +143 -12
- package/src/app/components/board/board.css +51 -0
- package/src/app/components/board/layout.test.ts +53 -1
- package/src/app/components/board/layout.ts +50 -2
- package/src/app/components/board/page-node.tsx +287 -13
- package/src/app/components/board/use-board-cards.ts +210 -0
- package/src/app/components/board/use-page-order.ts +17 -9
- package/src/app/components/command/slide-command-menu.tsx +10 -0
- package/src/app/components/icon-switcher-indicator.tsx +1 -1
- package/src/app/components/inspector/comment-layer.tsx +1011 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
- package/src/app/components/inspector/inspect-overlay.tsx +2 -39
- package/src/app/components/inspector/inspector-panel.tsx +10 -93
- package/src/app/components/inspector/inspector-provider.tsx +121 -47
- package/src/app/components/panel/editor-sidebar.tsx +85 -16
- package/src/app/components/panel/panel-shell.tsx +2 -2
- package/src/app/components/panel/resize-handle.tsx +129 -0
- package/src/app/components/player.tsx +0 -1
- package/src/app/components/preview-step-host.tsx +16 -0
- package/src/app/components/sidebar/sidebar.tsx +2 -2
- package/src/app/components/style-panel/style-panel.tsx +3 -2
- package/src/app/components/thumbnail-rail.tsx +76 -1
- package/src/app/favicon.ico +0 -0
- package/src/app/favicon.svg +39 -0
- package/src/app/index.html +2 -1
- package/src/app/lib/agent/use-agent.ts +543 -0
- package/src/app/lib/inspector/freeze-motion.ts +61 -0
- package/src/app/lib/inspector/use-comments.ts +32 -3
- package/src/app/lib/locale-store.ts +6 -1
- package/src/app/lib/print-ready.ts +9 -6
- package/src/app/lib/use-auto-grow.ts +15 -0
- package/src/app/lib/utils.ts +4 -0
- package/src/app/routes/board.tsx +48 -11
- package/src/app/routes/home.tsx +43 -7
- package/src/app/routes/presenter.tsx +1 -16
- package/src/app/routes/preview.tsx +169 -0
- package/src/app/routes/slide.tsx +221 -173
- package/src/app/styles.css +234 -80
- package/src/editing/placeholder-id.ts +1 -0
- package/src/locale/en.ts +127 -5
- package/src/locale/es.ts +692 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/ja.ts +120 -6
- package/src/locale/types.ts +132 -5
- package/src/locale/zh-cn.ts +117 -5
- package/src/locale/zh-tw.ts +117 -5
- package/src/app/assets/open-slide.png +0 -0
- 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
|
+
}
|