@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,574 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useWorkspaceStore,
|
|
4
|
+
workspacePreviewUrl,
|
|
5
|
+
type WorkspaceFile,
|
|
6
|
+
} from "../stores/workspaceStore";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
projectId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatSize(bytes: number): string {
|
|
13
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
14
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
15
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function AgentWorkspace({ projectId }: Props) {
|
|
19
|
+
const ws = useWorkspaceStore();
|
|
20
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
21
|
+
const listRef = useRef<HTMLUListElement>(null);
|
|
22
|
+
|
|
23
|
+
const [preview, setPreview] = useState<WorkspaceFile | null>(null);
|
|
24
|
+
const [previewContent, setPreviewContent] = useState<string | null>(null);
|
|
25
|
+
const [editing, setEditing] = useState<WorkspaceFile | null>(null);
|
|
26
|
+
const [editContent, setEditContent] = useState("");
|
|
27
|
+
const [renaming, setRenaming] = useState<string | null>(null);
|
|
28
|
+
const [renameValue, setRenameValue] = useState("");
|
|
29
|
+
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
ws.fetchFiles(projectId);
|
|
33
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
+
}, [projectId]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!preview || !listRef.current) return;
|
|
38
|
+
const index = ws.files.findIndex((f) => f.path === preview.path);
|
|
39
|
+
if (index < 0) return;
|
|
40
|
+
const items = listRef.current.querySelectorAll("li");
|
|
41
|
+
if (items[index]) items[index].scrollIntoView({ block: "nearest" });
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [preview]);
|
|
44
|
+
|
|
45
|
+
// ========== Handlers ==========
|
|
46
|
+
|
|
47
|
+
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
const file = e.target.files?.[0];
|
|
49
|
+
if (!file) return;
|
|
50
|
+
const reader = new FileReader();
|
|
51
|
+
reader.onload = () => {
|
|
52
|
+
ws.uploadFile(projectId, reader.result as string, file.name);
|
|
53
|
+
};
|
|
54
|
+
reader.readAsDataURL(file);
|
|
55
|
+
e.target.value = "";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleSelect = async (f: WorkspaceFile) => {
|
|
59
|
+
setEditing(null);
|
|
60
|
+
setPreview(f);
|
|
61
|
+
if (f.kind === "text") {
|
|
62
|
+
const content = await ws.readFileContent(projectId, f.path);
|
|
63
|
+
setPreviewContent(content ?? "");
|
|
64
|
+
} else {
|
|
65
|
+
setPreviewContent(null);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleListKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
|
|
70
|
+
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
|
|
71
|
+
const target = e.target as HTMLElement;
|
|
72
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return;
|
|
73
|
+
if (ws.files.length === 0) return;
|
|
74
|
+
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
const currentIndex = preview
|
|
77
|
+
? ws.files.findIndex((f) => f.path === preview.path)
|
|
78
|
+
: -1;
|
|
79
|
+
let nextIndex = e.key === "ArrowDown" ? currentIndex + 1 : currentIndex - 1;
|
|
80
|
+
if (nextIndex < 0) nextIndex = 0;
|
|
81
|
+
if (nextIndex >= ws.files.length) nextIndex = ws.files.length - 1;
|
|
82
|
+
|
|
83
|
+
if (nextIndex !== currentIndex) handleSelect(ws.files[nextIndex]);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleEdit = async (f: WorkspaceFile) => {
|
|
87
|
+
setPreview(null);
|
|
88
|
+
const content = await ws.readFileContent(projectId, f.path);
|
|
89
|
+
setEditing(f);
|
|
90
|
+
setEditContent(content ?? "");
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleSave = async () => {
|
|
94
|
+
if (!editing) return;
|
|
95
|
+
await ws.writeFileContent(projectId, editing.path, editContent);
|
|
96
|
+
setEditing(null);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleRename = (f: WorkspaceFile) => {
|
|
100
|
+
setRenaming(f.path);
|
|
101
|
+
setRenameValue(f.name);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const confirmRename = async (f: WorkspaceFile) => {
|
|
105
|
+
const name = renameValue.trim();
|
|
106
|
+
// Only bare filenames — no separators, `..`, or leading dots.
|
|
107
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
108
|
+
setRenaming(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const dir = f.path.slice(0, f.path.lastIndexOf("/") + 1);
|
|
112
|
+
await ws.renameFile(projectId, f.path, dir + name);
|
|
113
|
+
setRenaming(null);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleDelete = (f: WorkspaceFile) => {
|
|
117
|
+
setConfirmDelete(f.path);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const confirmDeleteAction = async () => {
|
|
121
|
+
if (!confirmDelete) return;
|
|
122
|
+
const path = confirmDelete;
|
|
123
|
+
await ws.removeFile(projectId, path);
|
|
124
|
+
setConfirmDelete(null);
|
|
125
|
+
if (preview?.path === path) {
|
|
126
|
+
setPreview(null);
|
|
127
|
+
setPreviewContent(null);
|
|
128
|
+
}
|
|
129
|
+
if (editing?.path === path) setEditing(null);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!confirmDelete) return;
|
|
134
|
+
const onKey = (e: KeyboardEvent) => {
|
|
135
|
+
if (e.key === "Escape") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
setConfirmDelete(null);
|
|
138
|
+
} else if (e.key === "Enter") {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
confirmDeleteAction();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("keydown", onKey);
|
|
144
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
}, [confirmDelete]);
|
|
147
|
+
|
|
148
|
+
// ========== SVG Icons ==========
|
|
149
|
+
|
|
150
|
+
const UploadIcon = (
|
|
151
|
+
<svg
|
|
152
|
+
width="14"
|
|
153
|
+
height="14"
|
|
154
|
+
viewBox="0 0 24 24"
|
|
155
|
+
fill="none"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
strokeWidth="2"
|
|
158
|
+
strokeLinecap="round"
|
|
159
|
+
strokeLinejoin="round"
|
|
160
|
+
>
|
|
161
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
162
|
+
<polyline points="17 8 12 3 7 8" />
|
|
163
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
164
|
+
</svg>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const RefreshIcon = (
|
|
168
|
+
<svg
|
|
169
|
+
width="14"
|
|
170
|
+
height="14"
|
|
171
|
+
viewBox="0 0 24 24"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
strokeWidth="2"
|
|
175
|
+
strokeLinecap="round"
|
|
176
|
+
strokeLinejoin="round"
|
|
177
|
+
>
|
|
178
|
+
<polyline points="23 4 23 10 17 10" />
|
|
179
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
180
|
+
</svg>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const FolderIcon = (
|
|
184
|
+
<svg
|
|
185
|
+
width="14"
|
|
186
|
+
height="14"
|
|
187
|
+
viewBox="0 0 24 24"
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
strokeWidth="2"
|
|
191
|
+
strokeLinecap="round"
|
|
192
|
+
strokeLinejoin="round"
|
|
193
|
+
>
|
|
194
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
195
|
+
</svg>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const TrashIcon = (
|
|
199
|
+
<svg
|
|
200
|
+
width="14"
|
|
201
|
+
height="14"
|
|
202
|
+
viewBox="0 0 24 24"
|
|
203
|
+
fill="none"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
strokeWidth="2"
|
|
206
|
+
strokeLinecap="round"
|
|
207
|
+
strokeLinejoin="round"
|
|
208
|
+
>
|
|
209
|
+
<polyline points="3 6 5 6 21 6" />
|
|
210
|
+
<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" />
|
|
211
|
+
</svg>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const PencilIcon = (
|
|
215
|
+
<svg
|
|
216
|
+
width="14"
|
|
217
|
+
height="14"
|
|
218
|
+
viewBox="0 0 24 24"
|
|
219
|
+
fill="none"
|
|
220
|
+
stroke="currentColor"
|
|
221
|
+
strokeWidth="2"
|
|
222
|
+
strokeLinecap="round"
|
|
223
|
+
strokeLinejoin="round"
|
|
224
|
+
>
|
|
225
|
+
<path d="M12 20h9" />
|
|
226
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
227
|
+
</svg>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const FileIcon = (
|
|
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
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
242
|
+
<polyline points="14 2 14 8 20 8" />
|
|
243
|
+
</svg>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const ImageIcon = (
|
|
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
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
258
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
259
|
+
<polyline points="21 15 16 10 5 21" />
|
|
260
|
+
</svg>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const VideoIcon = (
|
|
264
|
+
<svg
|
|
265
|
+
width="14"
|
|
266
|
+
height="14"
|
|
267
|
+
viewBox="0 0 24 24"
|
|
268
|
+
fill="none"
|
|
269
|
+
stroke="currentColor"
|
|
270
|
+
strokeWidth="2"
|
|
271
|
+
strokeLinecap="round"
|
|
272
|
+
strokeLinejoin="round"
|
|
273
|
+
>
|
|
274
|
+
<polygon points="23 7 16 12 23 17 23 7" />
|
|
275
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
276
|
+
</svg>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const CloseIcon = (
|
|
280
|
+
<svg
|
|
281
|
+
width="14"
|
|
282
|
+
height="14"
|
|
283
|
+
viewBox="0 0 24 24"
|
|
284
|
+
fill="none"
|
|
285
|
+
stroke="currentColor"
|
|
286
|
+
strokeWidth="2"
|
|
287
|
+
strokeLinecap="round"
|
|
288
|
+
strokeLinejoin="round"
|
|
289
|
+
>
|
|
290
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
291
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
292
|
+
</svg>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const kindIcon = (f: WorkspaceFile) => {
|
|
296
|
+
if (f.kind === "image")
|
|
297
|
+
return <span className="text-tiffany-600">{ImageIcon}</span>;
|
|
298
|
+
if (f.kind === "video")
|
|
299
|
+
return <span className="text-tiffany-600">{VideoIcon}</span>;
|
|
300
|
+
if (f.kind === "text")
|
|
301
|
+
return <span className="text-tiffany-600">{FileIcon}</span>;
|
|
302
|
+
return <span className="text-ink-500">{FileIcon}</span>;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex flex-col gap-3">
|
|
307
|
+
{/* Header + upload */}
|
|
308
|
+
<div className="flex items-center justify-between">
|
|
309
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
310
|
+
Agent Workspace
|
|
311
|
+
</label>
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => ws.fetchFiles(projectId)}
|
|
315
|
+
title="Refresh"
|
|
316
|
+
className="flex items-center justify-center w-8 h-8 rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
317
|
+
>
|
|
318
|
+
{RefreshIcon}
|
|
319
|
+
</button>
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => ws.openWorkspace(projectId)}
|
|
322
|
+
title="Open workspace folder"
|
|
323
|
+
className="flex items-center justify-center w-8 h-8 rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
324
|
+
>
|
|
325
|
+
{FolderIcon}
|
|
326
|
+
</button>
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => fileInputRef.current?.click()}
|
|
329
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
330
|
+
>
|
|
331
|
+
{UploadIcon}
|
|
332
|
+
Upload File
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
<input
|
|
336
|
+
ref={fileInputRef}
|
|
337
|
+
type="file"
|
|
338
|
+
accept="image/*,video/*,text/*,.csv,.md,.txt,.json,.log"
|
|
339
|
+
onChange={handleUpload}
|
|
340
|
+
className="hidden"
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{ws.error && (
|
|
345
|
+
<div className="p-2 bg-red-50 border border-red-200 rounded-xl text-red-600 text-xs">
|
|
346
|
+
{ws.error}
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
<div className="flex gap-3 items-start">
|
|
351
|
+
{/* Left column: file list + editor */}
|
|
352
|
+
<div className="flex-1 min-w-0 flex flex-col gap-3">
|
|
353
|
+
{/* File list */}
|
|
354
|
+
<div className="border border-ink-200 rounded-2xl overflow-hidden">
|
|
355
|
+
<div className="max-h-64 overflow-y-auto">
|
|
356
|
+
{ws.loading ? (
|
|
357
|
+
<p className="text-xs text-ink-500 italic text-center py-6">
|
|
358
|
+
Loading files...
|
|
359
|
+
</p>
|
|
360
|
+
) : ws.files.length === 0 ? (
|
|
361
|
+
<p className="text-xs text-ink-500 italic text-center py-6">
|
|
362
|
+
No files yet. Upload a file or let the agent save a memory.
|
|
363
|
+
</p>
|
|
364
|
+
) : (
|
|
365
|
+
<ul
|
|
366
|
+
ref={listRef}
|
|
367
|
+
tabIndex={0}
|
|
368
|
+
onKeyDown={handleListKeyDown}
|
|
369
|
+
className="divide-y divide-ink-200 outline-none focus:ring-2 focus:ring-inset focus:ring-tiffany-500"
|
|
370
|
+
>
|
|
371
|
+
{ws.files.map((f) => (
|
|
372
|
+
<li
|
|
373
|
+
key={f.path}
|
|
374
|
+
onClick={() => handleSelect(f)}
|
|
375
|
+
className={`flex items-center gap-2 px-3 py-2 hover:bg-ink-100/60 transition-colors cursor-pointer ${
|
|
376
|
+
preview?.path === f.path ? "bg-ink-100/60" : ""
|
|
377
|
+
}`}
|
|
378
|
+
>
|
|
379
|
+
{kindIcon(f)}
|
|
380
|
+
<div className="flex-1 min-w-0">
|
|
381
|
+
{renaming === f.path ? (
|
|
382
|
+
<input
|
|
383
|
+
type="text"
|
|
384
|
+
value={renameValue}
|
|
385
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
386
|
+
onClick={(e) => e.stopPropagation()}
|
|
387
|
+
onKeyDown={(e) => {
|
|
388
|
+
if (e.key === "Enter") confirmRename(f);
|
|
389
|
+
if (e.key === "Escape") setRenaming(null);
|
|
390
|
+
}}
|
|
391
|
+
autoFocus
|
|
392
|
+
className="w-full px-2 py-1 bg-white border border-ink-300 rounded text-xs text-ink-900 focus:outline-none focus:ring-1 focus:ring-tiffany-500"
|
|
393
|
+
/>
|
|
394
|
+
) : (
|
|
395
|
+
<p
|
|
396
|
+
className="text-xs text-ink-800 truncate"
|
|
397
|
+
title={f.path}
|
|
398
|
+
>
|
|
399
|
+
{f.path}
|
|
400
|
+
</p>
|
|
401
|
+
)}
|
|
402
|
+
<p className="text-[10px] text-ink-500">
|
|
403
|
+
{f.kind} · {formatSize(f.size)}
|
|
404
|
+
</p>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
{renaming === f.path ? (
|
|
408
|
+
<button
|
|
409
|
+
onClick={(e) => {
|
|
410
|
+
e.stopPropagation();
|
|
411
|
+
confirmRename(f);
|
|
412
|
+
}}
|
|
413
|
+
className="px-2 py-1 text-[10px] font-medium text-emerald-600 hover:bg-emerald-50 rounded transition-colors"
|
|
414
|
+
>
|
|
415
|
+
Save
|
|
416
|
+
</button>
|
|
417
|
+
) : (
|
|
418
|
+
<>
|
|
419
|
+
{f.kind === "text" && (
|
|
420
|
+
<button
|
|
421
|
+
onClick={(e) => {
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
handleEdit(f);
|
|
424
|
+
}}
|
|
425
|
+
className="p-1.5 text-tiffany-600 hover:bg-ink-200 rounded transition-colors"
|
|
426
|
+
title="Edit"
|
|
427
|
+
>
|
|
428
|
+
{PencilIcon}
|
|
429
|
+
</button>
|
|
430
|
+
)}
|
|
431
|
+
<button
|
|
432
|
+
onClick={(e) => {
|
|
433
|
+
e.stopPropagation();
|
|
434
|
+
handleRename(f);
|
|
435
|
+
}}
|
|
436
|
+
className="p-1.5 text-tiffany-600 hover:bg-ink-200 rounded transition-colors"
|
|
437
|
+
title="Rename"
|
|
438
|
+
>
|
|
439
|
+
{PencilIcon}
|
|
440
|
+
</button>
|
|
441
|
+
<button
|
|
442
|
+
onClick={(e) => {
|
|
443
|
+
e.stopPropagation();
|
|
444
|
+
handleDelete(f);
|
|
445
|
+
}}
|
|
446
|
+
className="p-1.5 text-red-400 hover:bg-red-50 rounded transition-colors"
|
|
447
|
+
title="Delete"
|
|
448
|
+
>
|
|
449
|
+
{TrashIcon}
|
|
450
|
+
</button>
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
</li>
|
|
454
|
+
))}
|
|
455
|
+
</ul>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{/* Editor panel */}
|
|
461
|
+
{editing && (
|
|
462
|
+
<div className="border border-ink-200 rounded-2xl overflow-hidden">
|
|
463
|
+
<div className="flex items-center justify-between px-3 py-2 bg-ink-50 border-b border-ink-200">
|
|
464
|
+
<span className="text-xs font-medium text-ink-700 truncate">
|
|
465
|
+
Editing {editing.path}
|
|
466
|
+
</span>
|
|
467
|
+
<div className="flex items-center gap-1.5">
|
|
468
|
+
<button
|
|
469
|
+
onClick={handleSave}
|
|
470
|
+
className="px-2 py-1 text-[10px] font-medium text-emerald-600 hover:bg-emerald-50 rounded transition-colors"
|
|
471
|
+
>
|
|
472
|
+
Save
|
|
473
|
+
</button>
|
|
474
|
+
<button
|
|
475
|
+
onClick={() => setEditing(null)}
|
|
476
|
+
className="p-1 text-tiffany-600 hover:bg-ink-200 rounded transition-colors"
|
|
477
|
+
title="Close"
|
|
478
|
+
>
|
|
479
|
+
{CloseIcon}
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
<textarea
|
|
484
|
+
value={editContent}
|
|
485
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
486
|
+
rows={10}
|
|
487
|
+
className="w-full px-3 py-2 bg-white text-ink-900 text-xs font-mono focus:outline-none resize-y"
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
{/* Right column: preview pane */}
|
|
494
|
+
<div className="w-72 shrink-0">
|
|
495
|
+
<div className="border border-ink-200 rounded-2xl overflow-hidden">
|
|
496
|
+
<div className="flex items-center justify-between px-3 py-2 bg-ink-50 border-b border-ink-200">
|
|
497
|
+
<span className="text-xs font-medium text-ink-700 truncate">
|
|
498
|
+
{preview ? preview.path : "Preview"}
|
|
499
|
+
</span>
|
|
500
|
+
{preview && (
|
|
501
|
+
<button
|
|
502
|
+
onClick={() => {
|
|
503
|
+
setPreview(null);
|
|
504
|
+
setPreviewContent(null);
|
|
505
|
+
}}
|
|
506
|
+
className="p-1 text-tiffany-600 hover:bg-ink-200 rounded transition-colors"
|
|
507
|
+
title="Close"
|
|
508
|
+
>
|
|
509
|
+
{CloseIcon}
|
|
510
|
+
</button>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
<div className="p-2 flex justify-center bg-ink-100/60">
|
|
514
|
+
{!preview ? (
|
|
515
|
+
<p className="text-xs text-ink-500 italic text-center py-10">
|
|
516
|
+
Select a file to preview
|
|
517
|
+
</p>
|
|
518
|
+
) : preview.kind === "image" ? (
|
|
519
|
+
<img
|
|
520
|
+
src={workspacePreviewUrl(projectId, preview.path)}
|
|
521
|
+
alt={preview.name}
|
|
522
|
+
className="max-h-72 max-w-full object-contain"
|
|
523
|
+
/>
|
|
524
|
+
) : preview.kind === "video" ? (
|
|
525
|
+
<video
|
|
526
|
+
src={workspacePreviewUrl(projectId, preview.path)}
|
|
527
|
+
controls
|
|
528
|
+
className="max-h-72 max-w-full"
|
|
529
|
+
/>
|
|
530
|
+
) : preview.kind === "text" ? (
|
|
531
|
+
<pre className="w-full max-h-72 overflow-auto p-2 bg-white rounded text-[11px] text-ink-800 whitespace-pre-wrap break-words">
|
|
532
|
+
{previewContent ?? ""}
|
|
533
|
+
</pre>
|
|
534
|
+
) : (
|
|
535
|
+
<p className="text-xs text-ink-500 italic text-center py-10">
|
|
536
|
+
No preview available
|
|
537
|
+
</p>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
{/* Delete confirmation modal */}
|
|
545
|
+
{confirmDelete && (
|
|
546
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
|
547
|
+
<div className="bg-white rounded-3xl shadow-card p-6 w-80">
|
|
548
|
+
<h3 className="text-sm font-semibold text-ink-900 mb-2">
|
|
549
|
+
Delete File
|
|
550
|
+
</h3>
|
|
551
|
+
<p className="text-xs text-ink-600 mb-4">
|
|
552
|
+
Are you sure you want to delete "{confirmDelete}"? This action
|
|
553
|
+
cannot be undone.
|
|
554
|
+
</p>
|
|
555
|
+
<div className="flex justify-end gap-2">
|
|
556
|
+
<button
|
|
557
|
+
onClick={() => setConfirmDelete(null)}
|
|
558
|
+
className="px-3 py-2 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 transition-colors"
|
|
559
|
+
>
|
|
560
|
+
Cancel
|
|
561
|
+
</button>
|
|
562
|
+
<button
|
|
563
|
+
onClick={confirmDeleteAction}
|
|
564
|
+
className="px-3 py-2 text-xs font-medium rounded-xl bg-red-500 hover:bg-red-600 text-white transition-colors"
|
|
565
|
+
>
|
|
566
|
+
Delete
|
|
567
|
+
</button>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|