@effectnode/media 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/bin/effectnode-media +2 -0
- package/dist/backend/index.d.ts +8 -0
- package/dist/backend/index.js +13 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
- package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
- package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
- package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
- package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
- package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
- package/dist/backend/movie-backend/agent/tools/index.js +59 -0
- package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
- package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
- package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
- package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
- package/dist/backend/movie-backend/agent/tools/types.js +1 -0
- package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
- package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
- package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
- package/dist/backend/movie-backend/agent/workspace.js +124 -0
- package/dist/backend/movie-backend/core.d.ts +4 -0
- package/dist/backend/movie-backend/core.js +1167 -0
- package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
- package/dist/backend/movie-backend/generation-queue.js +766 -0
- package/dist/backend/movie-backend/process.d.ts +21 -0
- package/dist/backend/movie-backend/process.js +98 -0
- package/dist/backend/movie-backend/render-media.d.ts +48 -0
- package/dist/backend/movie-backend/render-media.js +3311 -0
- package/dist/backend/routes/health.d.ts +1 -0
- package/dist/backend/routes/health.js +9 -0
- package/dist/backend/routes/index.d.ts +1 -0
- package/dist/backend/routes/index.js +11 -0
- package/dist/backend/routes/media.d.ts +1 -0
- package/dist/backend/routes/media.js +13 -0
- package/dist/backend/routes/projects.d.ts +8 -0
- package/dist/backend/routes/projects.js +68 -0
- package/dist/backend/routes/voice.d.ts +1 -0
- package/dist/backend/routes/voice.js +39 -0
- package/dist/backend/routes.d.ts +1 -0
- package/dist/backend/routes.js +20 -0
- package/dist/backend/server.d.ts +1 -0
- package/dist/backend/server.js +23 -0
- package/dist/backend/workspace.d.ts +5 -0
- package/dist/backend/workspace.js +27 -0
- package/dist/backend/ws/index.d.ts +3 -0
- package/dist/backend/ws/index.js +27 -0
- package/dist/backend/ws/messages.d.ts +8 -0
- package/dist/backend/ws/messages.js +21 -0
- package/dist/backend/ws.d.ts +3 -0
- package/dist/backend/ws.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/frontend/index.html +22 -0
- package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/index.css +46 -0
- package/frontend/src/main.tsx +13 -0
- package/frontend/src/movie-app/AppRouter.tsx +14 -0
- package/frontend/src/movie-app/MediaStudio.tsx +9 -0
- package/frontend/src/movie-app/SetupPage.tsx +293 -0
- package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
- package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
- package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
- package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
- package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
- package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
- package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
- package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
- package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
- package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
- package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
- package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
- package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
- package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
- package/frontend/src/movie-app/index.css +46 -0
- package/frontend/src/movie-app/index.html +16 -0
- package/frontend/src/movie-app/lambobo.png +0 -0
- package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
- package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
- package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
- package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
- package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
- package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
- package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
- package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
- package/frontend/src/movie-app/stores/characterStore.ts +109 -0
- package/frontend/src/movie-app/stores/chatStore.ts +459 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
- package/frontend/src/movie-app/stores/logStore.ts +25 -0
- package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
- package/frontend/src/movie-app/stores/projectStore.ts +124 -0
- package/frontend/src/movie-app/stores/queueStore.ts +214 -0
- package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
- package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
- package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
- package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +19 -0
- package/package.json +65 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useGenerationStore } from "../../stores/generationStore";
|
|
3
|
+
import { useChatStore } from "../../stores/chatStore";
|
|
4
|
+
import { MarkdownMessage } from "./MarkdownMessage";
|
|
5
|
+
|
|
6
|
+
const API_BASE = `http://localhost:${(window as any).PORT}`;
|
|
7
|
+
|
|
8
|
+
/** Extract a readable summary of a tool call's JSON arguments. */
|
|
9
|
+
function summarizeArgs(args?: string): string {
|
|
10
|
+
if (!args) return "";
|
|
11
|
+
try {
|
|
12
|
+
const obj = JSON.parse(args);
|
|
13
|
+
const strings = Object.values(obj).filter((v) => typeof v === "string");
|
|
14
|
+
return strings.join(", ");
|
|
15
|
+
} catch {
|
|
16
|
+
return args;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
projectId: string;
|
|
22
|
+
agent?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function uploadToAgentWorkspace(
|
|
26
|
+
projectId: string,
|
|
27
|
+
dataUrl: string,
|
|
28
|
+
filename: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${API_BASE}/api/agent/upload`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify({ image: dataUrl, filename, projectId }),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
console.error("Failed to upload to agent workspace:", await res.text());
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("Failed to upload to agent workspace:", e);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ChatUI({ projectId, agent = "default" }: Props) {
|
|
45
|
+
const gen = useGenerationStore();
|
|
46
|
+
const chat = useChatStore();
|
|
47
|
+
|
|
48
|
+
const port = gen.agent.port;
|
|
49
|
+
const model = gen.agent.model;
|
|
50
|
+
const serverRunning = gen.agent.serverRunning;
|
|
51
|
+
|
|
52
|
+
const activeSession =
|
|
53
|
+
chat.sessions.find((s) => s.id === chat.activeSessionId) ?? null;
|
|
54
|
+
const messages = activeSession?.messages ?? [];
|
|
55
|
+
|
|
56
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
57
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const fileRef = useRef<HTMLInputElement>(null);
|
|
59
|
+
|
|
60
|
+
const [confirmDeleteSession, setConfirmDeleteSession] = useState<
|
|
61
|
+
string | null
|
|
62
|
+
>(null);
|
|
63
|
+
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (scrollRef.current) {
|
|
67
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
68
|
+
}
|
|
69
|
+
}, [messages, chat.sending]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
chat.loadThreads(projectId, agent);
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
}, [projectId, agent]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!previewImage) return;
|
|
78
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
79
|
+
if (e.key === "Escape") setPreviewImage(null);
|
|
80
|
+
};
|
|
81
|
+
window.addEventListener("keydown", onKeyDown);
|
|
82
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
83
|
+
}, [previewImage]);
|
|
84
|
+
|
|
85
|
+
const handleSend = () => {
|
|
86
|
+
const value = inputRef.current?.value ?? "";
|
|
87
|
+
const hasText = value.trim().length > 0;
|
|
88
|
+
const hasImage = Boolean(chat.pendingImage);
|
|
89
|
+
if ((!hasText && !hasImage) || chat.sending || !serverRunning) return;
|
|
90
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
91
|
+
chat.sendMessage(
|
|
92
|
+
value.trim(),
|
|
93
|
+
port,
|
|
94
|
+
model,
|
|
95
|
+
projectId,
|
|
96
|
+
chat.pendingImage ?? undefined,
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleAttach = () => fileRef.current?.click();
|
|
101
|
+
|
|
102
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
103
|
+
const file = e.target.files?.[0];
|
|
104
|
+
if (!file) return;
|
|
105
|
+
const reader = new FileReader();
|
|
106
|
+
reader.onload = () => {
|
|
107
|
+
const dataUrl = reader.result as string;
|
|
108
|
+
chat.setPendingImage(dataUrl);
|
|
109
|
+
uploadToAgentWorkspace(
|
|
110
|
+
projectId,
|
|
111
|
+
dataUrl,
|
|
112
|
+
`chat-${Date.now()}-${file.name}`,
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
reader.readAsDataURL(file);
|
|
116
|
+
e.target.value = "";
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
120
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
handleSend();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const confirmDeleteSessionAction = () => {
|
|
127
|
+
if (confirmDeleteSession) {
|
|
128
|
+
chat.deleteSession(confirmDeleteSession);
|
|
129
|
+
setConfirmDeleteSession(null);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!confirmDeleteSession) return;
|
|
135
|
+
const onKey = (e: KeyboardEvent) => {
|
|
136
|
+
if (e.key === "Escape") {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
setConfirmDeleteSession(null);
|
|
139
|
+
} else if (e.key === "Enter") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
confirmDeleteSessionAction();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener("keydown", onKey);
|
|
145
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, [confirmDeleteSession]);
|
|
148
|
+
|
|
149
|
+
// ========== SVG Icons ==========
|
|
150
|
+
|
|
151
|
+
const SendIcon = (
|
|
152
|
+
<svg
|
|
153
|
+
width="16"
|
|
154
|
+
height="16"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
fill="none"
|
|
157
|
+
stroke="currentColor"
|
|
158
|
+
strokeWidth="2"
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
strokeLinejoin="round"
|
|
161
|
+
>
|
|
162
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
163
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
164
|
+
</svg>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const BotIcon = (
|
|
168
|
+
<svg
|
|
169
|
+
width="16"
|
|
170
|
+
height="16"
|
|
171
|
+
viewBox="0 0 24 24"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
strokeWidth="2"
|
|
175
|
+
strokeLinecap="round"
|
|
176
|
+
strokeLinejoin="round"
|
|
177
|
+
>
|
|
178
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
179
|
+
<circle cx="9" cy="9" r="1" />
|
|
180
|
+
<circle cx="15" cy="9" r="1" />
|
|
181
|
+
<line x1="9" y1="14" x2="15" y2="14" />
|
|
182
|
+
</svg>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const WrenchIcon = (
|
|
186
|
+
<svg
|
|
187
|
+
width="12"
|
|
188
|
+
height="12"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
fill="none"
|
|
191
|
+
stroke="currentColor"
|
|
192
|
+
strokeWidth="2"
|
|
193
|
+
strokeLinecap="round"
|
|
194
|
+
strokeLinejoin="round"
|
|
195
|
+
>
|
|
196
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
|
197
|
+
</svg>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const SpinnerIcon = (
|
|
201
|
+
<svg
|
|
202
|
+
className="animate-spin text-tiffany-600"
|
|
203
|
+
width="16"
|
|
204
|
+
height="16"
|
|
205
|
+
viewBox="0 0 24 24"
|
|
206
|
+
fill="none"
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
strokeWidth="2"
|
|
209
|
+
>
|
|
210
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
211
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
212
|
+
</svg>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const AttachIcon = (
|
|
216
|
+
<svg
|
|
217
|
+
width="16"
|
|
218
|
+
height="16"
|
|
219
|
+
viewBox="0 0 24 24"
|
|
220
|
+
fill="none"
|
|
221
|
+
stroke="currentColor"
|
|
222
|
+
strokeWidth="2"
|
|
223
|
+
strokeLinecap="round"
|
|
224
|
+
strokeLinejoin="round"
|
|
225
|
+
>
|
|
226
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
|
227
|
+
</svg>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const RemoveIcon = (
|
|
231
|
+
<svg
|
|
232
|
+
width="14"
|
|
233
|
+
height="14"
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
fill="none"
|
|
236
|
+
stroke="currentColor"
|
|
237
|
+
strokeWidth="2"
|
|
238
|
+
strokeLinecap="round"
|
|
239
|
+
strokeLinejoin="round"
|
|
240
|
+
>
|
|
241
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
242
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
243
|
+
</svg>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const ResetIcon = (
|
|
247
|
+
<svg
|
|
248
|
+
width="14"
|
|
249
|
+
height="14"
|
|
250
|
+
viewBox="0 0 24 24"
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
strokeWidth="2"
|
|
254
|
+
strokeLinecap="round"
|
|
255
|
+
strokeLinejoin="round"
|
|
256
|
+
>
|
|
257
|
+
<polyline points="1 4 1 10 7 10" />
|
|
258
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
259
|
+
</svg>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const PlusIcon = (
|
|
263
|
+
<svg
|
|
264
|
+
width="14"
|
|
265
|
+
height="14"
|
|
266
|
+
viewBox="0 0 24 24"
|
|
267
|
+
fill="none"
|
|
268
|
+
stroke="currentColor"
|
|
269
|
+
strokeWidth="2"
|
|
270
|
+
strokeLinecap="round"
|
|
271
|
+
strokeLinejoin="round"
|
|
272
|
+
>
|
|
273
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
274
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
275
|
+
</svg>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const TrashIcon = (
|
|
279
|
+
<svg
|
|
280
|
+
width="14"
|
|
281
|
+
height="14"
|
|
282
|
+
viewBox="0 0 24 24"
|
|
283
|
+
fill="none"
|
|
284
|
+
stroke="currentColor"
|
|
285
|
+
strokeWidth="2"
|
|
286
|
+
strokeLinecap="round"
|
|
287
|
+
strokeLinejoin="round"
|
|
288
|
+
>
|
|
289
|
+
<polyline points="3 6 5 6 21 6" />
|
|
290
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
291
|
+
</svg>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const StopIcon = (
|
|
295
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
296
|
+
<rect x="5" y="5" width="14" height="14" rx="2" />
|
|
297
|
+
</svg>
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const CloseIcon = (
|
|
301
|
+
<svg
|
|
302
|
+
width="20"
|
|
303
|
+
height="20"
|
|
304
|
+
viewBox="0 0 24 24"
|
|
305
|
+
fill="none"
|
|
306
|
+
stroke="currentColor"
|
|
307
|
+
strokeWidth="2"
|
|
308
|
+
strokeLinecap="round"
|
|
309
|
+
strokeLinejoin="round"
|
|
310
|
+
>
|
|
311
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
312
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
313
|
+
</svg>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const canSend = serverRunning && !chat.sending;
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className="flex border border-tiffany-200/70 rounded-2xl overflow-hidden bg-white">
|
|
320
|
+
{/* ===== Session sidebar ===== */}
|
|
321
|
+
<div className="w-44 shrink-0 border-r border-tiffany-100 bg-tiffany-50/60 flex flex-col">
|
|
322
|
+
<div className="p-2 border-b border-tiffany-100">
|
|
323
|
+
<button
|
|
324
|
+
onClick={() => chat.createSession()}
|
|
325
|
+
className="flex items-center justify-center gap-1.5 w-full px-2 py-1.5 text-xs font-medium rounded-xl border border-tiffany-500 bg-tiffany-500 text-ink-950 hover:bg-tiffany-600 hover:border-tiffany-600 transition-colors"
|
|
326
|
+
>
|
|
327
|
+
{PlusIcon}
|
|
328
|
+
New Session
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
<div className="flex-1 overflow-y-auto">
|
|
332
|
+
{chat.sessions.map((sess) => {
|
|
333
|
+
const active = sess.id === chat.activeSessionId;
|
|
334
|
+
return (
|
|
335
|
+
<div
|
|
336
|
+
key={sess.id}
|
|
337
|
+
onClick={() => chat.selectSession(sess.id)}
|
|
338
|
+
className={`group flex items-center gap-1 px-2.5 py-2 cursor-pointer text-xs border-b border-tiffany-100 transition-colors ${
|
|
339
|
+
active
|
|
340
|
+
? "bg-tiffany-100 text-tiffany-900"
|
|
341
|
+
: "text-ink-600 hover:bg-tiffany-50"
|
|
342
|
+
}`}
|
|
343
|
+
>
|
|
344
|
+
<span className="flex-1 truncate">{sess.title}</span>
|
|
345
|
+
<button
|
|
346
|
+
onClick={(e) => {
|
|
347
|
+
e.stopPropagation();
|
|
348
|
+
setConfirmDeleteSession(sess.id);
|
|
349
|
+
}}
|
|
350
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 text-red-400 hover:bg-red-50 rounded transition-opacity"
|
|
351
|
+
title="Delete session"
|
|
352
|
+
>
|
|
353
|
+
{TrashIcon}
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
})}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
{/* ===== Chat area ===== */}
|
|
362
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
363
|
+
{/* Header */}
|
|
364
|
+
<div className="flex items-center gap-2 px-4 py-3 bg-tiffany-600 border-b border-tiffany-700">
|
|
365
|
+
<span className="flex items-center justify-center w-6 h-6 rounded-lg bg-white/15 text-white">
|
|
366
|
+
{BotIcon}
|
|
367
|
+
</span>
|
|
368
|
+
<span className="text-sm font-semibold text-white truncate">
|
|
369
|
+
{activeSession?.title ?? "Agent Chat"}
|
|
370
|
+
</span>
|
|
371
|
+
<span className="ml-auto text-xs font-medium text-white/90">
|
|
372
|
+
{serverRunning ? "Connected" : "Server offline"}
|
|
373
|
+
</span>
|
|
374
|
+
<button
|
|
375
|
+
onClick={() => chat.resetActiveSession()}
|
|
376
|
+
disabled={messages.length === 0}
|
|
377
|
+
className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium rounded-xl border border-white/30 text-white hover:bg-white/10 transition-colors disabled:opacity-40"
|
|
378
|
+
title="Reset chat history"
|
|
379
|
+
>
|
|
380
|
+
{ResetIcon}
|
|
381
|
+
Reset
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Messages */}
|
|
386
|
+
<div
|
|
387
|
+
ref={scrollRef}
|
|
388
|
+
className="flex flex-col gap-3 p-5 h-[500px] overflow-y-auto bg-tiffany-50/40"
|
|
389
|
+
>
|
|
390
|
+
{messages.length === 0 && (
|
|
391
|
+
<div className="m-auto text-center text-xs text-tiffany-600/70 italic max-w-xs">
|
|
392
|
+
{serverRunning
|
|
393
|
+
? "Ask the agent anything. It can use tools like get_time or list_projects."
|
|
394
|
+
: "Start the mlx-vlm server above to begin chatting."}
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{messages.map((m) =>
|
|
399
|
+
m.role === "user" ? (
|
|
400
|
+
<div key={m.id} className="flex justify-end">
|
|
401
|
+
<div className="max-w-[80%] px-3 py-2 rounded-2xl bg-tiffany-500 text-ink-950 text-sm whitespace-pre-wrap font-serif">
|
|
402
|
+
{m.image && (
|
|
403
|
+
<img
|
|
404
|
+
src={m.image}
|
|
405
|
+
alt="uploaded"
|
|
406
|
+
onClick={() => setPreviewImage(m.image!)}
|
|
407
|
+
className="mb-2 max-w-48 rounded-xl cursor-pointer hover:opacity-80 transition-opacity"
|
|
408
|
+
/>
|
|
409
|
+
)}
|
|
410
|
+
{m.content}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
) : (
|
|
414
|
+
<div key={m.id} className="flex flex-col gap-1.5">
|
|
415
|
+
{m.steps.length > 0 && (
|
|
416
|
+
<div className="flex flex-col gap-1">
|
|
417
|
+
{m.steps.map((step, i) => {
|
|
418
|
+
const argSummary = summarizeArgs(step.args);
|
|
419
|
+
return (
|
|
420
|
+
<div
|
|
421
|
+
key={i}
|
|
422
|
+
className="flex items-start gap-1.5 text-[11px] text-tiffany-700/80"
|
|
423
|
+
>
|
|
424
|
+
<span className="mt-0.5 shrink-0 text-tiffany-500">
|
|
425
|
+
{WrenchIcon}
|
|
426
|
+
</span>
|
|
427
|
+
<span className="truncate">
|
|
428
|
+
<span className="font-medium">
|
|
429
|
+
Tool: {step.text}
|
|
430
|
+
</span>
|
|
431
|
+
{argSummary && (
|
|
432
|
+
<span className="text-tiffany-600/60">
|
|
433
|
+
{" "}
|
|
434
|
+
· {argSummary}
|
|
435
|
+
</span>
|
|
436
|
+
)}
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
})}
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{m.thinking && (
|
|
445
|
+
<details className="text-[11px] text-tiffany-600/80">
|
|
446
|
+
<summary className="cursor-pointer select-none">
|
|
447
|
+
Thinking
|
|
448
|
+
</summary>
|
|
449
|
+
<p className="mt-1 whitespace-pre-wrap italic">
|
|
450
|
+
{m.thinking}
|
|
451
|
+
</p>
|
|
452
|
+
</details>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{m.notices && m.notices.length > 0 && (
|
|
456
|
+
<div className="flex flex-col gap-1">
|
|
457
|
+
{m.notices.map((n, i) => (
|
|
458
|
+
<div
|
|
459
|
+
key={i}
|
|
460
|
+
className="text-xs text-tiffany-600/70 italic whitespace-pre-wrap"
|
|
461
|
+
>
|
|
462
|
+
{n}
|
|
463
|
+
</div>
|
|
464
|
+
))}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
{m.images && m.images.length > 0 && (
|
|
469
|
+
<div className="flex flex-wrap gap-2">
|
|
470
|
+
{m.images.map((img, i) => (
|
|
471
|
+
<img
|
|
472
|
+
key={i}
|
|
473
|
+
src={img}
|
|
474
|
+
alt={`result ${i + 1}`}
|
|
475
|
+
onClick={() => setPreviewImage(img)}
|
|
476
|
+
className="max-w-48 max-h-48 rounded-xl border border-ink-200 object-contain cursor-pointer hover:opacity-80 transition-opacity"
|
|
477
|
+
/>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
{m.videos && m.videos.length > 0 && (
|
|
483
|
+
<div className="flex flex-wrap gap-2">
|
|
484
|
+
{m.videos.map((v, i) => (
|
|
485
|
+
<video
|
|
486
|
+
key={i}
|
|
487
|
+
src={v}
|
|
488
|
+
controls
|
|
489
|
+
className="max-w-64 max-h-48 rounded-xl border border-ink-200"
|
|
490
|
+
/>
|
|
491
|
+
))}
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
{m.content ? (
|
|
496
|
+
<div className="flex justify-start">
|
|
497
|
+
<div className="max-w-[80%] px-3 py-2 rounded-2xl bg-white border border-tiffany-100 text-ink-900 text-sm">
|
|
498
|
+
<MarkdownMessage content={m.content} />
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
) : (
|
|
502
|
+
chat.sending && (
|
|
503
|
+
<div className="flex justify-start">
|
|
504
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-2xl bg-white border border-tiffany-100 text-tiffany-600 text-sm">
|
|
505
|
+
{SpinnerIcon}
|
|
506
|
+
Thinking...
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
)
|
|
510
|
+
)}
|
|
511
|
+
|
|
512
|
+
{m.trace && (
|
|
513
|
+
<details className="text-[11px] text-tiffany-600/70">
|
|
514
|
+
<summary className="cursor-pointer">
|
|
515
|
+
Trace ({m.trace.length} messages)
|
|
516
|
+
</summary>
|
|
517
|
+
<pre className="mt-1 max-h-48 overflow-auto p-2 bg-ink-50 rounded-xl whitespace-pre-wrap text-ink-600">
|
|
518
|
+
{JSON.stringify(m.trace, null, 2)}
|
|
519
|
+
</pre>
|
|
520
|
+
</details>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
),
|
|
524
|
+
)}
|
|
525
|
+
|
|
526
|
+
{chat.error && (
|
|
527
|
+
<div className="px-3 py-2 rounded-2xl bg-red-50 border border-red-200 text-red-600 text-xs">
|
|
528
|
+
{chat.error}
|
|
529
|
+
</div>
|
|
530
|
+
)}
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
{/* Input */}
|
|
534
|
+
<div className="border-t border-ink-200 bg-white">
|
|
535
|
+
<input
|
|
536
|
+
ref={fileRef}
|
|
537
|
+
type="file"
|
|
538
|
+
accept="image/*"
|
|
539
|
+
onChange={handleFileChange}
|
|
540
|
+
className="hidden"
|
|
541
|
+
/>
|
|
542
|
+
|
|
543
|
+
{chat.pendingImage && (
|
|
544
|
+
<div className="flex items-center gap-2 px-3 pt-2">
|
|
545
|
+
<img
|
|
546
|
+
src={chat.pendingImage}
|
|
547
|
+
alt="pending"
|
|
548
|
+
className="w-14 h-14 rounded-xl object-cover border border-ink-200"
|
|
549
|
+
/>
|
|
550
|
+
<button
|
|
551
|
+
onClick={() => chat.setPendingImage(null)}
|
|
552
|
+
className="flex items-center justify-center w-6 h-6 rounded-full bg-tiffany-100 text-tiffany-700 hover:bg-tiffany-200 transition-colors"
|
|
553
|
+
title="Remove image"
|
|
554
|
+
>
|
|
555
|
+
{RemoveIcon}
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
|
|
560
|
+
<div className="flex items-end gap-2 px-3 py-3">
|
|
561
|
+
<button
|
|
562
|
+
onClick={handleAttach}
|
|
563
|
+
disabled={!serverRunning || chat.sending}
|
|
564
|
+
className="flex items-center justify-center w-10 h-10 shrink-0 rounded-2xl border border-ink-200 text-ink-600 hover:border-tiffany-400 hover:text-tiffany-600 disabled:opacity-50 transition-all"
|
|
565
|
+
title="Attach image"
|
|
566
|
+
>
|
|
567
|
+
{AttachIcon}
|
|
568
|
+
</button>
|
|
569
|
+
<textarea
|
|
570
|
+
ref={inputRef}
|
|
571
|
+
rows={2}
|
|
572
|
+
placeholder="Type a message..."
|
|
573
|
+
onKeyDown={handleKeyDown}
|
|
574
|
+
disabled={!serverRunning || chat.sending}
|
|
575
|
+
className="flex-1 px-3 py-2 bg-tiffany-50/40 border border-tiffany-200/60 rounded-2xl text-ink-900 text-sm placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
|
|
576
|
+
/>
|
|
577
|
+
{chat.sending ? (
|
|
578
|
+
<button
|
|
579
|
+
onClick={() => {
|
|
580
|
+
chat.stop();
|
|
581
|
+
fetch(`${API_BASE}/api/render/cancel`, {
|
|
582
|
+
method: "POST",
|
|
583
|
+
}).catch(() => {});
|
|
584
|
+
}}
|
|
585
|
+
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
|
|
586
|
+
>
|
|
587
|
+
{StopIcon}
|
|
588
|
+
Stop
|
|
589
|
+
</button>
|
|
590
|
+
) : (
|
|
591
|
+
<button
|
|
592
|
+
onClick={handleSend}
|
|
593
|
+
disabled={!canSend}
|
|
594
|
+
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 text-sm font-semibold rounded-2xl transition-all duration-150 shadow-glow"
|
|
595
|
+
>
|
|
596
|
+
{SendIcon}
|
|
597
|
+
Send
|
|
598
|
+
</button>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
{/* Session delete confirmation modal */}
|
|
605
|
+
{confirmDeleteSession && (
|
|
606
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
|
607
|
+
<div className="bg-white rounded-3xl shadow-card p-6 w-80">
|
|
608
|
+
<h3 className="text-sm font-semibold text-ink-900 mb-2">
|
|
609
|
+
Delete Session
|
|
610
|
+
</h3>
|
|
611
|
+
<p className="text-xs text-ink-600 mb-4">
|
|
612
|
+
Are you sure you want to delete this chat session?
|
|
613
|
+
</p>
|
|
614
|
+
<div className="flex justify-end gap-2">
|
|
615
|
+
<button
|
|
616
|
+
onClick={() => setConfirmDeleteSession(null)}
|
|
617
|
+
className="px-3 py-2 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 transition-colors"
|
|
618
|
+
>
|
|
619
|
+
Cancel
|
|
620
|
+
</button>
|
|
621
|
+
<button
|
|
622
|
+
onClick={confirmDeleteSessionAction}
|
|
623
|
+
className="px-3 py-2 text-xs font-medium rounded-xl bg-red-500 hover:bg-red-600 text-white transition-colors"
|
|
624
|
+
>
|
|
625
|
+
Delete
|
|
626
|
+
</button>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
)}
|
|
631
|
+
|
|
632
|
+
{/* Image preview modal */}
|
|
633
|
+
{previewImage && (
|
|
634
|
+
<div
|
|
635
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-8"
|
|
636
|
+
onClick={() => setPreviewImage(null)}
|
|
637
|
+
>
|
|
638
|
+
<button
|
|
639
|
+
onClick={() => setPreviewImage(null)}
|
|
640
|
+
className="absolute top-4 right-4 flex items-center justify-center w-9 h-9 rounded-full bg-black/40 text-white hover:bg-black/60 transition-colors"
|
|
641
|
+
title="Close"
|
|
642
|
+
>
|
|
643
|
+
{CloseIcon}
|
|
644
|
+
</button>
|
|
645
|
+
<img
|
|
646
|
+
src={previewImage}
|
|
647
|
+
alt="preview"
|
|
648
|
+
onClick={(e) => e.stopPropagation()}
|
|
649
|
+
className="max-w-full max-h-full rounded-xl shadow-2xl object-contain"
|
|
650
|
+
/>
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|