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,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { PendingQuestion } from "@/lib/chat/types";
|
|
5
|
+
import { Check, HelpCircle } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
const OTHER_OPTION_KEY = "__other__";
|
|
8
|
+
|
|
9
|
+
export function AskUserQuestionPanel({
|
|
10
|
+
pendingQuestion,
|
|
11
|
+
onSubmit,
|
|
12
|
+
}: {
|
|
13
|
+
pendingQuestion: PendingQuestion;
|
|
14
|
+
onSubmit: (answers: Record<string, string>) => void;
|
|
15
|
+
}) {
|
|
16
|
+
const [selectedAnswers, setSelectedAnswers] = useState<Record<string, string[]>>({});
|
|
17
|
+
const [customTexts, setCustomTexts] = useState<Record<string, string>>({});
|
|
18
|
+
|
|
19
|
+
const handleSelect = (questionText: string, label: string, multiSelect?: boolean) => {
|
|
20
|
+
setSelectedAnswers((prev) => {
|
|
21
|
+
const current = prev[questionText] || [];
|
|
22
|
+
if (multiSelect) {
|
|
23
|
+
if (current.includes(label)) {
|
|
24
|
+
return { ...prev, [questionText]: current.filter((l) => l !== label) };
|
|
25
|
+
}
|
|
26
|
+
return { ...prev, [questionText]: [...current, label] };
|
|
27
|
+
}
|
|
28
|
+
if (label !== OTHER_OPTION_KEY) {
|
|
29
|
+
setCustomTexts((prev) => ({ ...prev, [questionText]: "" }));
|
|
30
|
+
}
|
|
31
|
+
return { ...prev, [questionText]: [label] };
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleCustomTextChange = (questionText: string, text: string) => {
|
|
36
|
+
setCustomTexts((prev) => ({ ...prev, [questionText]: text }));
|
|
37
|
+
setSelectedAnswers((prev) => {
|
|
38
|
+
const current = prev[questionText] || [];
|
|
39
|
+
if (!current.includes(OTHER_OPTION_KEY)) {
|
|
40
|
+
return { ...prev, [questionText]: [OTHER_OPTION_KEY] };
|
|
41
|
+
}
|
|
42
|
+
return prev;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleSubmit = () => {
|
|
47
|
+
const answers: Record<string, string> = {};
|
|
48
|
+
for (const q of pendingQuestion.questions) {
|
|
49
|
+
const selected = selectedAnswers[q.question] || [];
|
|
50
|
+
const finalAnswers = selected
|
|
51
|
+
.map((label) => (label === OTHER_OPTION_KEY ? customTexts[q.question] || "" : label))
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
answers[q.question] = finalAnswers.join(", ");
|
|
54
|
+
}
|
|
55
|
+
onSubmit(answers);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const allAnswered = pendingQuestion.questions.every((q) => {
|
|
59
|
+
const selected = selectedAnswers[q.question] || [];
|
|
60
|
+
if (selected.length === 0) return false;
|
|
61
|
+
if (selected.length === 1 && selected[0] === OTHER_OPTION_KEY) {
|
|
62
|
+
return (customTexts[q.question] || "").trim().length > 0;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="rounded-xl border-2 border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-950/20 p-4 space-y-4 animate-in slide-in-from-bottom-2">
|
|
69
|
+
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
|
70
|
+
<HelpCircle className="w-5 h-5" />
|
|
71
|
+
<span className="font-medium">Agent needs your input</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{pendingQuestion.questions.map((q, qIdx) => {
|
|
75
|
+
const isOtherSelected = (selectedAnswers[q.question] || []).includes(OTHER_OPTION_KEY);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div key={qIdx} className="space-y-2">
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
|
81
|
+
{q.header}
|
|
82
|
+
</span>
|
|
83
|
+
{q.multiSelect && <span className="text-xs text-muted-foreground">(multiple)</span>}
|
|
84
|
+
</div>
|
|
85
|
+
<p className="text-sm text-foreground font-medium">{q.question}</p>
|
|
86
|
+
<div className="grid gap-2">
|
|
87
|
+
{q.options.map((opt, optIdx) => {
|
|
88
|
+
const isSelected = (selectedAnswers[q.question] || []).includes(opt.label);
|
|
89
|
+
return (
|
|
90
|
+
<button
|
|
91
|
+
key={optIdx}
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => handleSelect(q.question, opt.label, q.multiSelect)}
|
|
94
|
+
className={`w-full text-left px-3 py-2 rounded-lg border transition-all ${
|
|
95
|
+
isSelected
|
|
96
|
+
? "border-amber-400 dark:border-amber-600 bg-amber-100 dark:bg-amber-900/40"
|
|
97
|
+
: "border-border hover:border-amber-300 dark:hover:border-amber-700 hover:bg-muted/50"
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<div className="flex items-start gap-2">
|
|
101
|
+
<div
|
|
102
|
+
className={`mt-0.5 w-4 h-4 rounded ${
|
|
103
|
+
q.multiSelect ? "rounded" : "rounded-full"
|
|
104
|
+
} border-2 flex items-center justify-center shrink-0 ${
|
|
105
|
+
isSelected ? "border-amber-500 bg-amber-500" : "border-muted-foreground/40"
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{isSelected && <Check className="w-3 h-3 text-white" />}
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex-1 min-w-0">
|
|
111
|
+
<div className="text-sm font-medium text-foreground">{opt.label}</div>
|
|
112
|
+
<div className="text-xs text-muted-foreground">{opt.description}</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={() => handleSelect(q.question, OTHER_OPTION_KEY, q.multiSelect)}
|
|
122
|
+
className={`w-full text-left px-3 py-2 rounded-lg border transition-all ${
|
|
123
|
+
isOtherSelected
|
|
124
|
+
? "border-amber-400 dark:border-amber-600 bg-amber-100 dark:bg-amber-900/40"
|
|
125
|
+
: "border-border hover:border-amber-300 dark:hover:border-amber-700 hover:bg-muted/50"
|
|
126
|
+
}`}
|
|
127
|
+
>
|
|
128
|
+
<div className="flex items-start gap-2">
|
|
129
|
+
<div
|
|
130
|
+
className={`mt-0.5 w-4 h-4 rounded ${
|
|
131
|
+
q.multiSelect ? "rounded" : "rounded-full"
|
|
132
|
+
} border-2 flex items-center justify-center shrink-0 ${
|
|
133
|
+
isOtherSelected
|
|
134
|
+
? "border-amber-500 bg-amber-500"
|
|
135
|
+
: "border-muted-foreground/40"
|
|
136
|
+
}`}
|
|
137
|
+
>
|
|
138
|
+
{isOtherSelected && <Check className="w-3 h-3 text-white" />}
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex-1 min-w-0">
|
|
141
|
+
<div className="text-sm font-medium text-foreground">Other</div>
|
|
142
|
+
<div className="text-xs text-muted-foreground">Enter custom answer</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</button>
|
|
146
|
+
|
|
147
|
+
{isOtherSelected && (
|
|
148
|
+
<input
|
|
149
|
+
type="text"
|
|
150
|
+
value={customTexts[q.question] || ""}
|
|
151
|
+
onChange={(e) => handleCustomTextChange(q.question, e.target.value)}
|
|
152
|
+
placeholder="Enter your answer..."
|
|
153
|
+
autoFocus
|
|
154
|
+
className="w-full px-3 py-2 rounded-lg border border-amber-300 dark:border-amber-700 bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-amber-400/50"
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={handleSubmit}
|
|
165
|
+
disabled={!allAnswered}
|
|
166
|
+
className="w-full py-2 px-4 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed enabled:cursor-pointer"
|
|
167
|
+
>
|
|
168
|
+
Submit
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ChatSession } from "@/lib/state/chat-atoms";
|
|
4
|
+
import {
|
|
5
|
+
Loader2,
|
|
6
|
+
MessageSquare,
|
|
7
|
+
PanelLeftClose,
|
|
8
|
+
PanelLeftOpen,
|
|
9
|
+
Plus,
|
|
10
|
+
Trash2,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TITLE = "New session";
|
|
15
|
+
|
|
16
|
+
export function SessionSidebar({
|
|
17
|
+
sessions,
|
|
18
|
+
currentSessionId,
|
|
19
|
+
isOpen,
|
|
20
|
+
onToggle,
|
|
21
|
+
onSelect,
|
|
22
|
+
onNew,
|
|
23
|
+
onDelete,
|
|
24
|
+
}: {
|
|
25
|
+
sessions: ChatSession[];
|
|
26
|
+
currentSessionId: string | null;
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
onToggle: () => void;
|
|
29
|
+
onSelect: (sessionId: string) => void;
|
|
30
|
+
onNew: () => void;
|
|
31
|
+
onDelete: (sessionId: string) => void;
|
|
32
|
+
}) {
|
|
33
|
+
const sortedSessions = [...sessions].sort(
|
|
34
|
+
(a, b) => b.updatedAt - a.updatedAt,
|
|
35
|
+
);
|
|
36
|
+
const [mounted, setMounted] = useState(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
queueMicrotask(() => setMounted(true));
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<aside
|
|
44
|
+
className={`
|
|
45
|
+
relative flex flex-col h-screen border-r border-border/50
|
|
46
|
+
bg-background/95 backdrop-blur-xl supports-backdrop-filter:bg-background/60
|
|
47
|
+
transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]
|
|
48
|
+
will-change-[width]
|
|
49
|
+
${isOpen ? "w-[280px]" : "w-[68px]"}
|
|
50
|
+
`}
|
|
51
|
+
>
|
|
52
|
+
{/* Header Section */}
|
|
53
|
+
<div className="flex flex-col shrink-0 pt-2 relative">
|
|
54
|
+
{/* Top Controls */}
|
|
55
|
+
<div className="flex items-center h-12 px-3 mb-2 relative shrink-0">
|
|
56
|
+
{/* Title (Only visible when open) */}
|
|
57
|
+
<div
|
|
58
|
+
className={`
|
|
59
|
+
font-semibold text-sm tracking-tight text-foreground/90 overflow-hidden whitespace-nowrap transition-all duration-300
|
|
60
|
+
${isOpen ? "w-auto opacity-100 translate-x-0" : "w-0 opacity-0 -translate-x-4 pointer-events-none"}
|
|
61
|
+
`}
|
|
62
|
+
>
|
|
63
|
+
Sessions
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Toggle Button - Absolute positioning for smooth movement from Right to Center */}
|
|
67
|
+
<button
|
|
68
|
+
onClick={onToggle}
|
|
69
|
+
className={`
|
|
70
|
+
absolute flex h-8 w-8 items-center justify-center rounded-lg
|
|
71
|
+
hover:bg-muted/80 text-muted-foreground hover:text-foreground
|
|
72
|
+
transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]
|
|
73
|
+
${
|
|
74
|
+
isOpen
|
|
75
|
+
? "right-3 bg-transparent"
|
|
76
|
+
: "left-1/2 -translate-x-1/2 bg-transparent"
|
|
77
|
+
}
|
|
78
|
+
`}
|
|
79
|
+
aria-label={isOpen ? "Collapse sidebar" : "Expand sidebar"}
|
|
80
|
+
>
|
|
81
|
+
{isOpen ? (
|
|
82
|
+
<PanelLeftClose className="h-4.5 w-4.5" />
|
|
83
|
+
) : (
|
|
84
|
+
<PanelLeftOpen className="h-4.5 w-4.5" />
|
|
85
|
+
)}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* New Chat Button */}
|
|
90
|
+
<div className="px-3 pb-3">
|
|
91
|
+
<button
|
|
92
|
+
onClick={onNew}
|
|
93
|
+
className={`
|
|
94
|
+
group flex items-center
|
|
95
|
+
rounded-xl border transition-all duration-300 active:scale-[0.98] overflow-hidden
|
|
96
|
+
h-10 w-full
|
|
97
|
+
${
|
|
98
|
+
isOpen
|
|
99
|
+
? "border-border/50 bg-background shadow-sm hover:shadow-md hover:border-border hover:bg-muted/30"
|
|
100
|
+
: "border-transparent bg-transparent hover:bg-muted/50"
|
|
101
|
+
}
|
|
102
|
+
`}
|
|
103
|
+
title="New Chat"
|
|
104
|
+
>
|
|
105
|
+
{/* Stable Icon Wrapper */}
|
|
106
|
+
<div className="flex w-10 h-full shrink-0 items-center justify-center">
|
|
107
|
+
<Plus
|
|
108
|
+
className={`h-5 w-5 text-primary transition-transform duration-300 group-hover:rotate-90`}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Text */}
|
|
113
|
+
<span
|
|
114
|
+
className={`
|
|
115
|
+
text-sm font-medium text-foreground/80 whitespace-nowrap overflow-hidden transition-all duration-300
|
|
116
|
+
${isOpen ? "w-auto opacity-100 pr-4" : "w-0 opacity-0 pr-0"}
|
|
117
|
+
`}
|
|
118
|
+
>
|
|
119
|
+
New Chat
|
|
120
|
+
</span>
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Divider */}
|
|
126
|
+
<div
|
|
127
|
+
className={`mx-4 h-px bg-border/40 mb-2 transition-opacity duration-300 ${isOpen ? "opacity-100" : "opacity-0"}`}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Session List */}
|
|
131
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4 space-y-2 scrollbar-none">
|
|
132
|
+
{mounted &&
|
|
133
|
+
sortedSessions.map((session) => {
|
|
134
|
+
const isActive = session.id === currentSessionId;
|
|
135
|
+
const isRunning =
|
|
136
|
+
session.isStreaming ||
|
|
137
|
+
session.subagentActivities?.some((a) => a.status === "running");
|
|
138
|
+
const title = session.title || DEFAULT_TITLE;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
key={session.id}
|
|
143
|
+
onClick={() => onSelect(session.id)}
|
|
144
|
+
className={`
|
|
145
|
+
group relative flex items-center rounded-lg cursor-pointer
|
|
146
|
+
transition-all duration-200 ease-out border w-full
|
|
147
|
+
h-10
|
|
148
|
+
${
|
|
149
|
+
isActive
|
|
150
|
+
? "bg-muted border-border/60 shadow-sm"
|
|
151
|
+
: "border-transparent hover:bg-muted/50 hover:border-border/30"
|
|
152
|
+
}
|
|
153
|
+
`}
|
|
154
|
+
title={!isOpen ? title : undefined}
|
|
155
|
+
>
|
|
156
|
+
{/* Active Indicator */}
|
|
157
|
+
{isActive && (
|
|
158
|
+
<div
|
|
159
|
+
className={`
|
|
160
|
+
absolute left-0 top-1/2 -translate-y-1/2 w-1 h-5 bg-primary rounded-r-full transition-all duration-300
|
|
161
|
+
${isOpen ? "-ml-3 opacity-0" : "-ml-3 opacity-100"}
|
|
162
|
+
`}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
{/* Indicator at -ml-3 (12px) aligns with px-3 padding edge perfectly */}
|
|
166
|
+
|
|
167
|
+
{/* Stable Icon Wrapper */}
|
|
168
|
+
<div
|
|
169
|
+
className={`
|
|
170
|
+
flex w-10 h-full shrink-0 items-center justify-center transition-colors duration-300
|
|
171
|
+
`}
|
|
172
|
+
>
|
|
173
|
+
{isRunning ? (
|
|
174
|
+
<Loader2 className="h-4.5 w-4.5 text-blue-500 animate-spin" />
|
|
175
|
+
) : (
|
|
176
|
+
<MessageSquare
|
|
177
|
+
className={`
|
|
178
|
+
h-4.5 w-4.5 transition-colors
|
|
179
|
+
${isActive ? "text-foreground" : "text-muted-foreground group-hover:text-foreground/80"}
|
|
180
|
+
`}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Text Content */}
|
|
186
|
+
<div
|
|
187
|
+
className={`
|
|
188
|
+
flex flex-col flex-1 min-w-0 transition-opacity duration-200
|
|
189
|
+
${isOpen ? "opacity-100 pl-1" : "opacity-0 w-0 px-0 absolute pointer-events-none"}
|
|
190
|
+
`}
|
|
191
|
+
>
|
|
192
|
+
<span
|
|
193
|
+
className={`text-sm font-medium truncate leading-tight ${isActive ? "text-foreground" : "text-muted-foreground group-hover:text-foreground"}`}
|
|
194
|
+
>
|
|
195
|
+
{title}
|
|
196
|
+
</span>
|
|
197
|
+
<span className="text-[10px] text-muted-foreground/70 truncate pt-0.5">
|
|
198
|
+
{new Date(session.updatedAt).toLocaleDateString()}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Actions (Expanded Only) */}
|
|
203
|
+
{isOpen && (
|
|
204
|
+
<div className="flex items-center pr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
205
|
+
<button
|
|
206
|
+
onClick={(e) => {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
onDelete(session.id);
|
|
209
|
+
}}
|
|
210
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-500/10 transition-colors"
|
|
211
|
+
>
|
|
212
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</div>
|
|
220
|
+
</aside>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import type { SubagentActivity } from "@/lib/chat/types";
|
|
5
|
+
import { Activity, CheckCircle2, ChevronDown, ChevronRight, Circle, Clock, Loader2 } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
function getAgentTypeLabel(type: string) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "perceiver":
|
|
10
|
+
return "Perceiver";
|
|
11
|
+
case "creator":
|
|
12
|
+
return "Creator";
|
|
13
|
+
case "crafter":
|
|
14
|
+
return "Crafter";
|
|
15
|
+
case "composer-agent":
|
|
16
|
+
return "Composer";
|
|
17
|
+
case "generalPurpose":
|
|
18
|
+
return "General";
|
|
19
|
+
case "explore":
|
|
20
|
+
return "Explorer";
|
|
21
|
+
case "background":
|
|
22
|
+
return "Background";
|
|
23
|
+
default:
|
|
24
|
+
return type;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatRunningTime(startedAt: number, now: number) {
|
|
29
|
+
const seconds = Math.floor((now - startedAt) / 1000);
|
|
30
|
+
if (seconds < 60) return `${seconds}s`;
|
|
31
|
+
const minutes = Math.floor(seconds / 60);
|
|
32
|
+
const remainingSeconds = seconds % 60;
|
|
33
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SubagentActivityItem({
|
|
37
|
+
agent,
|
|
38
|
+
now,
|
|
39
|
+
}: {
|
|
40
|
+
agent: SubagentActivity;
|
|
41
|
+
now: number;
|
|
42
|
+
}) {
|
|
43
|
+
const [expanded, setExpanded] = useState(false);
|
|
44
|
+
const isRunning = agent.status === "running";
|
|
45
|
+
const hasTodos = (agent.todos ?? []).length > 0;
|
|
46
|
+
const hasDetails = agent.prompt || agent.result || agent.description || hasTodos;
|
|
47
|
+
const todoStats = hasTodos
|
|
48
|
+
? {
|
|
49
|
+
total: agent.todos?.length ?? 0,
|
|
50
|
+
completed: agent.todos?.filter((t) => t.status === "completed").length ?? 0,
|
|
51
|
+
inProgress: agent.todos?.filter((t) => t.status === "in_progress").length ?? 0,
|
|
52
|
+
pending: agent.todos?.filter((t) => t.status === "pending").length ?? 0,
|
|
53
|
+
}
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={`rounded-lg border overflow-hidden ${
|
|
59
|
+
isRunning
|
|
60
|
+
? "border-violet-200 dark:border-violet-800 bg-violet-50/50 dark:bg-violet-950/20"
|
|
61
|
+
: "border-border bg-muted/30"
|
|
62
|
+
}`}
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => hasDetails && setExpanded((e) => !e)}
|
|
67
|
+
disabled={!hasDetails}
|
|
68
|
+
className={`w-full p-3 text-left ${hasDetails ? "enabled:cursor-pointer hover:bg-muted/50" : ""}`}
|
|
69
|
+
>
|
|
70
|
+
<div className="flex items-center gap-2">
|
|
71
|
+
{isRunning ? (
|
|
72
|
+
<Loader2 className="w-4 h-4 text-violet-500 animate-spin shrink-0" />
|
|
73
|
+
) : (
|
|
74
|
+
<CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
|
|
75
|
+
)}
|
|
76
|
+
<span
|
|
77
|
+
className={`text-sm font-medium ${isRunning ? "text-foreground" : "text-muted-foreground"}`}
|
|
78
|
+
>
|
|
79
|
+
{agent.description || getAgentTypeLabel(agent.agentType)}
|
|
80
|
+
</span>
|
|
81
|
+
<div className="flex-1" />
|
|
82
|
+
<span
|
|
83
|
+
className={`text-xs px-1.5 py-0.5 rounded ${
|
|
84
|
+
isRunning
|
|
85
|
+
? "bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400"
|
|
86
|
+
: "bg-muted text-muted-foreground"
|
|
87
|
+
}`}
|
|
88
|
+
>
|
|
89
|
+
{getAgentTypeLabel(agent.agentType)}
|
|
90
|
+
</span>
|
|
91
|
+
{agent.isBackground && (
|
|
92
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400">
|
|
93
|
+
BG
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
{hasDetails &&
|
|
97
|
+
(expanded ? (
|
|
98
|
+
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
99
|
+
) : (
|
|
100
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
|
|
104
|
+
<Clock className="w-3 h-3" />
|
|
105
|
+
<span>
|
|
106
|
+
{isRunning
|
|
107
|
+
? formatRunningTime(agent.startedAt, now)
|
|
108
|
+
: agent.stoppedAt
|
|
109
|
+
? formatRunningTime(agent.startedAt, agent.stoppedAt)
|
|
110
|
+
: "—"}
|
|
111
|
+
</span>
|
|
112
|
+
{todoStats && (
|
|
113
|
+
<>
|
|
114
|
+
<span className="opacity-50">·</span>
|
|
115
|
+
<span>{todoStats.completed}/{todoStats.total}</span>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
<span className="opacity-50">·</span>
|
|
119
|
+
<span className="font-mono opacity-60">{agent.agentId.slice(0, 8)}</span>
|
|
120
|
+
</div>
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
{expanded && hasDetails && (
|
|
124
|
+
<div className="px-3 pb-3 pt-0 border-t border-border/50 space-y-2">
|
|
125
|
+
{hasTodos && (
|
|
126
|
+
<div className="text-xs">
|
|
127
|
+
<div className="font-medium text-muted-foreground mb-1">Todo</div>
|
|
128
|
+
<div className="space-y-1">
|
|
129
|
+
{agent.todos?.map((todo, idx) => (
|
|
130
|
+
<div key={idx} className="flex items-start gap-2">
|
|
131
|
+
{todo.status === "completed" ? (
|
|
132
|
+
<CheckCircle2 className="w-3 h-3 text-emerald-500" />
|
|
133
|
+
) : todo.status === "in_progress" ? (
|
|
134
|
+
<Clock className="w-3 h-3 text-violet-500" />
|
|
135
|
+
) : (
|
|
136
|
+
<Circle className="w-3 h-3 text-muted-foreground" />
|
|
137
|
+
)}
|
|
138
|
+
<span
|
|
139
|
+
className={`text-muted-foreground ${
|
|
140
|
+
todo.status === "completed" ? "line-through opacity-70" : ""
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
{todo.content}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
{agent.prompt && (
|
|
151
|
+
<div className="text-xs">
|
|
152
|
+
<div className="font-medium text-muted-foreground mb-1">Prompt</div>
|
|
153
|
+
<div className="text-foreground bg-background/50 rounded p-2 max-h-24 overflow-y-auto whitespace-pre-wrap">
|
|
154
|
+
{agent.prompt.length > 300 ? agent.prompt.slice(0, 300) + "..." : agent.prompt}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{agent.result && (
|
|
159
|
+
<div className="text-xs">
|
|
160
|
+
<div className="font-medium text-muted-foreground mb-1">Result</div>
|
|
161
|
+
<div className="text-foreground bg-background/50 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap">
|
|
162
|
+
{agent.result.length > 500 ? agent.result.slice(0, 500) + "..." : agent.result}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function SubagentActivitySidebar({
|
|
173
|
+
activities,
|
|
174
|
+
isOpen,
|
|
175
|
+
}: {
|
|
176
|
+
activities: SubagentActivity[];
|
|
177
|
+
isOpen: boolean;
|
|
178
|
+
}) {
|
|
179
|
+
const [now, setNow] = useState(() => Date.now());
|
|
180
|
+
const runningAgents = activities.filter((a) => a.status === "running");
|
|
181
|
+
const stoppedAgents = activities.filter((a) => a.status === "stopped");
|
|
182
|
+
const hasRunning = runningAgents.length > 0;
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!hasRunning) return;
|
|
186
|
+
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
187
|
+
return () => clearInterval(timer);
|
|
188
|
+
}, [hasRunning]);
|
|
189
|
+
|
|
190
|
+
if (activities.length === 0) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className={`fixed right-0 top-0 h-full w-96 bg-background border-l border-border shadow-lg z-10 transform transition-transform duration-300 ${
|
|
197
|
+
isOpen ? "translate-x-0" : "translate-x-full"
|
|
198
|
+
}`}
|
|
199
|
+
>
|
|
200
|
+
<div className="flex flex-col h-full">
|
|
201
|
+
<div className="flex items-center gap-2 h-16 px-6 py-4 border-b border-border bg-background/80 backdrop-blur-md">
|
|
202
|
+
<Activity className="w-5 h-5 text-violet-500" />
|
|
203
|
+
<h2 className="text-sm font-semibold text-foreground">Subagent Activity</h2>
|
|
204
|
+
<div className="flex-1" />
|
|
205
|
+
{hasRunning && (
|
|
206
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400">
|
|
207
|
+
{runningAgents.length} running
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
|
213
|
+
{runningAgents.length > 0 && (
|
|
214
|
+
<div className="space-y-2">
|
|
215
|
+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
216
|
+
Running
|
|
217
|
+
</div>
|
|
218
|
+
{runningAgents.map((agent) => (
|
|
219
|
+
<SubagentActivityItem key={agent.agentId} agent={agent} now={now} />
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{stoppedAgents.length > 0 && (
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
227
|
+
Completed
|
|
228
|
+
</div>
|
|
229
|
+
{stoppedAgents
|
|
230
|
+
.slice(-10)
|
|
231
|
+
.reverse()
|
|
232
|
+
.map((agent) => (
|
|
233
|
+
<SubagentActivityItem key={agent.agentId} agent={agent} now={now} />
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{activities.length === 0 && (
|
|
239
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
240
|
+
<Activity className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
|
241
|
+
<p className="text-sm">No subagent activity</p>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|