@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.
Files changed (133) hide show
  1. package/bin/effectnode-media +2 -0
  2. package/dist/backend/index.d.ts +8 -0
  3. package/dist/backend/index.js +13 -0
  4. package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
  5. package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
  6. package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
  7. package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
  8. package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
  9. package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
  10. package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
  11. package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
  12. package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
  13. package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
  14. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
  15. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
  16. package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
  17. package/dist/backend/movie-backend/agent/tools/index.js +59 -0
  18. package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
  19. package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
  20. package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
  21. package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
  22. package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
  23. package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
  24. package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
  25. package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
  26. package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
  27. package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
  28. package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
  29. package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
  30. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
  31. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
  32. package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
  33. package/dist/backend/movie-backend/agent/tools/types.js +1 -0
  34. package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
  35. package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
  36. package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
  37. package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
  38. package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
  39. package/dist/backend/movie-backend/agent/workspace.js +124 -0
  40. package/dist/backend/movie-backend/core.d.ts +4 -0
  41. package/dist/backend/movie-backend/core.js +1167 -0
  42. package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
  43. package/dist/backend/movie-backend/generation-queue.js +766 -0
  44. package/dist/backend/movie-backend/process.d.ts +21 -0
  45. package/dist/backend/movie-backend/process.js +98 -0
  46. package/dist/backend/movie-backend/render-media.d.ts +48 -0
  47. package/dist/backend/movie-backend/render-media.js +3311 -0
  48. package/dist/backend/routes/health.d.ts +1 -0
  49. package/dist/backend/routes/health.js +9 -0
  50. package/dist/backend/routes/index.d.ts +1 -0
  51. package/dist/backend/routes/index.js +11 -0
  52. package/dist/backend/routes/media.d.ts +1 -0
  53. package/dist/backend/routes/media.js +13 -0
  54. package/dist/backend/routes/projects.d.ts +8 -0
  55. package/dist/backend/routes/projects.js +68 -0
  56. package/dist/backend/routes/voice.d.ts +1 -0
  57. package/dist/backend/routes/voice.js +39 -0
  58. package/dist/backend/routes.d.ts +1 -0
  59. package/dist/backend/routes.js +20 -0
  60. package/dist/backend/server.d.ts +1 -0
  61. package/dist/backend/server.js +23 -0
  62. package/dist/backend/workspace.d.ts +5 -0
  63. package/dist/backend/workspace.js +27 -0
  64. package/dist/backend/ws/index.d.ts +3 -0
  65. package/dist/backend/ws/index.js +27 -0
  66. package/dist/backend/ws/messages.d.ts +8 -0
  67. package/dist/backend/ws/messages.js +21 -0
  68. package/dist/backend/ws.d.ts +3 -0
  69. package/dist/backend/ws.js +29 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +80 -0
  72. package/frontend/index.html +22 -0
  73. package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
  74. package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
  75. package/frontend/src/index.css +46 -0
  76. package/frontend/src/main.tsx +13 -0
  77. package/frontend/src/movie-app/AppRouter.tsx +14 -0
  78. package/frontend/src/movie-app/MediaStudio.tsx +9 -0
  79. package/frontend/src/movie-app/SetupPage.tsx +293 -0
  80. package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
  81. package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
  82. package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
  83. package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
  84. package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
  85. package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
  86. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
  87. package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
  88. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
  89. package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
  90. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
  91. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
  92. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
  93. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
  94. package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
  95. package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
  96. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
  97. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
  98. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
  99. package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
  100. package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
  101. package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
  102. package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
  103. package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
  104. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
  105. package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
  106. package/frontend/src/movie-app/index.css +46 -0
  107. package/frontend/src/movie-app/index.html +16 -0
  108. package/frontend/src/movie-app/lambobo.png +0 -0
  109. package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
  110. package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
  111. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
  112. package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
  113. package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
  114. package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
  115. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
  116. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
  117. package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
  118. package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
  119. package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
  120. package/frontend/src/movie-app/stores/characterStore.ts +109 -0
  121. package/frontend/src/movie-app/stores/chatStore.ts +459 -0
  122. package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
  123. package/frontend/src/movie-app/stores/logStore.ts +25 -0
  124. package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
  125. package/frontend/src/movie-app/stores/projectStore.ts +124 -0
  126. package/frontend/src/movie-app/stores/queueStore.ts +214 -0
  127. package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
  128. package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
  129. package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
  130. package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
  131. package/frontend/src/vite-env.d.ts +1 -0
  132. package/frontend/tsconfig.json +19 -0
  133. package/package.json +65 -0
