@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,267 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useSceneVisualStore } from "../../stores/sceneVisualStore";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
projectId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function SceneVisualTab({ projectId }: Props) {
|
|
9
|
+
const sceneStore = useSceneVisualStore();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
sceneStore.ensureProject(projectId);
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
}, [projectId]);
|
|
15
|
+
|
|
16
|
+
const anyGenerating = sceneStore.items.some((i) => i.generating);
|
|
17
|
+
|
|
18
|
+
const uploadFileInputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
const uploadTargetRef = useRef<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const handleUploadFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
const file = e.target.files?.[0];
|
|
23
|
+
const targetId = uploadTargetRef.current;
|
|
24
|
+
e.target.value = "";
|
|
25
|
+
if (!file || !targetId) return;
|
|
26
|
+
const reader = new FileReader();
|
|
27
|
+
reader.onload = () => {
|
|
28
|
+
sceneStore.uploadItemImage(
|
|
29
|
+
projectId,
|
|
30
|
+
targetId,
|
|
31
|
+
reader.result as string,
|
|
32
|
+
file.name,
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
reader.readAsDataURL(file);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ========== SVG Icons ==========
|
|
39
|
+
|
|
40
|
+
const SceneIcon = (
|
|
41
|
+
<svg
|
|
42
|
+
width="18"
|
|
43
|
+
height="18"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="2"
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
>
|
|
51
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
52
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
53
|
+
<polyline points="21 15 16 10 5 21" />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const PlusIcon = (
|
|
58
|
+
<svg
|
|
59
|
+
width="16"
|
|
60
|
+
height="16"
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="2"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
>
|
|
68
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
69
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const TrashIcon = (
|
|
74
|
+
<svg
|
|
75
|
+
width="14"
|
|
76
|
+
height="14"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth="2"
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
>
|
|
84
|
+
<polyline points="3 6 5 6 21 6" />
|
|
85
|
+
<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" />
|
|
86
|
+
</svg>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const SparkleIcon = (
|
|
90
|
+
<svg
|
|
91
|
+
width="14"
|
|
92
|
+
height="14"
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
strokeWidth="2"
|
|
97
|
+
>
|
|
98
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
99
|
+
</svg>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const SpinnerIcon = (
|
|
103
|
+
<svg
|
|
104
|
+
className="animate-spin text-tiffany-600"
|
|
105
|
+
width="14"
|
|
106
|
+
height="14"
|
|
107
|
+
viewBox="0 0 24 24"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth="2"
|
|
111
|
+
>
|
|
112
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
113
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
114
|
+
</svg>
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const StopIcon = (
|
|
118
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
119
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
120
|
+
</svg>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const UploadIcon = (
|
|
124
|
+
<svg
|
|
125
|
+
width="14"
|
|
126
|
+
height="14"
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
fill="none"
|
|
129
|
+
stroke="currentColor"
|
|
130
|
+
strokeWidth="2"
|
|
131
|
+
strokeLinecap="round"
|
|
132
|
+
strokeLinejoin="round"
|
|
133
|
+
>
|
|
134
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
135
|
+
<polyline points="17 8 12 3 7 8" />
|
|
136
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
137
|
+
</svg>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex flex-col gap-7">
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<span className="text-tiffany-600">{SceneIcon}</span>
|
|
144
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
145
|
+
Scene Visual
|
|
146
|
+
</h2>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Halt generation */}
|
|
150
|
+
{anyGenerating && (
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => sceneStore.haltAll()}
|
|
153
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-red-50 hover:bg-red-100 text-red-600 text-sm font-semibold rounded-2xl border border-red-200 transition-colors"
|
|
154
|
+
>
|
|
155
|
+
{StopIcon}
|
|
156
|
+
Stop Generation
|
|
157
|
+
</button>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Add scene */}
|
|
161
|
+
<button
|
|
162
|
+
onClick={() => sceneStore.addItem()}
|
|
163
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-ink-50 hover:bg-ink-200 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors"
|
|
164
|
+
>
|
|
165
|
+
{PlusIcon}
|
|
166
|
+
Add Scene
|
|
167
|
+
</button>
|
|
168
|
+
|
|
169
|
+
<input
|
|
170
|
+
ref={uploadFileInputRef}
|
|
171
|
+
type="file"
|
|
172
|
+
accept="image/*"
|
|
173
|
+
onChange={handleUploadFileChange}
|
|
174
|
+
className="hidden"
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
{/* Scene items */}
|
|
178
|
+
{sceneStore.items.length === 0 ? (
|
|
179
|
+
<p className="text-xs text-ink-500 italic text-center py-8 border border-dashed border-ink-200 rounded-2xl">
|
|
180
|
+
No scenes yet. Add one to generate a scene visual.
|
|
181
|
+
</p>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="flex flex-col gap-6">
|
|
184
|
+
{sceneStore.items.map((item, index) => (
|
|
185
|
+
<div
|
|
186
|
+
key={item.id}
|
|
187
|
+
className="border border-ink-200 rounded-2xl p-5 flex flex-col gap-3 bg-ink-100/60"
|
|
188
|
+
>
|
|
189
|
+
<div className="flex items-center justify-between">
|
|
190
|
+
<span className="text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
191
|
+
Scene {index + 1}
|
|
192
|
+
</span>
|
|
193
|
+
<button
|
|
194
|
+
onClick={() => sceneStore.removeItem(item.id)}
|
|
195
|
+
disabled={item.generating}
|
|
196
|
+
className="flex items-center justify-center w-6 h-6 rounded-full text-ink-500 hover:bg-red-50 hover:text-red-600 disabled:opacity-50 transition-colors"
|
|
197
|
+
title="Remove scene"
|
|
198
|
+
>
|
|
199
|
+
{TrashIcon}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<textarea
|
|
204
|
+
value={item.prompt}
|
|
205
|
+
onChange={(e) => sceneStore.setPrompt(item.id, e.target.value)}
|
|
206
|
+
placeholder="Describe the scene, e.g. a little lamb standing in a sunny meadow."
|
|
207
|
+
rows={2}
|
|
208
|
+
disabled={item.generating}
|
|
209
|
+
className="w-full px-3 py-2 bg-white border border-ink-200 rounded-xl text-ink-900 text-sm placeholder-ink-500 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
{item.generating || item.uploading ? (
|
|
213
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-ink-50 border border-ink-200 rounded-xl text-xs text-ink-700">
|
|
214
|
+
{SpinnerIcon}
|
|
215
|
+
{item.generating ? "Generating..." : "Uploading..."}
|
|
216
|
+
</div>
|
|
217
|
+
) : (
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => sceneStore.generateItem(projectId, item.id)}
|
|
221
|
+
disabled={!item.prompt.trim()}
|
|
222
|
+
className="flex items-center justify-center gap-2 px-4 py-2 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 transition-colors"
|
|
223
|
+
>
|
|
224
|
+
{SparkleIcon}
|
|
225
|
+
Generate
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => {
|
|
229
|
+
uploadTargetRef.current = item.id;
|
|
230
|
+
uploadFileInputRef.current?.click();
|
|
231
|
+
}}
|
|
232
|
+
className="flex items-center justify-center gap-2 px-4 py-2 text-xs font-medium rounded-xl border border-ink-200 bg-white text-ink-600 hover:border-ink-300 transition-colors"
|
|
233
|
+
>
|
|
234
|
+
{UploadIcon}
|
|
235
|
+
Upload Image
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{item.error && (
|
|
241
|
+
<p className="text-xs text-red-600">{item.error}</p>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{item.logs.length > 0 && (
|
|
245
|
+
<div className="p-2 bg-ink-50 border border-ink-200 rounded-xl max-h-24 overflow-y-auto">
|
|
246
|
+
<pre className="text-[10px] text-ink-600 font-mono whitespace-pre-wrap">
|
|
247
|
+
{item.logs.join("")}
|
|
248
|
+
</pre>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{item.result && (
|
|
253
|
+
<div className="rounded-xl overflow-hidden border border-ink-200 inline-block">
|
|
254
|
+
<img
|
|
255
|
+
src={item.result}
|
|
256
|
+
alt={`Scene ${index + 1}`}
|
|
257
|
+
className="max-w-full h-auto"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useGenerationStore } from "../../stores/generationStore";
|
|
2
|
+
import { ChatUI } from "../ChatUI/ChatUI";
|
|
3
|
+
import MlxVlmServerPanel from "./MlxVlmServerPanel";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
projectId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function StoryWriterTab({ projectId }: Props) {
|
|
10
|
+
const store = useGenerationStore();
|
|
11
|
+
|
|
12
|
+
const StoryWriterIcon = (
|
|
13
|
+
<svg
|
|
14
|
+
width="18"
|
|
15
|
+
height="18"
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
fill="none"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="2"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
>
|
|
23
|
+
<path d="M12 20h9" />
|
|
24
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-7">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<span className="text-tiffany-600">{StoryWriterIcon}</span>
|
|
32
|
+
<h2 className="text-base font-semibold text-ink-900">Story Writer</h2>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{store.agent.serverRunning && store.agent.serverOnline ? null : (
|
|
36
|
+
<MlxVlmServerPanel />
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{store.agent.serverRunning && store.agent.serverOnline ? (
|
|
40
|
+
<ChatUI projectId={projectId} agent="story-writer" />
|
|
41
|
+
) : (
|
|
42
|
+
<div className="flex flex-col items-center justify-center gap-2 p-8 border border-dashed border-ink-200 rounded-2xl">
|
|
43
|
+
<span className="text-ink-500">{StoryWriterIcon}</span>
|
|
44
|
+
<p className="text-xs text-ink-500 italic">
|
|
45
|
+
Start the LLM server to plan videos and build scene CSV scripts.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useQueueStore,
|
|
3
|
+
type QueueTask,
|
|
4
|
+
type QueueTaskStatus,
|
|
5
|
+
} from "../../stores/queueStore";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
projectId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STATUS_META: Record<
|
|
12
|
+
QueueTaskStatus,
|
|
13
|
+
{ label: string; badgeClass: string; dotClass: string }
|
|
14
|
+
> = {
|
|
15
|
+
pending: {
|
|
16
|
+
label: "Queued",
|
|
17
|
+
badgeClass: "bg-ink-100 text-ink-600 border-ink-200",
|
|
18
|
+
dotClass: "bg-ink-400",
|
|
19
|
+
},
|
|
20
|
+
running: {
|
|
21
|
+
label: "Running",
|
|
22
|
+
badgeClass: "bg-tiffany-50 text-tiffany-700 border-tiffany-200",
|
|
23
|
+
dotClass: "bg-tiffany-500",
|
|
24
|
+
},
|
|
25
|
+
completed: {
|
|
26
|
+
label: "Done",
|
|
27
|
+
badgeClass: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
|
28
|
+
dotClass: "bg-emerald-500",
|
|
29
|
+
},
|
|
30
|
+
failed: {
|
|
31
|
+
label: "Failed",
|
|
32
|
+
badgeClass: "bg-red-50 text-red-700 border-red-200",
|
|
33
|
+
dotClass: "bg-red-500",
|
|
34
|
+
},
|
|
35
|
+
cancelled: {
|
|
36
|
+
label: "Cancelled",
|
|
37
|
+
badgeClass: "bg-amber-50 text-amber-700 border-amber-200",
|
|
38
|
+
dotClass: "bg-amber-500",
|
|
39
|
+
},
|
|
40
|
+
paused: {
|
|
41
|
+
label: "Paused",
|
|
42
|
+
badgeClass: "bg-violet-50 text-violet-700 border-violet-200",
|
|
43
|
+
dotClass: "bg-violet-500",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const QueueIcon = (
|
|
48
|
+
<svg
|
|
49
|
+
width="16"
|
|
50
|
+
height="16"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth="2"
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
>
|
|
58
|
+
<line x1="8" y1="6" x2="21" y2="6" />
|
|
59
|
+
<line x1="8" y1="12" x2="21" y2="12" />
|
|
60
|
+
<line x1="8" y1="18" x2="21" y2="18" />
|
|
61
|
+
<line x1="3" y1="6" x2="3.01" y2="6" />
|
|
62
|
+
<line x1="3" y1="12" x2="3.01" y2="12" />
|
|
63
|
+
<line x1="3" y1="18" x2="3.01" y2="18" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const StopIcon = (
|
|
68
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
69
|
+
<rect x="5" y="5" width="14" height="14" rx="2" />
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const ClearIcon = (
|
|
74
|
+
<svg
|
|
75
|
+
width="14"
|
|
76
|
+
height="14"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth="2"
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
>
|
|
84
|
+
<path d="M3 6h18" />
|
|
85
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
|
86
|
+
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
87
|
+
<line x1="10" y1="11" x2="10" y2="17" />
|
|
88
|
+
<line x1="14" y1="11" x2="14" y2="17" />
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const PauseIcon = (
|
|
93
|
+
<svg
|
|
94
|
+
width="14"
|
|
95
|
+
height="14"
|
|
96
|
+
viewBox="0 0 24 24"
|
|
97
|
+
fill="currentColor"
|
|
98
|
+
>
|
|
99
|
+
<rect x="6" y="4" width="4" height="16" rx="1" />
|
|
100
|
+
<rect x="14" y="4" width="4" height="16" rx="1" />
|
|
101
|
+
</svg>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const PlayIcon = (
|
|
105
|
+
<svg
|
|
106
|
+
width="14"
|
|
107
|
+
height="14"
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="currentColor"
|
|
110
|
+
>
|
|
111
|
+
<path d="M8 5v14l11-7z" />
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
function StatusBadge({ status }: { status: QueueTaskStatus }) {
|
|
116
|
+
const meta = STATUS_META[status];
|
|
117
|
+
return (
|
|
118
|
+
<span
|
|
119
|
+
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full border text-[11px] font-semibold ${meta.badgeClass}`}
|
|
120
|
+
>
|
|
121
|
+
<span className={`w-1.5 h-1.5 rounded-full ${meta.dotClass}`} />
|
|
122
|
+
{meta.label}
|
|
123
|
+
</span>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function TaskRow({
|
|
128
|
+
task,
|
|
129
|
+
onCancel,
|
|
130
|
+
}: {
|
|
131
|
+
task: QueueTask;
|
|
132
|
+
onCancel: () => void;
|
|
133
|
+
}) {
|
|
134
|
+
const active = task.status === "pending" || task.status === "running";
|
|
135
|
+
const cancellable = active || task.status === "paused";
|
|
136
|
+
const progress =
|
|
137
|
+
task.progress && task.progress.total > 0
|
|
138
|
+
? Math.round((task.progress.current / task.progress.total) * 100)
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<li className="flex flex-col gap-1.5 px-3 py-2 rounded-xl border border-ink-200 bg-white">
|
|
143
|
+
<div className="flex items-center gap-2">
|
|
144
|
+
<StatusBadge status={task.status} />
|
|
145
|
+
<span className="flex-1 text-sm font-medium text-ink-800 truncate">
|
|
146
|
+
{task.label}
|
|
147
|
+
</span>
|
|
148
|
+
{cancellable && (
|
|
149
|
+
<button
|
|
150
|
+
onClick={onCancel}
|
|
151
|
+
className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium rounded-lg border border-ink-200 text-ink-600 hover:border-red-300 hover:text-red-600 transition-colors"
|
|
152
|
+
>
|
|
153
|
+
{StopIcon}
|
|
154
|
+
Cancel
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{active && task.statusText && (
|
|
160
|
+
<span className="text-xs text-ink-600 truncate">{task.statusText}</span>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{active && progress !== null && (
|
|
164
|
+
<progress
|
|
165
|
+
value={task.progress!.current}
|
|
166
|
+
max={task.progress!.total}
|
|
167
|
+
className="w-full h-2 [&::-webkit-progress-bar]:bg-ink-100 [&::-webkit-progress-value]:bg-tiffany-500"
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{task.status === "failed" && task.error && (
|
|
172
|
+
<span className="text-xs text-red-600 whitespace-pre-wrap">
|
|
173
|
+
{task.error}
|
|
174
|
+
</span>
|
|
175
|
+
)}
|
|
176
|
+
</li>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default function TaskQueuePanel({ projectId }: Props) {
|
|
181
|
+
const tasks = useQueueStore((s) => s.tasks);
|
|
182
|
+
const paused = useQueueStore((s) => s.paused);
|
|
183
|
+
const cancel = useQueueStore((s) => s.cancel);
|
|
184
|
+
const pause = useQueueStore((s) => s.pause);
|
|
185
|
+
const resume = useQueueStore((s) => s.resume);
|
|
186
|
+
const clearFinished = useQueueStore((s) => s.clearFinished);
|
|
187
|
+
|
|
188
|
+
if (tasks.length === 0 && !paused) return null;
|
|
189
|
+
|
|
190
|
+
const hasActive = tasks.some(
|
|
191
|
+
(t) => t.status === "pending" || t.status === "running",
|
|
192
|
+
);
|
|
193
|
+
const hasFinished = tasks.some(
|
|
194
|
+
(t) => t.status !== "pending" && t.status !== "running" && t.status !== "paused",
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex flex-col gap-2">
|
|
199
|
+
<div className="flex items-center gap-2">
|
|
200
|
+
<span className="text-tiffany-600">{QueueIcon}</span>
|
|
201
|
+
<h3 className="text-sm font-semibold text-ink-900">Generation Queue</h3>
|
|
202
|
+
<span className="text-xs text-ink-500">{tasks.length}</span>
|
|
203
|
+
<div className="ml-auto flex items-center gap-2">
|
|
204
|
+
{paused ? (
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => resume(projectId)}
|
|
207
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
|
|
208
|
+
>
|
|
209
|
+
{PlayIcon}
|
|
210
|
+
Resume
|
|
211
|
+
</button>
|
|
212
|
+
) : (
|
|
213
|
+
hasActive && (
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => pause(projectId)}
|
|
216
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
|
|
217
|
+
>
|
|
218
|
+
{PauseIcon}
|
|
219
|
+
Pause
|
|
220
|
+
</button>
|
|
221
|
+
)
|
|
222
|
+
)}
|
|
223
|
+
{hasFinished && (
|
|
224
|
+
<button
|
|
225
|
+
onClick={() => clearFinished(projectId)}
|
|
226
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
|
|
227
|
+
>
|
|
228
|
+
{ClearIcon}
|
|
229
|
+
Clear finished
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<ul className="flex flex-col gap-1.5">
|
|
236
|
+
{tasks.map((task) => (
|
|
237
|
+
<TaskRow
|
|
238
|
+
key={task.id}
|
|
239
|
+
task={task}
|
|
240
|
+
onCancel={() => cancel(projectId, task.id)}
|
|
241
|
+
/>
|
|
242
|
+
))}
|
|
243
|
+
</ul>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useQueueStore } from "../../stores/queueStore";
|
|
3
|
+
|
|
4
|
+
const TerminalIcon = (
|
|
5
|
+
<svg
|
|
6
|
+
width="16"
|
|
7
|
+
height="16"
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="none"
|
|
10
|
+
stroke="currentColor"
|
|
11
|
+
strokeWidth="2"
|
|
12
|
+
strokeLinecap="round"
|
|
13
|
+
strokeLinejoin="round"
|
|
14
|
+
>
|
|
15
|
+
<polyline points="4 17 10 11 4 5" />
|
|
16
|
+
<line x1="12" y1="19" x2="20" y2="19" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export default function TerminalLogPanel() {
|
|
21
|
+
const logs = useQueueStore((s) => s.logs);
|
|
22
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
// Keep the view pinned to the bottom as new terminal output streams in.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const el = scrollRef.current;
|
|
27
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
28
|
+
}, [logs]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-2">
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<span className="text-tiffany-600">{TerminalIcon}</span>
|
|
34
|
+
<h3 className="text-sm font-semibold text-ink-900">Terminal Log</h3>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
ref={scrollRef}
|
|
39
|
+
className="p-3 bg-ink-950 border border-ink-800 rounded-xl max-h-72 overflow-y-auto font-mono text-xs leading-relaxed text-ink-100"
|
|
40
|
+
>
|
|
41
|
+
{logs ? (
|
|
42
|
+
<pre className="whitespace-pre-wrap break-words">{logs}</pre>
|
|
43
|
+
) : (
|
|
44
|
+
<span className="text-ink-500 italic">No logs yet.</span>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|