remotion-claude-agent-demo 0.1.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/README.md +160 -0
- package/apps/web/README.md +36 -0
- package/apps/web/env.example +20 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package-lock.json +10348 -0
- package/apps/web/package.json +35 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
- package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
- package/apps/web/src/app/events/stream/route.ts +58 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +174 -0
- package/apps/web/src/app/layout.tsx +34 -0
- package/apps/web/src/app/messages/answer/route.ts +57 -0
- package/apps/web/src/app/messages/stream/route.ts +381 -0
- package/apps/web/src/app/page.tsx +358 -0
- package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
- package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
- package/apps/web/src/app/tasks/route.ts +13 -0
- package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
- package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
- package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
- package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
- package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
- package/apps/web/src/lib/a2a/activity-store.ts +150 -0
- package/apps/web/src/lib/a2a/client.ts +357 -0
- package/apps/web/src/lib/a2a/sse.ts +19 -0
- package/apps/web/src/lib/a2a/task-store.ts +111 -0
- package/apps/web/src/lib/a2a/types.ts +216 -0
- package/apps/web/src/lib/agent/answer-store.ts +109 -0
- package/apps/web/src/lib/agent/background-delivery.ts +343 -0
- package/apps/web/src/lib/agent/background-tool.ts +78 -0
- package/apps/web/src/lib/agent/background.ts +452 -0
- package/apps/web/src/lib/agent/chat.ts +543 -0
- package/apps/web/src/lib/agent/session-store.ts +26 -0
- package/apps/web/src/lib/chat/types.ts +44 -0
- package/apps/web/src/lib/env.ts +31 -0
- package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
- package/apps/web/src/lib/state/chat-atoms.ts +52 -0
- package/apps/web/src/lib/workspace.ts +9 -0
- package/apps/web/tsconfig.json +35 -0
- package/bin/remotion-agent.js +451 -0
- package/package.json +34 -0
- package/templates/.claude/CLAUDE.md +95 -0
- package/templates/.claude/README.md +129 -0
- package/templates/.claude/agents/composer-agent.md +188 -0
- package/templates/.claude/agents/crafter.md +181 -0
- package/templates/.claude/agents/creator.md +134 -0
- package/templates/.claude/agents/perceiver.md +92 -0
- package/templates/.claude/settings.json +36 -0
- package/templates/.claude/settings.local.json +39 -0
- package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
- package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
- package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
- package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
- package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
- package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
- package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
- package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
- package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
- package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
- package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
- package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
- package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
- package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
- package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
- package/templates/.claude/skills/render-config/SKILL.md +290 -0
- package/templates/.claude/skills/script-writer/SKILL.md +59 -0
- package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
- package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
- package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
- package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
- package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
- package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
- package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
- package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
- package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
- package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
- package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
- package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
- package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
- package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
- package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useAtom } from "jotai";
|
|
5
|
+
import { useA2AChat } from "@/lib/hooks/useA2AChat";
|
|
6
|
+
import { AgentBlocks } from "@/components/chat/agent-blocks";
|
|
7
|
+
import { AskUserQuestionPanel } from "@/components/chat/ask-user-question-panel";
|
|
8
|
+
import { SubagentActivitySidebar } from "@/components/chat/subagent-activity-sidebar";
|
|
9
|
+
import { SessionSidebar } from "@/components/chat/session-sidebar";
|
|
10
|
+
import {
|
|
11
|
+
leftSidebarOpenAtom,
|
|
12
|
+
rightSidebarOpenAtom,
|
|
13
|
+
} from "@/lib/state/chat-atoms";
|
|
14
|
+
import {
|
|
15
|
+
Activity,
|
|
16
|
+
AlertCircle,
|
|
17
|
+
ArrowUp,
|
|
18
|
+
Plus,
|
|
19
|
+
Square,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
|
|
22
|
+
export default function Home() {
|
|
23
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
queueMicrotask(() => setIsHydrated(true));
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (!isHydrated) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex h-screen items-center justify-center bg-background text-foreground font-sans">
|
|
32
|
+
<div className="text-sm text-muted-foreground">Loading sessions...</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <HomeContent />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function HomeContent() {
|
|
41
|
+
const {
|
|
42
|
+
sessions,
|
|
43
|
+
currentSessionId,
|
|
44
|
+
items,
|
|
45
|
+
isStreaming,
|
|
46
|
+
error,
|
|
47
|
+
send,
|
|
48
|
+
cancel,
|
|
49
|
+
contextId,
|
|
50
|
+
activeTaskId,
|
|
51
|
+
newSession,
|
|
52
|
+
switchSession,
|
|
53
|
+
deleteSession,
|
|
54
|
+
pendingQuestion,
|
|
55
|
+
submitAnswer,
|
|
56
|
+
subagentActivities,
|
|
57
|
+
inputDraft,
|
|
58
|
+
setInputDraft,
|
|
59
|
+
} = useA2AChat();
|
|
60
|
+
const [leftSidebarOpen, setLeftSidebarOpen] = useAtom(leftSidebarOpenAtom);
|
|
61
|
+
const [rightSidebarOpen, setRightSidebarOpen] = useAtom(rightSidebarOpenAtom);
|
|
62
|
+
const backgroundCountRef = useRef(0);
|
|
63
|
+
const lastSessionIdRef = useRef<string | null>(null);
|
|
64
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
66
|
+
|
|
67
|
+
// Auto-scroll with conversation flow (skip when AskUserQuestion panel is shown)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (pendingQuestion) return;
|
|
70
|
+
if (scrollRef.current) {
|
|
71
|
+
const scroller = scrollRef.current;
|
|
72
|
+
const sessionChanged = lastSessionIdRef.current !== currentSessionId;
|
|
73
|
+
if (sessionChanged) {
|
|
74
|
+
lastSessionIdRef.current = currentSessionId;
|
|
75
|
+
}
|
|
76
|
+
scroller.scrollTo({
|
|
77
|
+
top: scroller.scrollHeight - scroller.clientHeight,
|
|
78
|
+
behavior: sessionChanged ? "auto" : "smooth",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}, [currentSessionId, items, isStreaming, pendingQuestion]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
textareaRef.current?.focus();
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const backgroundCount = subagentActivities.filter(
|
|
89
|
+
(a) => a.isBackground,
|
|
90
|
+
).length;
|
|
91
|
+
if (backgroundCount > backgroundCountRef.current) {
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
setRightSidebarOpen(true);
|
|
94
|
+
}, 100);
|
|
95
|
+
}
|
|
96
|
+
backgroundCountRef.current = backgroundCount;
|
|
97
|
+
}, [setRightSidebarOpen, subagentActivities]);
|
|
98
|
+
|
|
99
|
+
const TEXTAREA_MIN_H = 100;
|
|
100
|
+
const TEXTAREA_MAX_H = 140;
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const ta = textareaRef.current;
|
|
103
|
+
if (!ta) return;
|
|
104
|
+
ta.style.height = "auto";
|
|
105
|
+
const h = Math.min(
|
|
106
|
+
Math.max(ta.scrollHeight, TEXTAREA_MIN_H),
|
|
107
|
+
TEXTAREA_MAX_H,
|
|
108
|
+
);
|
|
109
|
+
ta.style.height = `${h}px`;
|
|
110
|
+
ta.style.overflowY = "hidden";
|
|
111
|
+
}, [inputDraft]);
|
|
112
|
+
|
|
113
|
+
const handleSubmit = async (e?: React.FormEvent) => {
|
|
114
|
+
e?.preventDefault();
|
|
115
|
+
if (isStreaming) return;
|
|
116
|
+
if (!inputDraft.trim()) return;
|
|
117
|
+
await send({ text: inputDraft });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
121
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
if (isStreaming) return;
|
|
124
|
+
handleSubmit();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const lastItem = items[items.length - 1];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex h-screen bg-background text-foreground font-sans">
|
|
132
|
+
<SessionSidebar
|
|
133
|
+
sessions={sessions}
|
|
134
|
+
currentSessionId={currentSessionId}
|
|
135
|
+
isOpen={leftSidebarOpen}
|
|
136
|
+
onToggle={() => setLeftSidebarOpen((open) => !open)}
|
|
137
|
+
onSelect={switchSession}
|
|
138
|
+
onNew={newSession}
|
|
139
|
+
onDelete={deleteSession}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
{/* Subagent activity sidebar */}
|
|
143
|
+
<SubagentActivitySidebar
|
|
144
|
+
activities={subagentActivities}
|
|
145
|
+
isOpen={rightSidebarOpen}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<div
|
|
149
|
+
className={`flex flex-col flex-1 min-w-0 relative transition-all duration-300 ${
|
|
150
|
+
rightSidebarOpen && subagentActivities.length > 0 ? "mr-96" : ""
|
|
151
|
+
}`}
|
|
152
|
+
>
|
|
153
|
+
<header className="flex-none h-16 px-6 py-4 border-b border-border flex items-center justify-between bg-background/80 backdrop-blur-md sticky top-0 z-10 transition-colors duration-300">
|
|
154
|
+
<div className="flex items-center gap-3">
|
|
155
|
+
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center border border-primary/20">
|
|
156
|
+
<svg
|
|
157
|
+
viewBox="0 0 24 24"
|
|
158
|
+
fill="none"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
strokeWidth="2.5"
|
|
161
|
+
strokeLinecap="round"
|
|
162
|
+
strokeLinejoin="round"
|
|
163
|
+
className="w-4 h-4 text-primary"
|
|
164
|
+
>
|
|
165
|
+
<path d="M6 17l5-5-5-5M13 17l5-5-5-5" />
|
|
166
|
+
</svg>
|
|
167
|
+
</div>
|
|
168
|
+
<h1 className="text-sm font-medium tracking-tight text-foreground">
|
|
169
|
+
Luci<span className="text-muted-foreground">.ai</span>
|
|
170
|
+
</h1>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="flex items-center gap-3 text-xs font-mono text-muted-foreground">
|
|
173
|
+
{contextId && (
|
|
174
|
+
<div className="items-center gap-2 max-sm:hidden sm:flex px-2 py-1 rounded bg-muted border border-border">
|
|
175
|
+
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground" />
|
|
176
|
+
<span className="opacity-75">CTX:</span>
|
|
177
|
+
<span>{contextId.slice(0, 8)}</span>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{activeTaskId && (
|
|
181
|
+
<div className="items-center gap-2 max-sm:hidden sm:flex px-2 py-1 rounded bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-100 dark:border-emerald-900/50 text-emerald-600 dark:text-emerald-400">
|
|
182
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
183
|
+
<span className="opacity-75">TASK:</span>
|
|
184
|
+
<span>{activeTaskId.slice(0, 8)}</span>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{/* Subagent activity toggle (in header, does not cover main UI) */}
|
|
188
|
+
{subagentActivities.length > 0 && (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => setRightSidebarOpen((o) => !o)}
|
|
192
|
+
className="max-sm:hidden sm:flex items-center gap-2 px-2.5 py-1.5 rounded-full border border-border bg-background hover:bg-muted text-muted-foreground transition-all enabled:cursor-pointer"
|
|
193
|
+
>
|
|
194
|
+
{subagentActivities.some((a) => a.status === "running") && (
|
|
195
|
+
<span className="relative flex h-2 w-2">
|
|
196
|
+
<span className="absolute inline-flex h-full w-full rounded-full bg-violet-400 opacity-75 animate-ping" />
|
|
197
|
+
<span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500" />
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
<Activity className="w-3.5 h-3.5" />
|
|
201
|
+
<span className="font-sans font-medium">
|
|
202
|
+
{subagentActivities.length} subagents
|
|
203
|
+
</span>
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
<button
|
|
207
|
+
onClick={newSession}
|
|
208
|
+
disabled={isStreaming}
|
|
209
|
+
className="group flex items-center gap-2 px-3 py-1.5 rounded-full border border-border bg-background hover:bg-muted text-muted-foreground transition-all enabled:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
|
210
|
+
>
|
|
211
|
+
<Plus className="w-3.5 h-3.5 group-hover:text-foreground transition-colors" />
|
|
212
|
+
<span className="font-sans font-medium group-hover:text-foreground transition-colors">
|
|
213
|
+
New Session
|
|
214
|
+
</span>
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</header>
|
|
218
|
+
|
|
219
|
+
{error && (
|
|
220
|
+
<div className="flex-none bg-red-50 dark:bg-red-500/10 px-6 py-3 border-b border-red-100 dark:border-red-500/20 flex items-center gap-3 text-red-600 dark:text-red-400 text-sm animate-in slide-in-from-top-2">
|
|
221
|
+
<AlertCircle className="w-4 h-4 shrink-0" />
|
|
222
|
+
<span className="font-medium">{error}</span>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<main
|
|
227
|
+
ref={scrollRef}
|
|
228
|
+
className="flex-1 overflow-y-auto p-4 sm:p-8 text-left"
|
|
229
|
+
>
|
|
230
|
+
{items.length === 0 ? (
|
|
231
|
+
<div className="h-full flex flex-col items-center justify-center text-center select-none pointer-events-none">
|
|
232
|
+
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-6 border border-primary/20">
|
|
233
|
+
<svg
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
fill="none"
|
|
236
|
+
stroke="currentColor"
|
|
237
|
+
strokeWidth="1.5"
|
|
238
|
+
strokeLinecap="round"
|
|
239
|
+
strokeLinejoin="round"
|
|
240
|
+
className="w-8 h-8 text-primary"
|
|
241
|
+
>
|
|
242
|
+
<circle cx="12" cy="12" r="10" />
|
|
243
|
+
<path
|
|
244
|
+
d="M10 8l6 4-6 4V8z"
|
|
245
|
+
fill="currentColor"
|
|
246
|
+
stroke="none"
|
|
247
|
+
/>
|
|
248
|
+
</svg>
|
|
249
|
+
</div>
|
|
250
|
+
<h2 className="text-lg font-medium text-foreground mb-2">
|
|
251
|
+
Ready to set your ideas in motion?
|
|
252
|
+
</h2>
|
|
253
|
+
<p className="text-sm text-muted-foreground max-w-xs">
|
|
254
|
+
{"I'm"} Luci. {"Let's"} turn your concepts into dynamic
|
|
255
|
+
Remotion video code.
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
) : (
|
|
259
|
+
<>
|
|
260
|
+
{/* workany 布局:用户气泡在右,AI 回复在左,全文左对齐 */}
|
|
261
|
+
<div className="max-w-3xl mx-auto space-y-6 text-left">
|
|
262
|
+
{items.map((item) => (
|
|
263
|
+
<div
|
|
264
|
+
key={item.id}
|
|
265
|
+
className={`w-full flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
|
|
266
|
+
>
|
|
267
|
+
{item.role === "user" ? (
|
|
268
|
+
<div className="rounded-lg bg-muted/80 border border-border px-4 py-3 max-w-[85%] sm:max-w-[75%]">
|
|
269
|
+
<div className="text-sm leading-relaxed wrap-break-word text-foreground text-left">
|
|
270
|
+
{item.text}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
) : (
|
|
274
|
+
<div className="min-w-0 w-full max-w-full">
|
|
275
|
+
<AgentBlocks
|
|
276
|
+
blocks={item.blocks}
|
|
277
|
+
isLastAgent={
|
|
278
|
+
isStreaming &&
|
|
279
|
+
lastItem?.role === "agent" &&
|
|
280
|
+
lastItem.id === item.id
|
|
281
|
+
}
|
|
282
|
+
isStreaming={isStreaming}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
))}
|
|
288
|
+
|
|
289
|
+
{isStreaming && lastItem?.role === "user" && (
|
|
290
|
+
<div className="flex items-center gap-1.5 justify-start">
|
|
291
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.3s]" />
|
|
292
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.15s]" />
|
|
293
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" />
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* AskUserQuestion 面板 */}
|
|
298
|
+
{pendingQuestion && (
|
|
299
|
+
<AskUserQuestionPanel
|
|
300
|
+
pendingQuestion={pendingQuestion}
|
|
301
|
+
onSubmit={submitAnswer}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
<div className="h-4" />
|
|
306
|
+
</>
|
|
307
|
+
)}
|
|
308
|
+
</main>
|
|
309
|
+
|
|
310
|
+
<footer className="flex-none p-4 sm:p-6 bg-background border-t border-border">
|
|
311
|
+
<div className="max-w-3xl mx-auto">
|
|
312
|
+
<form
|
|
313
|
+
onSubmit={handleSubmit}
|
|
314
|
+
className="flex items-end gap-2 rounded-2xl border border-border/40 bg-muted/40 transition-[border-color,box-shadow] focus-within:border-border/60 focus-within:ring-1 focus-within:ring-ring/20"
|
|
315
|
+
>
|
|
316
|
+
<textarea
|
|
317
|
+
ref={textareaRef}
|
|
318
|
+
value={inputDraft}
|
|
319
|
+
onChange={(e) => setInputDraft(e.target.value)}
|
|
320
|
+
onKeyDown={handleKeyDown}
|
|
321
|
+
placeholder="Type a message..."
|
|
322
|
+
rows={1}
|
|
323
|
+
className="min-w-0 flex-1 resize-none border-0 bg-transparent px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
|
|
324
|
+
style={{
|
|
325
|
+
minHeight: TEXTAREA_MIN_H,
|
|
326
|
+
maxHeight: TEXTAREA_MAX_H,
|
|
327
|
+
overflowY: "hidden",
|
|
328
|
+
}}
|
|
329
|
+
autoComplete="off"
|
|
330
|
+
/>
|
|
331
|
+
<button
|
|
332
|
+
type={isStreaming ? "button" : "submit"}
|
|
333
|
+
onClick={isStreaming ? cancel : undefined}
|
|
334
|
+
disabled={!inputDraft.trim() && !isStreaming}
|
|
335
|
+
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl transition-colors mr-2 mb-2 enabled:cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed enabled:hover:opacity-90 enabled:active:scale-95 ${
|
|
336
|
+
isStreaming
|
|
337
|
+
? "bg-muted text-muted-foreground"
|
|
338
|
+
: inputDraft.trim()
|
|
339
|
+
? "bg-primary text-primary-foreground"
|
|
340
|
+
: "bg-muted/60 text-muted-foreground"
|
|
341
|
+
}`}
|
|
342
|
+
>
|
|
343
|
+
{isStreaming ? (
|
|
344
|
+
<Square className="h-4 w-4 fill-current" />
|
|
345
|
+
) : (
|
|
346
|
+
<ArrowUp className="h-4 w-4" />
|
|
347
|
+
)}
|
|
348
|
+
</button>
|
|
349
|
+
</form>
|
|
350
|
+
<p className="mt-2 text-center text-[10px] text-muted-foreground">
|
|
351
|
+
AI can make mistakes. Please verify important information.
|
|
352
|
+
</p>
|
|
353
|
+
</div>
|
|
354
|
+
</footer>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import type { A2aError } from "@/lib/a2a/types";
|
|
4
|
+
import { requestCancel } from "@/lib/a2a/task-store";
|
|
5
|
+
import { getEnv } from "@/lib/env";
|
|
6
|
+
|
|
7
|
+
export const runtime = "nodejs";
|
|
8
|
+
|
|
9
|
+
export async function POST(_req: Request, ctx: { params: Promise<{ taskId: string }> }) {
|
|
10
|
+
const env = getEnv();
|
|
11
|
+
const { taskId } = await ctx.params;
|
|
12
|
+
const task = requestCancel(taskId);
|
|
13
|
+
|
|
14
|
+
if (!task) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: { code: "TaskNotFoundError", message: "Task not found" } } satisfies A2aError,
|
|
17
|
+
{ status: 404, headers: { "Content-Type": "application/a2a+json", "A2A-Version": env.a2aVersion } },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(task, {
|
|
22
|
+
headers: { "Content-Type": "application/a2a+json", "A2A-Version": env.a2aVersion },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import type { A2aError } from "@/lib/a2a/types";
|
|
4
|
+
import { getTask } from "@/lib/a2a/task-store";
|
|
5
|
+
import { getEnv } from "@/lib/env";
|
|
6
|
+
|
|
7
|
+
export const runtime = "nodejs";
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request, ctx: { params: Promise<{ taskId: string }> }) {
|
|
10
|
+
const env = getEnv();
|
|
11
|
+
const { taskId } = await ctx.params;
|
|
12
|
+
|
|
13
|
+
const task = getTask(taskId);
|
|
14
|
+
if (!task) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: { code: "TaskNotFoundError", message: "Task not found" } } satisfies A2aError,
|
|
17
|
+
{ status: 404, headers: { "Content-Type": "application/a2a+json", "A2A-Version": env.a2aVersion } },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(task, {
|
|
22
|
+
headers: { "Content-Type": "application/a2a+json", "A2A-Version": env.a2aVersion },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { listTasks } from "@/lib/a2a/task-store";
|
|
4
|
+
import { getEnv } from "@/lib/env";
|
|
5
|
+
|
|
6
|
+
export const runtime = "nodejs";
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
const env = getEnv();
|
|
10
|
+
return NextResponse.json(listTasks(), {
|
|
11
|
+
headers: { "Content-Type": "application/a2a+json", "A2A-Version": env.a2aVersion },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { AgentBlock, AgentToolBlock } from "@/lib/chat/types";
|
|
4
|
+
import { Streamdown } from "streamdown";
|
|
5
|
+
import { code } from "@streamdown/code";
|
|
6
|
+
import { cjk } from "@streamdown/cjk";
|
|
7
|
+
import { mermaid } from "@streamdown/mermaid";
|
|
8
|
+
import { math } from "@streamdown/math";
|
|
9
|
+
import { TaskBlock, ToolBlock, ToolGroupBlock } from "@/components/chat/tool-blocks";
|
|
10
|
+
|
|
11
|
+
export type RenderSegment =
|
|
12
|
+
| { type: "text"; text: string; textIdx: number }
|
|
13
|
+
| { type: "toolGroup"; tools: AgentToolBlock[]; startIdx: number }
|
|
14
|
+
| { type: "task"; task: AgentToolBlock; idx: number }
|
|
15
|
+
| { type: "standaloneTool"; tool: AgentToolBlock; idx: number };
|
|
16
|
+
|
|
17
|
+
const STANDALONE_TOOLS = ["Task", "TodoWrite", "mcp__background_tasks__start"];
|
|
18
|
+
|
|
19
|
+
function groupBlocks(blocks: AgentBlock[]): RenderSegment[] {
|
|
20
|
+
const out: RenderSegment[] = [];
|
|
21
|
+
let textIdx = 0;
|
|
22
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
23
|
+
const b = blocks[i];
|
|
24
|
+
if (b.kind === "text") {
|
|
25
|
+
out.push({ type: "text", text: b.text, textIdx: textIdx++ });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (STANDALONE_TOOLS.includes(b.toolName)) {
|
|
29
|
+
if (b.toolName === "Task" || b.toolName === "TodoWrite") {
|
|
30
|
+
out.push({ type: "task", task: b, idx: i });
|
|
31
|
+
} else {
|
|
32
|
+
out.push({ type: "standaloneTool", tool: b, idx: i });
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const tools: AgentToolBlock[] = [b];
|
|
37
|
+
while (
|
|
38
|
+
i + 1 < blocks.length &&
|
|
39
|
+
blocks[i + 1].kind === "tool" &&
|
|
40
|
+
!STANDALONE_TOOLS.includes((blocks[i + 1] as AgentToolBlock).toolName)
|
|
41
|
+
) {
|
|
42
|
+
tools.push(blocks[++i] as AgentToolBlock);
|
|
43
|
+
}
|
|
44
|
+
out.push({ type: "toolGroup", tools, startIdx: i - tools.length + 1 });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AgentBlocks({
|
|
50
|
+
blocks,
|
|
51
|
+
isLastAgent,
|
|
52
|
+
isStreaming,
|
|
53
|
+
}: {
|
|
54
|
+
blocks: AgentBlock[];
|
|
55
|
+
isLastAgent: boolean;
|
|
56
|
+
isStreaming: boolean;
|
|
57
|
+
}) {
|
|
58
|
+
if (blocks.length === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex items-center gap-1.5">
|
|
61
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.3s]" />
|
|
62
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:-0.15s]" />
|
|
63
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" />
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const segments = groupBlocks(blocks);
|
|
69
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
{segments.map((seg, segIdx) => {
|
|
74
|
+
if (seg.type === "text") {
|
|
75
|
+
const isAnimating =
|
|
76
|
+
isStreaming &&
|
|
77
|
+
isLastAgent &&
|
|
78
|
+
lastBlock?.kind === "text" &&
|
|
79
|
+
segIdx === segments.length - 1;
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
key={`text-${seg.textIdx}`}
|
|
83
|
+
className="text-sm leading-relaxed wrap-break-word text-foreground"
|
|
84
|
+
>
|
|
85
|
+
{seg.text ? (
|
|
86
|
+
<Streamdown plugins={{ code, cjk, mermaid, math }} isAnimating={isAnimating}>
|
|
87
|
+
{seg.text}
|
|
88
|
+
</Streamdown>
|
|
89
|
+
) : (
|
|
90
|
+
<span className="animate-pulse inline-block w-2 h-4 bg-muted rounded" />
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (seg.type === "task") {
|
|
96
|
+
return <TaskBlock key={`task-${seg.idx}`} block={seg.task} />;
|
|
97
|
+
}
|
|
98
|
+
if (seg.type === "standaloneTool") {
|
|
99
|
+
return <ToolBlock key={`tool-${seg.idx}`} block={seg.tool} />;
|
|
100
|
+
}
|
|
101
|
+
return (
|
|
102
|
+
<ToolGroupBlock
|
|
103
|
+
key={`toolGroup-${seg.startIdx}`}
|
|
104
|
+
tools={seg.tools}
|
|
105
|
+
startIdx={seg.startIdx}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|