@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,74 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { type AgentActivity, setDockOpen, useAgentState } from '@/lib/agent/use-agent';
|
|
3
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { ProviderLogo } from './provider-logo';
|
|
6
|
+
|
|
7
|
+
// `revision` changes whenever the deck module reloads, so each write the
|
|
8
|
+
// agent lands sweeps a band of light across the canvas.
|
|
9
|
+
export function AgentPresence({
|
|
10
|
+
activity,
|
|
11
|
+
revision,
|
|
12
|
+
}: {
|
|
13
|
+
activity: AgentActivity;
|
|
14
|
+
revision: unknown;
|
|
15
|
+
}) {
|
|
16
|
+
const [sweep, setSweep] = useState(0);
|
|
17
|
+
const first = useRef(true);
|
|
18
|
+
|
|
19
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: revision is the trigger
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (first.current) {
|
|
22
|
+
first.current = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setSweep((n) => n + 1);
|
|
26
|
+
}, [revision]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<div aria-hidden className="pointer-events-none absolute inset-0 z-10 rounded-[inherit]">
|
|
31
|
+
<span className="agent-glow" />
|
|
32
|
+
<span className="agent-ring" />
|
|
33
|
+
{sweep > 0 && <span key={sweep} className="agent-sweep" />}
|
|
34
|
+
</div>
|
|
35
|
+
<AgentPresencePill activity={activity} />
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function AgentPresencePill({ activity }: { activity: AgentActivity }) {
|
|
41
|
+
const t = useLocale();
|
|
42
|
+
const { status } = useAgentState();
|
|
43
|
+
const name = status?.providers.find((p) => p.id === activity.provider)?.name ?? '';
|
|
44
|
+
const waiting = activity.phase === 'waiting';
|
|
45
|
+
const step =
|
|
46
|
+
activity.phase === 'tool'
|
|
47
|
+
? [activity.title, activity.detail?.split(/[\\/]/).pop()].filter(Boolean).join(' ')
|
|
48
|
+
: waiting
|
|
49
|
+
? t.agent.presenceWaiting
|
|
50
|
+
: t.agent.presenceThinking;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => setDockOpen(true)}
|
|
56
|
+
className="island-enter absolute top-2 left-1/2 z-20 flex h-7 max-w-[min(420px,calc(100%-1rem))] -translate-x-1/2 items-center gap-2 rounded-full border border-hairline bg-popover/90 px-2.5 text-[11.5px] shadow-floating backdrop-blur-md md:top-3"
|
|
57
|
+
>
|
|
58
|
+
<ProviderLogo provider={activity.provider} className="size-3.5 shrink-0" />
|
|
59
|
+
<span className="shrink-0 font-medium">
|
|
60
|
+
{format(t.agent.presenceEditing, { provider: name })}
|
|
61
|
+
</span>
|
|
62
|
+
<span
|
|
63
|
+
aria-hidden
|
|
64
|
+
className={cn(
|
|
65
|
+
'size-1 shrink-0 rounded-full',
|
|
66
|
+
waiting ? 'bg-brand' : 'animate-pulse bg-foreground/50 motion-reduce:animate-none',
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
<span className={cn('min-w-0 truncate', waiting ? 'text-brand' : 'agent-shimmer')}>
|
|
70
|
+
{step}
|
|
71
|
+
</span>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ArrowUp, Square } from 'lucide-react';
|
|
2
|
+
import type { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { useAutoGrow } from '@/lib/use-auto-grow';
|
|
5
|
+
import { useLocale } from '@/lib/use-locale';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
export function Composer({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
onSend,
|
|
12
|
+
onStop,
|
|
13
|
+
running,
|
|
14
|
+
busy,
|
|
15
|
+
placeholder,
|
|
16
|
+
chips,
|
|
17
|
+
textareaRef,
|
|
18
|
+
}: {
|
|
19
|
+
value: string;
|
|
20
|
+
onChange: (next: string) => void;
|
|
21
|
+
onSend: () => void;
|
|
22
|
+
onStop: () => void;
|
|
23
|
+
running: boolean;
|
|
24
|
+
busy?: boolean;
|
|
25
|
+
placeholder: string;
|
|
26
|
+
chips?: ReactNode;
|
|
27
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
28
|
+
}) {
|
|
29
|
+
const t = useLocale();
|
|
30
|
+
const canSend = !running && !busy && value.trim().length > 0;
|
|
31
|
+
useAutoGrow(textareaRef, value, 160);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="relative z-10 shrink-0 bg-sidebar px-3 pt-1 pb-3">
|
|
35
|
+
<span
|
|
36
|
+
aria-hidden
|
|
37
|
+
className="pointer-events-none absolute inset-x-0 bottom-full h-5 bg-gradient-to-t from-sidebar to-transparent"
|
|
38
|
+
/>
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
'relative flex flex-col rounded-[8px] border border-border bg-background shadow-edge transition-colors duration-100',
|
|
42
|
+
'focus-within:border-foreground/40 focus-within:ring-2 focus-within:ring-ring/30',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<textarea
|
|
46
|
+
ref={textareaRef}
|
|
47
|
+
value={value}
|
|
48
|
+
rows={1}
|
|
49
|
+
onChange={(e) => onChange(e.target.value)}
|
|
50
|
+
onKeyDown={(e) => {
|
|
51
|
+
if (e.nativeEvent.isComposing) return;
|
|
52
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (canSend) onSend();
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
placeholder={placeholder}
|
|
58
|
+
className="w-full resize-none bg-transparent px-3 pt-2.5 text-[12.5px] leading-[1.5] outline-none placeholder:text-muted-foreground/60 disabled:opacity-60"
|
|
59
|
+
/>
|
|
60
|
+
<div className="flex items-end justify-between gap-2 px-2 pb-2">
|
|
61
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1">{chips}</div>
|
|
62
|
+
{running ? (
|
|
63
|
+
<Button
|
|
64
|
+
size="icon-sm"
|
|
65
|
+
variant="outline"
|
|
66
|
+
aria-label={t.agent.stop}
|
|
67
|
+
title={t.agent.stop}
|
|
68
|
+
onClick={onStop}
|
|
69
|
+
>
|
|
70
|
+
<Square className="size-3 fill-current" />
|
|
71
|
+
</Button>
|
|
72
|
+
) : (
|
|
73
|
+
<Button
|
|
74
|
+
size="icon-sm"
|
|
75
|
+
aria-label={t.agent.send}
|
|
76
|
+
title={t.agent.send}
|
|
77
|
+
disabled={!canSend}
|
|
78
|
+
onClick={onSend}
|
|
79
|
+
>
|
|
80
|
+
<ArrowUp className="size-3.5" />
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function ContextChip({
|
|
90
|
+
children,
|
|
91
|
+
tone = 'default',
|
|
92
|
+
onClick,
|
|
93
|
+
title,
|
|
94
|
+
}: {
|
|
95
|
+
children: ReactNode;
|
|
96
|
+
tone?: 'default' | 'brand';
|
|
97
|
+
onClick?: () => void;
|
|
98
|
+
title?: string;
|
|
99
|
+
}) {
|
|
100
|
+
const className = cn(
|
|
101
|
+
'inline-flex h-6 max-w-[180px] items-center gap-1 rounded-[5px] border px-1.5 text-[10.5px] leading-none',
|
|
102
|
+
tone === 'brand'
|
|
103
|
+
? 'border-brand/40 bg-brand/10 text-brand'
|
|
104
|
+
: 'border-hairline bg-muted/50 text-muted-foreground',
|
|
105
|
+
onClick &&
|
|
106
|
+
'cursor-pointer outline-none hover:border-foreground/30 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30',
|
|
107
|
+
);
|
|
108
|
+
if (onClick) {
|
|
109
|
+
return (
|
|
110
|
+
<button type="button" onClick={onClick} title={title} className={className}>
|
|
111
|
+
<span className="truncate">{children}</span>
|
|
112
|
+
</button>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<span title={title} className={className}>
|
|
117
|
+
<span className="truncate">{children}</span>
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Check, Loader2, RotateCw, Terminal } from 'lucide-react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
installProvider,
|
|
7
|
+
refreshStatus,
|
|
8
|
+
useAgentState,
|
|
9
|
+
useAgentStatus,
|
|
10
|
+
} from '@/lib/agent/use-agent';
|
|
11
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
12
|
+
import { cn } from '@/lib/utils';
|
|
13
|
+
import type { AgentProviderId, AgentProviderStatus } from '../../../agent/types';
|
|
14
|
+
import { ProviderLogo } from './provider-logo';
|
|
15
|
+
|
|
16
|
+
const CLI_INSTALL: Record<AgentProviderId, string> = {
|
|
17
|
+
claude: 'npm install -g @anthropic-ai/claude-code',
|
|
18
|
+
codex: 'npm install -g @openai/codex',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function needsCli(p: AgentProviderStatus): boolean {
|
|
22
|
+
return !p.cliFound && !p.ready;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ConnectView() {
|
|
26
|
+
const t = useLocale();
|
|
27
|
+
const { status, loading, enabled } = useAgentStatus();
|
|
28
|
+
const { installing, connectErrors } = useAgentState();
|
|
29
|
+
const [checking, setChecking] = useState(false);
|
|
30
|
+
const providers = status?.providers ?? [];
|
|
31
|
+
const needCli = providers.length > 0 && providers.every(needsCli);
|
|
32
|
+
const anyMissing = providers.some(needsCli);
|
|
33
|
+
|
|
34
|
+
const checkAgain = async () => {
|
|
35
|
+
setChecking(true);
|
|
36
|
+
try {
|
|
37
|
+
await refreshStatus();
|
|
38
|
+
} finally {
|
|
39
|
+
setChecking(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex flex-col gap-4 px-4 py-5">
|
|
45
|
+
<div>
|
|
46
|
+
<span className="eyebrow">{t.agent.title}</span>
|
|
47
|
+
<h2 className="mt-1.5 font-heading text-[15px] font-semibold tracking-tight">
|
|
48
|
+
{needCli ? t.agent.needCliTitle : t.agent.connectTitle}
|
|
49
|
+
</h2>
|
|
50
|
+
<p className="mt-1.5 text-[12.5px] leading-relaxed text-muted-foreground">
|
|
51
|
+
{needCli ? t.agent.needCliDescription : t.agent.connectDescription}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{loading ? (
|
|
56
|
+
<p className="text-[12px] text-muted-foreground">{t.agent.statusLoading}…</p>
|
|
57
|
+
) : !enabled ? (
|
|
58
|
+
<p className="text-[12px] text-muted-foreground">{t.agent.disabled}</p>
|
|
59
|
+
) : (
|
|
60
|
+
<div className="flex flex-col gap-2.5">
|
|
61
|
+
{providers.map((provider) => (
|
|
62
|
+
<ProviderCard
|
|
63
|
+
key={provider.id}
|
|
64
|
+
provider={provider}
|
|
65
|
+
packageManager={status?.packageManager ?? 'npm'}
|
|
66
|
+
installing={installing === provider.id}
|
|
67
|
+
busy={installing !== null}
|
|
68
|
+
error={connectErrors[provider.id]}
|
|
69
|
+
onConnect={async () => {
|
|
70
|
+
try {
|
|
71
|
+
await installProvider(provider.id);
|
|
72
|
+
toast.success(format(t.agent.toastConnected, { name: provider.name }));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
toast.error(format(t.agent.toastConnectFailed, { pkg: provider.sdkPackage }), {
|
|
75
|
+
description: err instanceof Error ? err.message : undefined,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
{anyMissing && (
|
|
82
|
+
<Button
|
|
83
|
+
size="sm"
|
|
84
|
+
variant="outline"
|
|
85
|
+
className="self-start"
|
|
86
|
+
disabled={checking}
|
|
87
|
+
onClick={checkAgain}
|
|
88
|
+
>
|
|
89
|
+
<RotateCw
|
|
90
|
+
className={cn('size-3.5', checking && 'animate-spin motion-reduce:animate-none')}
|
|
91
|
+
aria-hidden
|
|
92
|
+
/>
|
|
93
|
+
{t.agent.checkAgain}
|
|
94
|
+
</Button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ProviderCard({
|
|
103
|
+
provider,
|
|
104
|
+
packageManager,
|
|
105
|
+
installing,
|
|
106
|
+
busy,
|
|
107
|
+
error,
|
|
108
|
+
onConnect,
|
|
109
|
+
}: {
|
|
110
|
+
provider: AgentProviderStatus;
|
|
111
|
+
packageManager: string;
|
|
112
|
+
installing: boolean;
|
|
113
|
+
busy: boolean;
|
|
114
|
+
error: string | undefined;
|
|
115
|
+
onConnect: () => void;
|
|
116
|
+
}) {
|
|
117
|
+
const t = useLocale();
|
|
118
|
+
const missing = needsCli(provider);
|
|
119
|
+
const cliLabel = provider.cliFound
|
|
120
|
+
? provider.cliVersion
|
|
121
|
+
? format(t.agent.cliFound, { version: provider.cliVersion })
|
|
122
|
+
: t.agent.cliFoundNoVersion
|
|
123
|
+
: t.agent.cliMissing;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="rounded-[8px] border border-border bg-card p-3 shadow-edge">
|
|
127
|
+
<div className="flex items-center justify-between gap-2">
|
|
128
|
+
<div className="min-w-0">
|
|
129
|
+
<h3 className="flex items-center gap-1.5 font-heading text-[13px] font-semibold tracking-tight">
|
|
130
|
+
<ProviderLogo provider={provider.id} className="size-3.5 shrink-0" />
|
|
131
|
+
{provider.name}
|
|
132
|
+
</h3>
|
|
133
|
+
<p
|
|
134
|
+
className={cn(
|
|
135
|
+
'mt-0.5 flex items-center gap-1 text-[11px]',
|
|
136
|
+
provider.cliFound ? 'text-muted-foreground' : 'text-amber-600 dark:text-amber-400',
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<Terminal className="size-3" aria-hidden />
|
|
140
|
+
{cliLabel}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
{provider.ready ? (
|
|
144
|
+
<span className="inline-flex items-center gap-1 rounded-[5px] bg-emerald-500/10 px-2 py-1 text-[11px] font-medium text-emerald-700 dark:text-emerald-400">
|
|
145
|
+
<Check className="size-3" aria-hidden />
|
|
146
|
+
{t.agent.connected}
|
|
147
|
+
</span>
|
|
148
|
+
) : installing ? (
|
|
149
|
+
<span className="inline-flex items-center gap-1.5 px-1 text-[11.5px] text-muted-foreground">
|
|
150
|
+
<Loader2 className="size-3 animate-spin motion-reduce:animate-none" aria-hidden />
|
|
151
|
+
{t.agent.connecting}
|
|
152
|
+
</span>
|
|
153
|
+
) : missing ? null : (
|
|
154
|
+
<Button
|
|
155
|
+
size="sm"
|
|
156
|
+
variant={error ? 'outline' : 'default'}
|
|
157
|
+
disabled={busy}
|
|
158
|
+
onClick={onConnect}
|
|
159
|
+
>
|
|
160
|
+
{error ? t.agent.retry : t.agent.connect}
|
|
161
|
+
</Button>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
{missing ? (
|
|
165
|
+
<div className="mt-2.5 flex flex-col gap-1.5">
|
|
166
|
+
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
|
167
|
+
{format(t.agent.cliHint, { name: provider.name })}
|
|
168
|
+
</p>
|
|
169
|
+
<code className="block overflow-x-auto rounded-[5px] bg-muted px-2 py-1.5 font-mono text-[11px] whitespace-nowrap select-all">
|
|
170
|
+
{CLI_INSTALL[provider.id]}
|
|
171
|
+
</code>
|
|
172
|
+
</div>
|
|
173
|
+
) : error && !installing ? (
|
|
174
|
+
<p className="mt-2 line-clamp-3 text-[11px] leading-relaxed text-destructive">{error}</p>
|
|
175
|
+
) : (
|
|
176
|
+
<p className="mt-2 text-[11px] leading-relaxed text-muted-foreground">
|
|
177
|
+
{provider.ready
|
|
178
|
+
? `${provider.sdkPackage} v${provider.sdkVersion}`
|
|
179
|
+
: format(t.agent.connectInstallHint, { pkg: provider.sdkPackage, pm: packageManager })}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Check, MessageSquare, Trash2 } from 'lucide-react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useLocale } from '@/lib/use-locale';
|
|
4
|
+
import { useSlideTitles } from '@/lib/use-slide-titles';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import type { AgentSessionSummary } from '../../../agent/types';
|
|
7
|
+
import { ProviderLogo } from './provider-logo';
|
|
8
|
+
|
|
9
|
+
const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
|
|
10
|
+
['year', 365 * 24 * 3600],
|
|
11
|
+
['month', 30 * 24 * 3600],
|
|
12
|
+
['week', 7 * 24 * 3600],
|
|
13
|
+
['day', 24 * 3600],
|
|
14
|
+
['hour', 3600],
|
|
15
|
+
['minute', 60],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function relativeTime(iso: string, locale: string, now: number): string {
|
|
19
|
+
const seconds = (Date.parse(iso) - now) / 1000;
|
|
20
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });
|
|
21
|
+
for (const [unit, size] of UNITS) {
|
|
22
|
+
if (Math.abs(seconds) >= size) return rtf.format(Math.round(seconds / size), unit);
|
|
23
|
+
}
|
|
24
|
+
return rtf.format(0, 'second');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function HistoryView({
|
|
28
|
+
sessions,
|
|
29
|
+
activeId,
|
|
30
|
+
slideId,
|
|
31
|
+
onSelect,
|
|
32
|
+
onDelete,
|
|
33
|
+
}: {
|
|
34
|
+
sessions: AgentSessionSummary[];
|
|
35
|
+
activeId: string | null;
|
|
36
|
+
slideId: string | null;
|
|
37
|
+
onSelect: (session: AgentSessionSummary) => void;
|
|
38
|
+
onDelete: (id: string) => void;
|
|
39
|
+
}) {
|
|
40
|
+
const t = useLocale();
|
|
41
|
+
const groups = useMemo(() => {
|
|
42
|
+
if (!slideId) return [{ id: 'all', label: null, items: sessions }];
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
id: 'here',
|
|
46
|
+
label: t.agent.historyThisDeck,
|
|
47
|
+
items: sessions.filter((s) => s.slideId === slideId),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'other',
|
|
51
|
+
label: t.agent.historyOtherDecks,
|
|
52
|
+
items: sessions.filter((s) => s.slideId !== slideId),
|
|
53
|
+
},
|
|
54
|
+
].filter((g) => g.items.length > 0);
|
|
55
|
+
}, [sessions, slideId, t]);
|
|
56
|
+
const showDeck = groups.some((g) => g.id !== 'here');
|
|
57
|
+
const titles = useSlideTitles(showDeck && sessions.some((s) => s.slideId));
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
|
|
60
|
+
if (sessions.length === 0) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 px-6 text-center">
|
|
63
|
+
<MessageSquare className="size-5 text-muted-foreground/60" strokeWidth={1.5} aria-hidden />
|
|
64
|
+
<p className="text-[12.5px] font-medium">{t.agent.noSessions}</p>
|
|
65
|
+
<p className="text-[11.5px] leading-relaxed text-muted-foreground">
|
|
66
|
+
{t.agent.historyEmptyHint}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div data-agent-history className="min-h-0 flex-1 overflow-y-auto px-2 py-2">
|
|
74
|
+
{groups.map((group) => (
|
|
75
|
+
<section key={group.id} className="mb-2 last:mb-0">
|
|
76
|
+
{group.label && <h3 className="eyebrow px-2 pt-1.5 pb-1">{group.label}</h3>}
|
|
77
|
+
<ul className="flex flex-col gap-px">
|
|
78
|
+
{group.items.map((s) => {
|
|
79
|
+
const active = s.id === activeId;
|
|
80
|
+
const running = s.status === 'running';
|
|
81
|
+
const deck =
|
|
82
|
+
group.id !== 'here' && s.slideId ? (titles[s.slideId] ?? s.slideId) : null;
|
|
83
|
+
return (
|
|
84
|
+
<li key={s.id} className="group/session relative">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => onSelect(s)}
|
|
88
|
+
aria-current={active ? 'true' : undefined}
|
|
89
|
+
className={cn(
|
|
90
|
+
'flex w-full flex-col gap-0.5 rounded-[6px] py-2 pr-9 pl-2.5 text-left outline-none transition-colors duration-100 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/30',
|
|
91
|
+
active && 'bg-muted',
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<span className="flex min-w-0 items-center gap-1.5 text-[12.5px] font-medium">
|
|
95
|
+
<span className="min-w-0 truncate">{s.title}</span>
|
|
96
|
+
{active && (
|
|
97
|
+
<Check className="size-3 shrink-0 text-muted-foreground" aria-hidden />
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
<span className="flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
101
|
+
<ProviderLogo provider={s.provider} className="size-3 shrink-0" />
|
|
102
|
+
{running ? (
|
|
103
|
+
<span className="agent-shimmer shrink-0">{t.agent.sessionRunning}</span>
|
|
104
|
+
) : (
|
|
105
|
+
<span className="shrink-0 tabular-nums">
|
|
106
|
+
{relativeTime(s.updatedAt, t.id, now)}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
{deck && (
|
|
110
|
+
<>
|
|
111
|
+
<span aria-hidden>·</span>
|
|
112
|
+
<span className="min-w-0 truncate">{deck}</span>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</span>
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
aria-label={t.agent.deleteSession}
|
|
120
|
+
title={t.agent.deleteSession}
|
|
121
|
+
disabled={running}
|
|
122
|
+
onClick={() => onDelete(s.id)}
|
|
123
|
+
className="absolute top-1/2 right-1.5 grid size-6 -translate-y-1/2 place-items-center rounded-[5px] text-muted-foreground opacity-0 outline-none transition-opacity duration-100 hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/30 disabled:hidden group-hover/session:opacity-100"
|
|
124
|
+
>
|
|
125
|
+
<Trash2 className="size-3.5" />
|
|
126
|
+
</button>
|
|
127
|
+
</li>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</ul>
|
|
131
|
+
</section>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
import type { AgentProviderId } from '../../../agent/types';
|
|
3
|
+
|
|
4
|
+
// Marks from svgl.app (claude-ai-icon, openai).
|
|
5
|
+
function ClaudeLogo(props: SVGProps<SVGSVGElement>) {
|
|
6
|
+
return (
|
|
7
|
+
<svg viewBox="0 0 256 257" aria-hidden="true" {...props}>
|
|
8
|
+
<path
|
|
9
|
+
fill="#D97757"
|
|
10
|
+
d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
|
|
11
|
+
/>
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function OpenAILogo(props: SVGProps<SVGSVGElement>) {
|
|
17
|
+
return (
|
|
18
|
+
<svg viewBox="100 100 411 411" fill="none" aria-hidden="true" {...props}>
|
|
19
|
+
<path
|
|
20
|
+
fillRule="evenodd"
|
|
21
|
+
clipRule="evenodd"
|
|
22
|
+
fill="currentColor"
|
|
23
|
+
d="M252.794 108.802C289.191 99.0484 326.265 110.305 351.148 135.135C385.113 126.072 422.85 134.862 449.492 161.505C476.136 188.149 484.925 225.888 475.862 259.85V259.854C500.696 284.735 511.95 321.81 502.198 358.207C492.447 394.602 464.161 421.084 430.215 430.217C421.083 464.162 394.603 492.448 358.206 502.199C321.812 511.951 284.734 500.693 259.852 475.864C225.887 484.927 188.15 476.137 161.507 449.495C134.864 422.851 126.073 385.111 135.136 351.149C110.304 326.266 99.0496 289.192 108.801 252.795C118.552 216.4 146.84 189.918 180.784 180.785C189.917 146.841 216.396 118.553 252.794 108.802ZM374.292 407.145C374.292 411.271 372.092 415.086 368.517 417.148L283.723 466.102C302.487 480.585 327.555 486.459 352.217 479.852C386.997 470.532 410.068 439.312 410.555 405.006V317.717C410.555 315.08 409.125 312.621 406.843 311.303L374.292 292.509V407.145ZM251.868 415.897C248.296 417.959 243.893 417.959 240.317 415.897L155.526 366.942C152.366 390.436 159.811 415.08 177.866 433.136H177.863C203.325 458.594 241.896 462.962 271.85 446.232L347.449 402.586C349.735 401.268 351.148 398.8 351.148 396.163V358.579L251.868 415.897ZM368.602 220.628C366.319 219.309 363.474 219.318 361.191 220.637L328.641 239.431L427.921 296.749C431.496 298.811 433.697 302.627 433.697 306.752V404.661C455.622 395.654 473.244 376.881 479.851 352.218C489.169 317.442 473.668 281.85 444.201 264.274L368.602 220.628ZM177.303 206.34C155.377 215.348 137.756 234.122 131.148 258.783C121.832 293.561 137.331 329.153 166.799 346.727L242.398 390.373C244.68 391.692 247.525 391.684 249.807 390.366L282.357 371.572L183.078 314.253C179.504 312.189 177.303 308.375 177.303 304.251V206.34ZM259.849 279.145V331.858L305.5 358.213L351.15 331.858V279.145L305.5 252.789L259.849 279.145ZM327.276 144.9C308.512 130.418 283.445 124.543 258.782 131.15C224.002 140.471 200.931 171.691 200.445 205.995V293.286C200.445 295.923 201.875 298.381 204.158 299.7L236.707 318.493V203.856C236.707 199.731 238.909 195.916 242.483 193.853L327.276 144.9ZM433.137 177.867C407.675 152.407 369.103 148.038 339.149 164.769L263.55 208.415C261.265 209.734 259.852 212.202 259.852 214.838V252.423L359.132 195.105C362.703 193.041 367.108 193.041 370.682 195.105L455.473 244.06C458.635 220.567 451.189 195.922 433.135 177.867H433.137Z"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ProviderLogo({
|
|
30
|
+
provider,
|
|
31
|
+
...props
|
|
32
|
+
}: { provider: AgentProviderId } & SVGProps<SVGSVGElement>) {
|
|
33
|
+
return provider === 'claude' ? <ClaudeLogo {...props} /> : <OpenAILogo {...props} />;
|
|
34
|
+
}
|