@@ -0,0 +1,109 @@
1
+ import { create } from "zustand";
2
+
3
+ export interface Character {
4
+ id: string;
5
+ projectId: string;
6
+ name: string;
7
+ filename: string;
8
+ source: "upload" | "generated";
9
+ createdAt: string;
10
+ }
11
+
12
+ interface CharacterStore {
13
+ characters: Character[];
14
+ loading: boolean;
15
+ error: string | null;
16
+
17
+ fetchCharacters: (projectId: string) => Promise<void>;
18
+ createCharacter: (
19
+ projectId: string,
20
+ name: string,
21
+ filename: string,
22
+ source?: "upload" | "generated",
23
+ ) => Promise<Character | null>;
24
+ updateCharacter: (
25
+ id: string,
26
+ projectId: string,
27
+ data: Partial<Pick<Character, "name" | "filename" | "source">>,
28
+ ) => Promise<Character | null>;
29
+ deleteCharacter: (id: string, projectId: string) => Promise<boolean>;
30
+ }
31
+
32
+ const API_BASE = `http://localhost:${(window as any).PORT}`;
33
+
34
+ export const useCharacterStore = create<CharacterStore>((set, get) => ({
35
+ characters: [],
36
+ loading: false,
37
+ error: null,
38
+
39
+ fetchCharacters: async (projectId) => {
40
+ set({ loading: true, error: null });
41
+ try {
42
+ const res = await fetch(
43
+ `${API_BASE}/api/characters?projectId=${encodeURIComponent(projectId)}`,
44
+ );
45
+ if (!res.ok) throw new Error(await res.text());
46
+ const characters = await res.json();
47
+ set({ characters, loading: false });
48
+ } catch (e) {
49
+ set({ error: String(e), loading: false });
50
+ }
51
+ },
52
+
53
+ createCharacter: async (projectId, name, filename, source = "upload") => {
54
+ set({ error: null });
55
+ try {
56
+ const res = await fetch(`${API_BASE}/api/characters`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({ projectId, name, filename, source }),
60
+ });
61
+ if (!res.ok) throw new Error(await res.text());
62
+ const character = await res.json();
63
+ set({ characters: [...get().characters, character] });
64
+ return character;
65
+ } catch (e) {
66
+ set({ error: String(e) });
67
+ return null;
68
+ }
69
+ },
70
+
71
+ updateCharacter: async (id, projectId, data) => {
72
+ set({ error: null });
73
+ try {
74
+ const res = await fetch(
75
+ `${API_BASE}/api/characters/${id}?projectId=${encodeURIComponent(projectId)}`,
76
+ {
77
+ method: "PUT",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(data),
80
+ },
81
+ );
82
+ if (!res.ok) throw new Error(await res.text());
83
+ const updated = await res.json();
84
+ set({
85
+ characters: get().characters.map((c) => (c.id === id ? updated : c)),
86
+ });
87
+ return updated;
88
+ } catch (e) {
89
+ set({ error: String(e) });
90
+ return null;
91
+ }
92
+ },
93
+
94
+ deleteCharacter: async (id, projectId) => {
95
+ set({ error: null });
96
+ try {
97
+ const res = await fetch(
98
+ `${API_BASE}/api/characters/${id}?projectId=${encodeURIComponent(projectId)}`,
99
+ { method: "DELETE" },
100
+ );
101
+ if (!res.ok) throw new Error(await res.text());
102
+ set({ characters: get().characters.filter((c) => c.id !== id) });
103
+ return true;
104
+ } catch (e) {
105
+ set({ error: String(e) });
106
+ return false;
107
+ }
108
+ },
109
+ }));
@@ -0,0 +1,459 @@
1
+ import { create } from "zustand";
2
+
3
+ const API_BASE = `http://localhost:${(window as any).PORT}`;
4
+
5
+ export interface AgentStep {
6
+ type: "thought" | "tool";
7
+ text: string;
8
+ detail?: string;
9
+ args?: string;
10
+ }
11
+
12
+ export interface ChatMessage {
13
+ id: string;
14
+ role: "user" | "assistant";
15
+ content: string;
16
+ image?: string;
17
+ images?: string[];
18
+ videos?: string[];
19
+ notices?: string[];
20
+ thinking?: string;
21
+ trace?: any[];
22
+ steps: AgentStep[];
23
+ }
24
+
25
+ export interface ChatSession {
26
+ id: string;
27
+ title: string;
28
+ messages: ChatMessage[];
29
+ createdAt: number;
30
+ }
31
+
32
+ let msgCounter = 0;
33
+ function nextMsgId(): string {
34
+ msgCounter += 1;
35
+ return `msg-${msgCounter}`;
36
+ }
37
+
38
+ let sessionCounter = 0;
39
+ function nextSessionId(): string {
40
+ sessionCounter += 1;
41
+ return `sess-${Date.now().toString(36)}-${sessionCounter}`;
42
+ }
43
+
44
+ function newSession(): ChatSession {
45
+ return {
46
+ id: nextSessionId(),
47
+ title: "New chat",
48
+ messages: [],
49
+ createdAt: Date.now(),
50
+ };
51
+ }
52
+
53
+ // ========== SSE Stream Reader ==========
54
+
55
+ async function readSSEStream(
56
+ response: Response,
57
+ onEvent: (event: string, data: any) => void,
58
+ ): Promise<void> {
59
+ const reader = response.body?.getReader();
60
+ if (!reader) return;
61
+
62
+ const decoder = new TextDecoder();
63
+ let buffer = "";
64
+
65
+ try {
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+
70
+ buffer += decoder.decode(value, { stream: true });
71
+ const lines = buffer.split("\n");
72
+ buffer = lines.pop() || "";
73
+
74
+ let eventType = "message";
75
+ for (const line of lines) {
76
+ if (line.startsWith("event: ")) {
77
+ eventType = line.slice(7).trim();
78
+ } else if (line.startsWith("data: ")) {
79
+ try {
80
+ const data = JSON.parse(line.slice(6));
81
+ onEvent(eventType, data);
82
+ } catch {
83
+ // skip malformed lines
84
+ }
85
+ eventType = "message";
86
+ }
87
+ }
88
+ }
89
+ } finally {
90
+ reader.releaseLock();
91
+ }
92
+ }
93
+
94
+ // ========== Store ==========
95
+
96
+ async function persistThreads(
97
+ projectId: string,
98
+ sessions: ChatSession[],
99
+ agent: string,
100
+ ): Promise<void> {
101
+ try {
102
+ await fetch(`${API_BASE}/api/agent/threads`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({
106
+ projectId,
107
+ agent,
108
+ threads: sessions.map((s) => ({
109
+ id: s.id,
110
+ title: s.title,
111
+ createdAt: s.createdAt,
112
+ })),
113
+ }),
114
+ });
115
+ } catch {
116
+ // ignore persistence failures
117
+ }
118
+ }
119
+
120
+ async function persistThreadChat(
121
+ projectId: string,
122
+ session: ChatSession,
123
+ agent: string,
124
+ ): Promise<void> {
125
+ try {
126
+ await fetch(`${API_BASE}/api/agent/thread/chat`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({
130
+ projectId,
131
+ agent,
132
+ threadId: session.id,
133
+ messages: session.messages,
134
+ }),
135
+ });
136
+ } catch {
137
+ // ignore persistence failures
138
+ }
139
+ }
140
+
141
+ interface ChatStore {
142
+ sessions: ChatSession[];
143
+ activeSessionId: string | null;
144
+ projectId: string | null;
145
+ agentType: string;
146
+ sending: boolean;
147
+ error: string | null;
148
+ pendingImage: string | null;
149
+ setPendingImage: (dataUrl: string | null) => void;
150
+ loadThreads: (projectId: string, agent?: string) => Promise<void>;
151
+ createSession: () => void;
152
+ selectSession: (id: string) => void;
153
+ deleteSession: (id: string) => void;
154
+ resetActiveSession: () => void;
155
+ stop: () => void;
156
+ sendMessage: (
157
+ content: string,
158
+ port: number,
159
+ model: string,
160
+ projectId: string,
161
+ image?: string,
162
+ ) => Promise<void>;
163
+ }
164
+
165
+ let chatAbortController: AbortController | null = null;
166
+
167
+ const firstSession = newSession();
168
+
169
+ export const useChatStore = create<ChatStore>((set, get) => ({
170
+ sessions: [firstSession],
171
+ activeSessionId: firstSession.id,
172
+ projectId: null,
173
+ agentType: "default",
174
+ sending: false,
175
+ error: null,
176
+ pendingImage: null,
177
+
178
+ setPendingImage: (dataUrl) => set({ pendingImage: dataUrl }),
179
+
180
+ loadThreads: async (projectId, agent = "default") => {
181
+ set({ projectId, agentType: agent });
182
+ try {
183
+ const res = await fetch(
184
+ `${API_BASE}/api/agent/threads?projectId=${encodeURIComponent(
185
+ projectId,
186
+ )}&agent=${encodeURIComponent(agent)}`,
187
+ );
188
+ if (!res.ok) return;
189
+ const data = await res.json();
190
+ const metas = (data.threads || []) as {
191
+ id: string;
192
+ title: string;
193
+ createdAt: number;
194
+ }[];
195
+
196
+ const sessions: ChatSession[] = [];
197
+ for (const meta of metas) {
198
+ const chatRes = await fetch(
199
+ `${API_BASE}/api/agent/thread/chat?projectId=${encodeURIComponent(
200
+ projectId,
201
+ )}&agent=${encodeURIComponent(agent)}&threadId=${encodeURIComponent(
202
+ meta.id,
203
+ )}`,
204
+ );
205
+ let messages: ChatMessage[] = [];
206
+ if (chatRes.ok) {
207
+ const chatData = await chatRes.json();
208
+ messages = (chatData.messages || []) as ChatMessage[];
209
+ }
210
+ sessions.push({
211
+ id: meta.id,
212
+ title: meta.title,
213
+ messages,
214
+ createdAt: meta.createdAt,
215
+ });
216
+ }
217
+
218
+ if (sessions.length === 0) {
219
+ sessions.push(newSession());
220
+ }
221
+ set({ sessions, activeSessionId: sessions[0].id, sending: false });
222
+ } catch {
223
+ // keep current in-memory sessions
224
+ }
225
+ },
226
+
227
+ createSession: () => {
228
+ const session = newSession();
229
+ set((s) => ({
230
+ sessions: [...s.sessions, session],
231
+ activeSessionId: session.id,
232
+ error: null,
233
+ }));
234
+ const st = get();
235
+ if (st.projectId) persistThreads(st.projectId, st.sessions, st.agentType);
236
+ },
237
+
238
+ selectSession: (id) => set({ activeSessionId: id, error: null }),
239
+
240
+ deleteSession: (id) => {
241
+ set((s) => {
242
+ let sessions = s.sessions.filter((x) => x.id !== id);
243
+ let activeSessionId = s.activeSessionId;
244
+ if (activeSessionId === id) {
245
+ if (sessions.length > 0) {
246
+ activeSessionId = sessions[sessions.length - 1].id;
247
+ } else {
248
+ const fresh = newSession();
249
+ sessions = [fresh];
250
+ activeSessionId = fresh.id;
251
+ }
252
+ }
253
+ return { sessions, activeSessionId };
254
+ });
255
+ const st = get();
256
+ if (st.projectId) persistThreads(st.projectId, st.sessions, st.agentType);
257
+ },
258
+
259
+ resetActiveSession: () => {
260
+ set((s) => ({
261
+ sessions: s.sessions.map((sess) =>
262
+ sess.id === s.activeSessionId
263
+ ? { ...sess, title: "New chat", messages: [] }
264
+ : sess,
265
+ ),
266
+ error: null,
267
+ }));
268
+ const st = get();
269
+ if (st.projectId) {
270
+ persistThreads(st.projectId, st.sessions, st.agentType);
271
+ const session = st.sessions.find((x) => x.id === st.activeSessionId);
272
+ if (session) persistThreadChat(st.projectId, session, st.agentType);
273
+ }
274
+ },
275
+
276
+ sendMessage: async (content, port, model, projectId, image) => {
277
+ const text = content.trim();
278
+ if ((!text && !image) || get().sending) return;
279
+
280
+ const agent = get().agentType;
281
+
282
+ let sessionId = get().activeSessionId;
283
+ if (!sessionId) {
284
+ const fresh = newSession();
285
+ sessionId = fresh.id;
286
+ set((s) => ({
287
+ sessions: [...s.sessions, fresh],
288
+ activeSessionId: fresh.id,
289
+ }));
290
+ }
291
+
292
+ const session = get().sessions.find((sess) => sess.id === sessionId);
293
+ if (!session) return;
294
+
295
+ // History sent to the backend: prior turns plus the new user message.
296
+ const history = [
297
+ ...session.messages
298
+ .filter((m) => m.content.trim() || (m.image && m.image.trim()))
299
+ .map((m) => ({ role: m.role, content: m.content, image: m.image })),
300
+ { role: "user" as const, content: text, image },
301
+ ];
302
+
303
+ const userMsg: ChatMessage = {
304
+ id: nextMsgId(),
305
+ role: "user",
306
+ content: text,
307
+ image,
308
+ steps: [],
309
+ };
310
+ const assistantMsg: ChatMessage = {
311
+ id: nextMsgId(),
312
+ role: "assistant",
313
+ content: "",
314
+ steps: [],
315
+ };
316
+
317
+ // Set the session title from the first user message.
318
+ const title =
319
+ session.title === "New chat" && text
320
+ ? text.length > 40
321
+ ? `${text.slice(0, 40)}…`
322
+ : text
323
+ : session.title;
324
+
325
+ set((s) => ({
326
+ sessions: s.sessions.map((sess) =>
327
+ sess.id === sessionId
328
+ ? {
329
+ ...sess,
330
+ title,
331
+ messages: [...sess.messages, userMsg, assistantMsg],
332
+ }
333
+ : sess,
334
+ ),
335
+ sending: true,
336
+ error: null,
337
+ pendingImage: null,
338
+ }));
339
+
340
+ const updateAssistant = (fn: (m: ChatMessage) => ChatMessage) =>
341
+ set((s) => ({
342
+ sessions: s.sessions.map((sess) =>
343
+ sess.id === sessionId
344
+ ? {
345
+ ...sess,
346
+ messages: sess.messages.map((m) =>
347
+ m.id === assistantMsg.id ? fn(m) : m,
348
+ ),
349
+ }
350
+ : sess,
351
+ ),
352
+ }));
353
+
354
+ chatAbortController = new AbortController();
355
+
356
+ try {
357
+ const res = await fetch(`${API_BASE}/api/agent/chat`, {
358
+ method: "POST",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({ messages: history, port, model, projectId, agent }),
361
+ signal: chatAbortController.signal,
362
+ });
363
+
364
+ if (!res.ok) {
365
+ const err = await res.text();
366
+ updateAssistant((m) => ({ ...m, content: err || "Request failed" }));
367
+ set({ error: err || "Request failed" });
368
+ return;
369
+ }
370
+
371
+ await readSSEStream(res, (event, data) => {
372
+ switch (event) {
373
+ case "tool":
374
+ updateAssistant((m) => ({
375
+ ...m,
376
+ steps: [
377
+ ...m.steps,
378
+ {
379
+ type: "tool",
380
+ text: data.name as string,
381
+ detail: data.output as string,
382
+ args: data.arguments as string,
383
+ },
384
+ ],
385
+ }));
386
+ break;
387
+ case "delta":
388
+ updateAssistant((m) => ({
389
+ ...m,
390
+ content: m.content + (data.text as string),
391
+ }));
392
+ break;
393
+ case "image": {
394
+ const url = data.url as string;
395
+ const fullUrl = url.startsWith("http") ? url : `${API_BASE}${url}`;
396
+ updateAssistant((m) => ({
397
+ ...m,
398
+ images: [...(m.images ?? []), fullUrl],
399
+ }));
400
+ break;
401
+ }
402
+ case "notice":
403
+ updateAssistant((m) => ({
404
+ ...m,
405
+ notices: [...(m.notices ?? []), data.text as string],
406
+ }));
407
+ break;
408
+ case "thinking":
409
+ updateAssistant((m) => ({
410
+ ...m,
411
+ thinking: (m.thinking ?? "") + (data.text as string),
412
+ }));
413
+ break;
414
+ case "video": {
415
+ const url = data.url as string;
416
+ const fullUrl = url.startsWith("http") ? url : `${API_BASE}${url}`;
417
+ updateAssistant((m) => ({
418
+ ...m,
419
+ videos: [...(m.videos ?? []), fullUrl],
420
+ }));
421
+ break;
422
+ }
423
+ case "messages":
424
+ updateAssistant((m) => ({
425
+ ...m,
426
+ trace: data.messages as any[],
427
+ }));
428
+ break;
429
+ case "error":
430
+ set({ error: data.error || "Something went wrong" });
431
+ break;
432
+ case "done":
433
+ break;
434
+ }
435
+ });
436
+ } catch (e) {
437
+ if ((e as any)?.name !== "AbortError") {
438
+ set({ error: String(e) });
439
+ }
440
+ } finally {
441
+ chatAbortController = null;
442
+ set({ sending: false });
443
+ const st = get();
444
+ if (st.projectId) {
445
+ const session = st.sessions.find((x) => x.id === sessionId);
446
+ if (session) persistThreadChat(st.projectId, session, agent);
447
+ persistThreads(st.projectId, st.sessions, agent);
448
+ }
449
+ }
450
+ },
451
+
452
+ stop: () => {
453
+ if (chatAbortController) {
454
+ chatAbortController.abort();
455
+ chatAbortController = null;
456
+ }
457
+ set({ sending: false });
458
+ },
459
+ }));