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