@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,555 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useGenerationStore } from "../../stores/generationStore";
|
|
3
|
+
import { useProjectStore } from "../../stores/projectStore";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
projectId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function FastImageEditTab({ projectId }: Props) {
|
|
10
|
+
const store = useGenerationStore();
|
|
11
|
+
const { openFolder } = useProjectStore();
|
|
12
|
+
const referenceFileRef = useRef<HTMLInputElement>(null);
|
|
13
|
+
const [preview, setPreview] = useState<{
|
|
14
|
+
url: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
} | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
store.checkFastImageEditStatus();
|
|
20
|
+
store.fetchProjectImages(projectId);
|
|
21
|
+
store.fetchCharacterSheets(projectId);
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, [projectId]);
|
|
24
|
+
|
|
25
|
+
// Close the preview modal on Escape.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!preview) return;
|
|
28
|
+
const onKey = (e: KeyboardEvent) => {
|
|
29
|
+
if (e.key === "Escape") setPreview(null);
|
|
30
|
+
};
|
|
31
|
+
window.addEventListener("keydown", onKey);
|
|
32
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
33
|
+
}, [preview]);
|
|
34
|
+
|
|
35
|
+
const handleReferenceUpload = async (
|
|
36
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
37
|
+
) => {
|
|
38
|
+
const file = e.target.files?.[0];
|
|
39
|
+
if (!file) return;
|
|
40
|
+
|
|
41
|
+
const reader = new FileReader();
|
|
42
|
+
reader.onload = async () => {
|
|
43
|
+
const base64 = reader.result as string;
|
|
44
|
+
const uploadedPath = await store.uploadImage(
|
|
45
|
+
projectId,
|
|
46
|
+
base64,
|
|
47
|
+
file.name,
|
|
48
|
+
);
|
|
49
|
+
if (uploadedPath) {
|
|
50
|
+
await store.fetchProjectImages(projectId);
|
|
51
|
+
const { uploadedImageFilename, uploadedImageUrl } =
|
|
52
|
+
useGenerationStore.getState();
|
|
53
|
+
store.toggleFastImageEditImage({
|
|
54
|
+
filename: uploadedImageFilename || file.name,
|
|
55
|
+
url: uploadedImageUrl || "",
|
|
56
|
+
source: "upload",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
reader.readAsDataURL(file);
|
|
61
|
+
|
|
62
|
+
e.target.value = "";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const busy =
|
|
66
|
+
store.fastImageEdit.generating || store.fastImageEdit.downloading;
|
|
67
|
+
|
|
68
|
+
const renderStatus = (
|
|
69
|
+
value: boolean | null,
|
|
70
|
+
okText: string,
|
|
71
|
+
missingText: string,
|
|
72
|
+
) => {
|
|
73
|
+
if (value === null) {
|
|
74
|
+
return (
|
|
75
|
+
<span className="inline-flex items-center gap-1.5 text-ink-500">
|
|
76
|
+
{SpinnerIcon}
|
|
77
|
+
Checking...
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (value) {
|
|
82
|
+
return (
|
|
83
|
+
<span className="inline-flex items-center gap-1.5 text-emerald-600">
|
|
84
|
+
{CheckCircleIcon}
|
|
85
|
+
{okText}
|
|
86
|
+
</span>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return (
|
|
90
|
+
<span className="inline-flex items-center gap-1.5 text-amber-600">
|
|
91
|
+
{AlertCircleIcon}
|
|
92
|
+
{missingText}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ========== SVG Icons ==========
|
|
98
|
+
|
|
99
|
+
const FastImageEditIcon = (
|
|
100
|
+
<svg
|
|
101
|
+
width="18"
|
|
102
|
+
height="18"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
strokeWidth="2"
|
|
107
|
+
strokeLinecap="round"
|
|
108
|
+
strokeLinejoin="round"
|
|
109
|
+
>
|
|
110
|
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
111
|
+
</svg>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const DownloadIcon = (
|
|
115
|
+
<svg
|
|
116
|
+
width="16"
|
|
117
|
+
height="16"
|
|
118
|
+
viewBox="0 0 24 24"
|
|
119
|
+
fill="none"
|
|
120
|
+
stroke="currentColor"
|
|
121
|
+
strokeWidth="2"
|
|
122
|
+
strokeLinecap="round"
|
|
123
|
+
strokeLinejoin="round"
|
|
124
|
+
>
|
|
125
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
126
|
+
<polyline points="7 10 12 15 17 10" />
|
|
127
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
128
|
+
</svg>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const SparkleIcon = (
|
|
132
|
+
<svg
|
|
133
|
+
width="16"
|
|
134
|
+
height="16"
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
fill="none"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
strokeWidth="2"
|
|
139
|
+
>
|
|
140
|
+
<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" />
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const SpinnerIcon = (
|
|
145
|
+
<svg
|
|
146
|
+
className="animate-spin text-tiffany-600"
|
|
147
|
+
width="16"
|
|
148
|
+
height="16"
|
|
149
|
+
viewBox="0 0 24 24"
|
|
150
|
+
fill="none"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
strokeWidth="2"
|
|
153
|
+
>
|
|
154
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
155
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
156
|
+
</svg>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const CheckCircleIcon = (
|
|
160
|
+
<svg
|
|
161
|
+
width="14"
|
|
162
|
+
height="14"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
strokeWidth="2"
|
|
167
|
+
strokeLinecap="round"
|
|
168
|
+
strokeLinejoin="round"
|
|
169
|
+
>
|
|
170
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
171
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
172
|
+
</svg>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const AlertCircleIcon = (
|
|
176
|
+
<svg
|
|
177
|
+
width="14"
|
|
178
|
+
height="14"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
fill="none"
|
|
181
|
+
stroke="currentColor"
|
|
182
|
+
strokeWidth="2"
|
|
183
|
+
strokeLinecap="round"
|
|
184
|
+
strokeLinejoin="round"
|
|
185
|
+
>
|
|
186
|
+
<circle cx="12" cy="12" r="10" />
|
|
187
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
188
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
189
|
+
</svg>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const CloseIcon = (
|
|
193
|
+
<svg
|
|
194
|
+
width="18"
|
|
195
|
+
height="18"
|
|
196
|
+
viewBox="0 0 24 24"
|
|
197
|
+
fill="none"
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
strokeWidth="2"
|
|
200
|
+
strokeLinecap="round"
|
|
201
|
+
strokeLinejoin="round"
|
|
202
|
+
>
|
|
203
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
204
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
205
|
+
</svg>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const MaximizeIcon = (
|
|
209
|
+
<svg
|
|
210
|
+
width="12"
|
|
211
|
+
height="12"
|
|
212
|
+
viewBox="0 0 24 24"
|
|
213
|
+
fill="none"
|
|
214
|
+
stroke="currentColor"
|
|
215
|
+
strokeWidth="2"
|
|
216
|
+
strokeLinecap="round"
|
|
217
|
+
strokeLinejoin="round"
|
|
218
|
+
>
|
|
219
|
+
<polyline points="15 3 21 3 21 9" />
|
|
220
|
+
<polyline points="9 21 3 21 3 15" />
|
|
221
|
+
<line x1="21" y1="3" x2="14" y2="10" />
|
|
222
|
+
<line x1="3" y1="21" x2="10" y2="14" />
|
|
223
|
+
</svg>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const FolderIcon = (
|
|
227
|
+
<svg
|
|
228
|
+
width="16"
|
|
229
|
+
height="16"
|
|
230
|
+
viewBox="0 0 24 24"
|
|
231
|
+
fill="none"
|
|
232
|
+
stroke="currentColor"
|
|
233
|
+
strokeWidth="2"
|
|
234
|
+
strokeLinecap="round"
|
|
235
|
+
strokeLinejoin="round"
|
|
236
|
+
>
|
|
237
|
+
<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" />
|
|
238
|
+
</svg>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="flex flex-col gap-7">
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<span className="text-tiffany-600">{FastImageEditIcon}</span>
|
|
245
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
246
|
+
Fast Image Edit
|
|
247
|
+
</h2>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* ===== Setup: download model ===== */}
|
|
251
|
+
<div>
|
|
252
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
253
|
+
Model Setup
|
|
254
|
+
</label>
|
|
255
|
+
<div className="flex flex-wrap gap-2">
|
|
256
|
+
<button
|
|
257
|
+
onClick={() => store.downloadFastImageEditModel()}
|
|
258
|
+
disabled={store.fastImageEdit.downloading}
|
|
259
|
+
className="flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300 disabled:opacity-50"
|
|
260
|
+
>
|
|
261
|
+
{store.fastImageEdit.downloading ? SpinnerIcon : DownloadIcon}
|
|
262
|
+
Download FLUX.2 Klein Model
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 mt-2 text-xs font-medium">
|
|
267
|
+
{renderStatus(
|
|
268
|
+
store.fastImageEdit.modelDownloaded,
|
|
269
|
+
"FLUX.2 Klein model downloaded",
|
|
270
|
+
"FLUX.2 Klein model not downloaded",
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{store.fastImageEdit.downloadingLogs.length > 0 && (
|
|
275
|
+
<div className="mt-2 p-4 bg-ink-50 border border-ink-200 rounded-2xl max-h-32 overflow-y-auto">
|
|
276
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
277
|
+
{store.fastImageEdit.downloadingLogs.join("")}
|
|
278
|
+
</pre>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{store.fastImageEdit.downloadingError && (
|
|
282
|
+
<div className="mt-2 p-4 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-xs">
|
|
283
|
+
{store.fastImageEdit.downloadingError}
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* ===== Reference images picker ===== */}
|
|
289
|
+
<div>
|
|
290
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
291
|
+
Reference Images
|
|
292
|
+
</label>
|
|
293
|
+
<div className="flex items-center gap-2">
|
|
294
|
+
<input
|
|
295
|
+
ref={referenceFileRef}
|
|
296
|
+
type="file"
|
|
297
|
+
accept="image/*"
|
|
298
|
+
onChange={handleReferenceUpload}
|
|
299
|
+
disabled={store.uploading}
|
|
300
|
+
className="inline-block text-sm text-ink-700 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-medium file:bg-ink-100 file:text-ink-700 hover:file:bg-ink-200 file:cursor-pointer file:transition-colors disabled:opacity-50"
|
|
301
|
+
/>
|
|
302
|
+
{store.fastImageEdit.referenceImages.length > 0 && (
|
|
303
|
+
<button
|
|
304
|
+
onClick={() => store.clearFastImageEditImages()}
|
|
305
|
+
disabled={busy}
|
|
306
|
+
className="px-3 py-2 text-xs font-medium text-red-600 hover:bg-red-50 rounded-xl transition-colors disabled:opacity-50"
|
|
307
|
+
>
|
|
308
|
+
Clear ({store.fastImageEdit.referenceImages.length})
|
|
309
|
+
</button>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
{store.uploading && (
|
|
313
|
+
<p className="text-xs text-ink-600 mt-1">Uploading...</p>
|
|
314
|
+
)}
|
|
315
|
+
{store.uploadError && (
|
|
316
|
+
<p className="text-xs text-red-600 mt-1">{store.uploadError}</p>
|
|
317
|
+
)}
|
|
318
|
+
{store.projectImagesLoading ? (
|
|
319
|
+
<p className="text-xs text-ink-500 italic py-4 text-center">
|
|
320
|
+
Loading images...
|
|
321
|
+
</p>
|
|
322
|
+
) : store.projectImages.length === 0 ? (
|
|
323
|
+
<p className="text-xs text-ink-500 italic py-4 text-center border border-dashed border-ink-200 rounded-2xl">
|
|
324
|
+
No images yet. Upload reference images above.
|
|
325
|
+
</p>
|
|
326
|
+
) : (
|
|
327
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-2 p-1">
|
|
328
|
+
{store.projectImages.map((img) => {
|
|
329
|
+
const isSelected = store.fastImageEdit.referenceImages.some(
|
|
330
|
+
(r) => r.filename === img.filename,
|
|
331
|
+
);
|
|
332
|
+
const fullUrl = img.url.startsWith("http")
|
|
333
|
+
? img.url
|
|
334
|
+
: `http://localhost:${(window as any).PORT}${img.url}`;
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
key={`${img.source}-${img.filename}`}
|
|
338
|
+
className="relative group"
|
|
339
|
+
>
|
|
340
|
+
<button
|
|
341
|
+
onClick={() => store.toggleFastImageEditImage(img)}
|
|
342
|
+
disabled={store.fastImageEdit.generating}
|
|
343
|
+
className={`w-full relative rounded-xl border-2 overflow-hidden transition-all ${
|
|
344
|
+
isSelected
|
|
345
|
+
? "border-tiffany-500 ring-2 ring-tiffany-500/40"
|
|
346
|
+
: "border-ink-200 hover:border-ink-300"
|
|
347
|
+
} disabled:opacity-50`}
|
|
348
|
+
>
|
|
349
|
+
<img
|
|
350
|
+
src={fullUrl}
|
|
351
|
+
alt={img.filename}
|
|
352
|
+
className="aspect-square object-cover object-center w-full"
|
|
353
|
+
/>
|
|
354
|
+
<span className="absolute bottom-0 left-0 right-0 bg-white/80 backdrop-blur-sm px-1.5 py-0.5 text-[10px] text-ink-700 truncate text-center">
|
|
355
|
+
{img.filename}
|
|
356
|
+
</span>
|
|
357
|
+
</button>
|
|
358
|
+
<button
|
|
359
|
+
onClick={() =>
|
|
360
|
+
setPreview({ url: fullUrl, filename: img.filename })
|
|
361
|
+
}
|
|
362
|
+
className="absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded-full bg-black/50 text-white hover:bg-tiffany-600 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
|
363
|
+
title="Preview"
|
|
364
|
+
>
|
|
365
|
+
{MaximizeIcon}
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
{store.fastImageEdit.referenceImages.length > 0 && (
|
|
373
|
+
<p className="text-xs text-ink-600/60 mt-1.5">
|
|
374
|
+
Selected:{" "}
|
|
375
|
+
<span className="font-medium text-ink-700">
|
|
376
|
+
{store.fastImageEdit.referenceImages
|
|
377
|
+
.map((r) => r.filename)
|
|
378
|
+
.join(", ")}
|
|
379
|
+
</span>
|
|
380
|
+
</p>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
{/* ===== Character sheet picker ===== */}
|
|
385
|
+
{store.characterSheets.length > 0 && (
|
|
386
|
+
<div>
|
|
387
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
388
|
+
Character Sheet
|
|
389
|
+
</label>
|
|
390
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-2 p-1">
|
|
391
|
+
{store.characterSheets.map((sheet) => {
|
|
392
|
+
const isSelected = store.fastImageEdit.referenceImages.some(
|
|
393
|
+
(r) => r.filename === sheet.filename,
|
|
394
|
+
);
|
|
395
|
+
const fullUrl = sheet.url.startsWith("http")
|
|
396
|
+
? sheet.url
|
|
397
|
+
: `http://localhost:${(window as any).PORT}${sheet.url}`;
|
|
398
|
+
return (
|
|
399
|
+
<div key={sheet.filename} className="relative group">
|
|
400
|
+
<button
|
|
401
|
+
onClick={() => store.toggleFastImageEditImage(sheet)}
|
|
402
|
+
disabled={store.fastImageEdit.generating}
|
|
403
|
+
className={`w-full relative rounded-xl border-2 overflow-hidden transition-all ${
|
|
404
|
+
isSelected
|
|
405
|
+
? "border-tiffany-500 ring-2 ring-tiffany-500/40"
|
|
406
|
+
: "border-ink-200 hover:border-ink-300"
|
|
407
|
+
} disabled:opacity-50`}
|
|
408
|
+
>
|
|
409
|
+
<img
|
|
410
|
+
src={fullUrl}
|
|
411
|
+
alt={sheet.filename}
|
|
412
|
+
className="aspect-square object-cover object-center w-full"
|
|
413
|
+
/>
|
|
414
|
+
<span className="absolute bottom-0 left-0 right-0 bg-white/80 backdrop-blur-sm px-1.5 py-0.5 text-[10px] text-ink-700 truncate text-center">
|
|
415
|
+
{sheet.filename}
|
|
416
|
+
</span>
|
|
417
|
+
</button>
|
|
418
|
+
<button
|
|
419
|
+
onClick={() =>
|
|
420
|
+
setPreview({ url: fullUrl, filename: sheet.filename })
|
|
421
|
+
}
|
|
422
|
+
className="absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded-full bg-black/50 text-white hover:bg-tiffany-600 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
|
423
|
+
title="Preview"
|
|
424
|
+
>
|
|
425
|
+
{MaximizeIcon}
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
|
|
434
|
+
{/* ===== Prompt ===== */}
|
|
435
|
+
<div>
|
|
436
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
437
|
+
Image Prompt
|
|
438
|
+
</label>
|
|
439
|
+
<textarea
|
|
440
|
+
value={store.fastImageEdit.prompt}
|
|
441
|
+
onChange={(e) => store.setFastImageEditPrompt(e.target.value)}
|
|
442
|
+
placeholder="Describe the composite image, e.g. The lamb and The ninja standing next to each other. taking a photo in a photo shooting studio."
|
|
443
|
+
rows={3}
|
|
444
|
+
disabled={store.fastImageEdit.generating}
|
|
445
|
+
className="w-full px-4 py-3 bg-ink-50 border border-ink-200 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"
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{/* ===== Generate ===== */}
|
|
450
|
+
{store.fastImageEdit.generating ? (
|
|
451
|
+
<div className="flex items-center gap-2 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
452
|
+
{SpinnerIcon}
|
|
453
|
+
<span className="text-sm font-medium text-ink-700">
|
|
454
|
+
Generating...
|
|
455
|
+
</span>
|
|
456
|
+
</div>
|
|
457
|
+
) : (
|
|
458
|
+
<button
|
|
459
|
+
onClick={() => store.generateFastImageEdit(projectId)}
|
|
460
|
+
disabled={
|
|
461
|
+
busy ||
|
|
462
|
+
!store.fastImageEdit.prompt.trim() ||
|
|
463
|
+
store.fastImageEdit.referenceImages.length === 0
|
|
464
|
+
}
|
|
465
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-3 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-sm hover:shadow-md disabled:shadow-none"
|
|
466
|
+
>
|
|
467
|
+
{SparkleIcon}
|
|
468
|
+
Generate Image
|
|
469
|
+
</button>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* ===== Open output folder ===== */}
|
|
473
|
+
<button
|
|
474
|
+
onClick={() => openFolder(projectId, "output")}
|
|
475
|
+
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"
|
|
476
|
+
>
|
|
477
|
+
{FolderIcon}
|
|
478
|
+
Open Output Folder
|
|
479
|
+
</button>
|
|
480
|
+
|
|
481
|
+
{/* ===== Error ===== */}
|
|
482
|
+
{store.fastImageEdit.error && (
|
|
483
|
+
<div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
|
|
484
|
+
{store.fastImageEdit.error}
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
|
|
488
|
+
{/* ===== Logs ===== */}
|
|
489
|
+
{store.fastImageEdit.logs.length > 0 && (
|
|
490
|
+
<div className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto">
|
|
491
|
+
<p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
492
|
+
Logs
|
|
493
|
+
</p>
|
|
494
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
495
|
+
{store.fastImageEdit.logs.join("")}
|
|
496
|
+
</pre>
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
|
|
500
|
+
{/* ===== Result ===== */}
|
|
501
|
+
{store.fastImageEdit.result && (
|
|
502
|
+
<div>
|
|
503
|
+
<div className="flex items-center justify-between mb-2">
|
|
504
|
+
<label className="text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
505
|
+
Generated Image
|
|
506
|
+
</label>
|
|
507
|
+
<button
|
|
508
|
+
onClick={() => store.clearFastImageEditResult()}
|
|
509
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors"
|
|
510
|
+
>
|
|
511
|
+
{CloseIcon}
|
|
512
|
+
Remove
|
|
513
|
+
</button>
|
|
514
|
+
</div>
|
|
515
|
+
<div className="rounded-2xl overflow-hidden border border-ink-200 shadow-card inline-block">
|
|
516
|
+
<img
|
|
517
|
+
src={store.fastImageEdit.result}
|
|
518
|
+
alt="Generated"
|
|
519
|
+
className="max-w-full h-auto"
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
{/* ===== Preview modal ===== */}
|
|
526
|
+
{preview && (
|
|
527
|
+
<div
|
|
528
|
+
className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-8"
|
|
529
|
+
onClick={() => setPreview(null)}
|
|
530
|
+
>
|
|
531
|
+
<div
|
|
532
|
+
className="relative max-w-[90vw] max-h-[90vh] flex flex-col items-center gap-3"
|
|
533
|
+
onClick={(e) => e.stopPropagation()}
|
|
534
|
+
>
|
|
535
|
+
<img
|
|
536
|
+
src={preview.url}
|
|
537
|
+
alt={preview.filename}
|
|
538
|
+
className="max-w-full max-h-[80vh] object-contain rounded-2xl shadow-2xl"
|
|
539
|
+
/>
|
|
540
|
+
<span className="text-xs text-white/70 truncate max-w-full">
|
|
541
|
+
{preview.filename}
|
|
542
|
+
</span>
|
|
543
|
+
<button
|
|
544
|
+
onClick={() => setPreview(null)}
|
|
545
|
+
className="absolute -top-3 -right-3 flex items-center justify-center w-9 h-9 bg-white text-ink-700 rounded-full shadow-lg hover:bg-ink-200 transition-colors"
|
|
546
|
+
title="Close (Esc)"
|
|
547
|
+
>
|
|
548
|
+
{CloseIcon}
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
)}
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
}
|