@weirdfingers/baseboards 0.2.1 → 0.4.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/README.md +14 -4
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +148 -0
- package/templates/api/Dockerfile +2 -2
- package/templates/api/README.md +138 -6
- package/templates/api/config/generators.yaml +41 -7
- package/templates/api/docs/TESTING_LIVE_APIS.md +417 -0
- package/templates/api/pyproject.toml +49 -9
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/auth/adapters/__init__.py +9 -2
- package/templates/api/src/boards/auth/factory.py +16 -2
- package/templates/api/src/boards/generators/__init__.py +2 -2
- package/templates/api/src/boards/generators/artifact_resolution.py +372 -0
- package/templates/api/src/boards/generators/artifacts.py +4 -4
- package/templates/api/src/boards/generators/base.py +8 -4
- package/templates/api/src/boards/generators/implementations/__init__.py +4 -2
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +25 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +173 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +221 -0
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +17 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +216 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +197 -0
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +191 -0
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +179 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +183 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +212 -0
- package/templates/api/src/boards/generators/implementations/fal/utils.py +61 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +13 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +168 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +167 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +180 -0
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{audio → openai/audio}/whisper.py +9 -6
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{image → openai/image}/dalle3.py +8 -5
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{image → replicate/image}/flux_pro.py +8 -5
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{video → replicate/video}/lipsync.py +9 -6
- package/templates/api/src/boards/generators/resolution.py +80 -20
- package/templates/api/src/boards/jobs/repository.py +49 -0
- package/templates/api/src/boards/storage/factory.py +16 -6
- package/templates/api/src/boards/workers/actors.py +69 -5
- package/templates/api/src/boards/workers/context.py +177 -21
- package/templates/web/package.json +2 -1
- package/templates/web/src/components/boards/GenerationInput.tsx +154 -52
- package/templates/web/src/components/boards/GeneratorSelector.tsx +57 -59
- package/templates/web/src/components/ui/dropdown-menu.tsx +200 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/video/__init__.py +0 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from uuid import UUID
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
6
|
|
|
7
7
|
from ..database.connection import get_async_session
|
|
8
8
|
from ..generators import resolution
|
|
@@ -24,12 +24,22 @@ class GeneratorExecutionContext:
|
|
|
24
24
|
storage_manager: StorageManager,
|
|
25
25
|
tenant_id: UUID,
|
|
26
26
|
board_id: UUID,
|
|
27
|
+
user_id: UUID,
|
|
28
|
+
generator_name: str,
|
|
29
|
+
artifact_type: str,
|
|
30
|
+
input_params: dict,
|
|
27
31
|
) -> None:
|
|
28
32
|
self.generation_id = str(generation_id)
|
|
29
33
|
self.publisher = publisher
|
|
30
34
|
self.storage_manager = storage_manager
|
|
31
35
|
self.tenant_id = str(tenant_id)
|
|
32
36
|
self.board_id = str(board_id)
|
|
37
|
+
self.user_id = str(user_id)
|
|
38
|
+
self.generator_name = generator_name
|
|
39
|
+
self.artifact_type = artifact_type
|
|
40
|
+
self.input_params = input_params
|
|
41
|
+
self._batch_id: str | None = None
|
|
42
|
+
self._batch_generations: list[str] = []
|
|
33
43
|
logger.info(
|
|
34
44
|
"Created execution context",
|
|
35
45
|
generation_id=str(generation_id),
|
|
@@ -52,15 +62,34 @@ class GeneratorExecutionContext:
|
|
|
52
62
|
self,
|
|
53
63
|
storage_url: str,
|
|
54
64
|
format: str,
|
|
55
|
-
width: int,
|
|
56
|
-
height: int,
|
|
65
|
+
width: int | None = None,
|
|
66
|
+
height: int | None = None,
|
|
67
|
+
output_index: int = 0,
|
|
57
68
|
) -> ImageArtifact:
|
|
58
|
-
"""Store image generation result.
|
|
59
|
-
|
|
69
|
+
"""Store image generation result.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
storage_url: URL to download the image from
|
|
73
|
+
format: Image format (png, jpg, etc.)
|
|
74
|
+
width: Image width in pixels (optional)
|
|
75
|
+
height: Image height in pixels (optional)
|
|
76
|
+
output_index: Index of this output in a batch (0 for primary, 1+ for additional)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ImageArtifact with the generation_id set appropriately
|
|
80
|
+
"""
|
|
81
|
+
logger.debug(
|
|
82
|
+
"Storing image result",
|
|
83
|
+
generation_id=self.generation_id,
|
|
84
|
+
output_index=output_index,
|
|
85
|
+
)
|
|
60
86
|
try:
|
|
87
|
+
# Determine which generation_id to use
|
|
88
|
+
target_generation_id = await self._get_or_create_generation_for_output(output_index)
|
|
89
|
+
|
|
61
90
|
result = await resolution.store_image_result(
|
|
62
91
|
storage_manager=self.storage_manager,
|
|
63
|
-
generation_id=
|
|
92
|
+
generation_id=target_generation_id,
|
|
64
93
|
tenant_id=self.tenant_id,
|
|
65
94
|
board_id=self.board_id,
|
|
66
95
|
storage_url=storage_url,
|
|
@@ -68,7 +97,11 @@ class GeneratorExecutionContext:
|
|
|
68
97
|
width=width,
|
|
69
98
|
height=height,
|
|
70
99
|
)
|
|
71
|
-
logger.info(
|
|
100
|
+
logger.info(
|
|
101
|
+
"Image result stored",
|
|
102
|
+
generation_id=target_generation_id,
|
|
103
|
+
output_index=output_index,
|
|
104
|
+
)
|
|
72
105
|
return result
|
|
73
106
|
except Exception as e:
|
|
74
107
|
logger.error("Failed to store image result", error=str(e))
|
|
@@ -78,17 +111,38 @@ class GeneratorExecutionContext:
|
|
|
78
111
|
self,
|
|
79
112
|
storage_url: str,
|
|
80
113
|
format: str,
|
|
81
|
-
width: int,
|
|
82
|
-
height: int,
|
|
114
|
+
width: int | None = None,
|
|
115
|
+
height: int | None = None,
|
|
83
116
|
duration: float | None = None,
|
|
84
117
|
fps: float | None = None,
|
|
118
|
+
output_index: int = 0,
|
|
85
119
|
) -> VideoArtifact:
|
|
86
|
-
"""Store video generation result.
|
|
87
|
-
|
|
120
|
+
"""Store video generation result.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
storage_url: URL to download the video from
|
|
124
|
+
format: Video format (mp4, webm, etc.)
|
|
125
|
+
width: Video width in pixels (optional)
|
|
126
|
+
height: Video height in pixels (optional)
|
|
127
|
+
duration: Video duration in seconds (optional)
|
|
128
|
+
fps: Frames per second (optional)
|
|
129
|
+
output_index: Index of this output in a batch (0 for primary, 1+ for additional)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
VideoArtifact with the generation_id set appropriately
|
|
133
|
+
"""
|
|
134
|
+
logger.debug(
|
|
135
|
+
"Storing video result",
|
|
136
|
+
generation_id=self.generation_id,
|
|
137
|
+
output_index=output_index,
|
|
138
|
+
)
|
|
88
139
|
try:
|
|
140
|
+
# Determine which generation_id to use
|
|
141
|
+
target_generation_id = await self._get_or_create_generation_for_output(output_index)
|
|
142
|
+
|
|
89
143
|
result = await resolution.store_video_result(
|
|
90
144
|
storage_manager=self.storage_manager,
|
|
91
|
-
generation_id=
|
|
145
|
+
generation_id=target_generation_id,
|
|
92
146
|
tenant_id=self.tenant_id,
|
|
93
147
|
board_id=self.board_id,
|
|
94
148
|
storage_url=storage_url,
|
|
@@ -98,7 +152,11 @@ class GeneratorExecutionContext:
|
|
|
98
152
|
duration=duration,
|
|
99
153
|
fps=fps,
|
|
100
154
|
)
|
|
101
|
-
logger.info(
|
|
155
|
+
logger.info(
|
|
156
|
+
"Video result stored",
|
|
157
|
+
generation_id=target_generation_id,
|
|
158
|
+
output_index=output_index,
|
|
159
|
+
)
|
|
102
160
|
return result
|
|
103
161
|
except Exception as e:
|
|
104
162
|
logger.error("Failed to store video result", error=str(e))
|
|
@@ -111,13 +169,33 @@ class GeneratorExecutionContext:
|
|
|
111
169
|
duration: float | None = None,
|
|
112
170
|
sample_rate: int | None = None,
|
|
113
171
|
channels: int | None = None,
|
|
172
|
+
output_index: int = 0,
|
|
114
173
|
) -> AudioArtifact:
|
|
115
|
-
"""Store audio generation result.
|
|
116
|
-
|
|
174
|
+
"""Store audio generation result.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
storage_url: URL to download the audio from
|
|
178
|
+
format: Audio format (mp3, wav, etc.)
|
|
179
|
+
duration: Audio duration in seconds (optional)
|
|
180
|
+
sample_rate: Sample rate in Hz (optional)
|
|
181
|
+
channels: Number of audio channels (optional)
|
|
182
|
+
output_index: Index of this output in a batch (0 for primary, 1+ for additional)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
AudioArtifact with the generation_id set appropriately
|
|
186
|
+
"""
|
|
187
|
+
logger.debug(
|
|
188
|
+
"Storing audio result",
|
|
189
|
+
generation_id=self.generation_id,
|
|
190
|
+
output_index=output_index,
|
|
191
|
+
)
|
|
117
192
|
try:
|
|
193
|
+
# Determine which generation_id to use
|
|
194
|
+
target_generation_id = await self._get_or_create_generation_for_output(output_index)
|
|
195
|
+
|
|
118
196
|
result = await resolution.store_audio_result(
|
|
119
197
|
storage_manager=self.storage_manager,
|
|
120
|
-
generation_id=
|
|
198
|
+
generation_id=target_generation_id,
|
|
121
199
|
tenant_id=self.tenant_id,
|
|
122
200
|
board_id=self.board_id,
|
|
123
201
|
storage_url=storage_url,
|
|
@@ -126,7 +204,11 @@ class GeneratorExecutionContext:
|
|
|
126
204
|
sample_rate=sample_rate,
|
|
127
205
|
channels=channels,
|
|
128
206
|
)
|
|
129
|
-
logger.info(
|
|
207
|
+
logger.info(
|
|
208
|
+
"Audio result stored",
|
|
209
|
+
generation_id=target_generation_id,
|
|
210
|
+
output_index=output_index,
|
|
211
|
+
)
|
|
130
212
|
return result
|
|
131
213
|
except Exception as e:
|
|
132
214
|
logger.error("Failed to store audio result", error=str(e))
|
|
@@ -136,19 +218,40 @@ class GeneratorExecutionContext:
|
|
|
136
218
|
self,
|
|
137
219
|
content: str,
|
|
138
220
|
format: str,
|
|
221
|
+
output_index: int = 0,
|
|
139
222
|
) -> TextArtifact:
|
|
140
|
-
"""Store text generation result.
|
|
141
|
-
|
|
223
|
+
"""Store text generation result.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
content: Text content to store
|
|
227
|
+
format: Text format (plain, markdown, html, etc.)
|
|
228
|
+
output_index: Index of this output in a batch (0 for primary, 1+ for additional)
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
TextArtifact with the generation_id set appropriately
|
|
232
|
+
"""
|
|
233
|
+
logger.debug(
|
|
234
|
+
"Storing text result",
|
|
235
|
+
generation_id=self.generation_id,
|
|
236
|
+
output_index=output_index,
|
|
237
|
+
)
|
|
142
238
|
try:
|
|
239
|
+
# Determine which generation_id to use
|
|
240
|
+
target_generation_id = await self._get_or_create_generation_for_output(output_index)
|
|
241
|
+
|
|
143
242
|
result = await resolution.store_text_result(
|
|
144
243
|
storage_manager=self.storage_manager,
|
|
145
|
-
generation_id=
|
|
244
|
+
generation_id=target_generation_id,
|
|
146
245
|
tenant_id=self.tenant_id,
|
|
147
246
|
board_id=self.board_id,
|
|
148
247
|
content=content,
|
|
149
248
|
format=format,
|
|
150
249
|
)
|
|
151
|
-
logger.info(
|
|
250
|
+
logger.info(
|
|
251
|
+
"Text result stored",
|
|
252
|
+
generation_id=target_generation_id,
|
|
253
|
+
output_index=output_index,
|
|
254
|
+
)
|
|
152
255
|
return result
|
|
153
256
|
except Exception as e:
|
|
154
257
|
logger.error("Failed to store text result", error=str(e))
|
|
@@ -186,3 +289,56 @@ class GeneratorExecutionContext:
|
|
|
186
289
|
)
|
|
187
290
|
async with get_async_session() as session:
|
|
188
291
|
await jobs_repo.set_external_job_id(session, self.generation_id, external_id)
|
|
292
|
+
|
|
293
|
+
async def _get_or_create_generation_for_output(self, output_index: int) -> str:
|
|
294
|
+
"""Get or create a generation record for the given output index.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
output_index: Index of the output (0 for primary, 1+ for batch outputs)
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
generation_id to use for storing this output
|
|
301
|
+
"""
|
|
302
|
+
# Index 0 is always the primary generation
|
|
303
|
+
if output_index == 0:
|
|
304
|
+
return self.generation_id
|
|
305
|
+
|
|
306
|
+
# For batch outputs, ensure we have a batch_id
|
|
307
|
+
if self._batch_id is None:
|
|
308
|
+
self._batch_id = str(uuid4())
|
|
309
|
+
logger.debug(
|
|
310
|
+
"Created batch_id for multi-output generation",
|
|
311
|
+
batch_id=self._batch_id,
|
|
312
|
+
primary_generation_id=self.generation_id,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Check if we've already created a generation for this index
|
|
316
|
+
batch_index = output_index - 1 # Adjust since index 0 is primary
|
|
317
|
+
if batch_index < len(self._batch_generations):
|
|
318
|
+
return self._batch_generations[batch_index]
|
|
319
|
+
|
|
320
|
+
# Create new batch generation record
|
|
321
|
+
async with get_async_session() as session:
|
|
322
|
+
batch_gen = await jobs_repo.create_batch_generation(
|
|
323
|
+
session,
|
|
324
|
+
tenant_id=UUID(self.tenant_id),
|
|
325
|
+
board_id=UUID(self.board_id),
|
|
326
|
+
user_id=UUID(self.user_id),
|
|
327
|
+
generator_name=self.generator_name,
|
|
328
|
+
artifact_type=self.artifact_type,
|
|
329
|
+
input_params=self.input_params,
|
|
330
|
+
batch_id=self._batch_id,
|
|
331
|
+
batch_index=output_index,
|
|
332
|
+
)
|
|
333
|
+
await session.commit()
|
|
334
|
+
batch_gen_id = str(batch_gen.id)
|
|
335
|
+
|
|
336
|
+
self._batch_generations.append(batch_gen_id)
|
|
337
|
+
logger.info(
|
|
338
|
+
"Created batch generation record",
|
|
339
|
+
batch_generation_id=batch_gen_id,
|
|
340
|
+
primary_generation_id=self.generation_id,
|
|
341
|
+
batch_id=self._batch_id,
|
|
342
|
+
batch_index=output_index,
|
|
343
|
+
)
|
|
344
|
+
return batch_gen_id
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
13
14
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
14
15
|
"@radix-ui/react-slot": "^1.2.3",
|
|
15
16
|
"@tailwindcss/postcss": "^4.1.13",
|
|
16
|
-
"@weirdfingers/boards": "^0.
|
|
17
|
+
"@weirdfingers/boards": "^0.4.0",
|
|
17
18
|
"class-variance-authority": "^0.7.1",
|
|
18
19
|
"clsx": "^2.0.0",
|
|
19
20
|
"graphql": "^16.11.0",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useState, useMemo, useEffect } from "react";
|
|
4
4
|
import { Settings, ArrowUp, X } from "lucide-react";
|
|
5
5
|
import Image from "next/image";
|
|
6
|
+
import {
|
|
7
|
+
ParsedGeneratorSchema,
|
|
8
|
+
parseGeneratorSchema,
|
|
9
|
+
} from "@weirdfingers/boards";
|
|
6
10
|
import { GeneratorSelector, GeneratorInfo } from "./GeneratorSelector";
|
|
7
11
|
import { ArtifactInputSlots } from "./ArtifactInputSlots";
|
|
8
12
|
|
|
@@ -13,12 +17,6 @@ interface Generation {
|
|
|
13
17
|
thumbnailUrl?: string | null;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
interface ArtifactSlot {
|
|
17
|
-
name: string;
|
|
18
|
-
type: string;
|
|
19
|
-
required: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
20
|
interface GenerationInputProps {
|
|
23
21
|
generators: GeneratorInfo[];
|
|
24
22
|
availableArtifacts: Generation[];
|
|
@@ -45,53 +43,42 @@ export function GenerationInput({
|
|
|
45
43
|
>(new Map());
|
|
46
44
|
const [attachedImage, setAttachedImage] = useState<Generation | null>(null);
|
|
47
45
|
const [showSettings, setShowSettings] = useState(false);
|
|
46
|
+
const [settings, setSettings] = useState<Record<string, unknown>>({});
|
|
48
47
|
|
|
49
|
-
// Parse input schema
|
|
50
|
-
const
|
|
51
|
-
if (!selectedGenerator)
|
|
52
|
-
|
|
53
|
-
const schema = selectedGenerator.inputSchema as {
|
|
54
|
-
properties?: Record<
|
|
55
|
-
string,
|
|
56
|
-
{
|
|
57
|
-
type?: string;
|
|
58
|
-
anyOf?: Array<{ $ref?: string }>;
|
|
59
|
-
description?: string;
|
|
60
|
-
}
|
|
61
|
-
>;
|
|
62
|
-
required?: string[];
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
if (!schema.properties) return [];
|
|
66
|
-
|
|
67
|
-
const slots: ArtifactSlot[] = [];
|
|
68
|
-
|
|
69
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
70
|
-
// Check if this property references an artifact type
|
|
71
|
-
const isArtifact =
|
|
72
|
-
value.anyOf?.some((ref) => ref.$ref?.includes("Artifact")) || false;
|
|
73
|
-
|
|
74
|
-
if (isArtifact) {
|
|
75
|
-
// Determine artifact type from the key or description
|
|
76
|
-
let type = "video";
|
|
77
|
-
if (key.toLowerCase().includes("audio")) type = "audio";
|
|
78
|
-
if (key.toLowerCase().includes("video")) type = "video";
|
|
79
|
-
if (key.toLowerCase().includes("image")) type = "image";
|
|
80
|
-
|
|
81
|
-
slots.push({
|
|
82
|
-
name: key,
|
|
83
|
-
type,
|
|
84
|
-
required: schema.required?.includes(key) || false,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
48
|
+
// Parse input schema using the toolkit's schema parser
|
|
49
|
+
const parsedSchema = useMemo((): ParsedGeneratorSchema => {
|
|
50
|
+
if (!selectedGenerator) {
|
|
51
|
+
return { artifactSlots: [], promptField: null, settingsFields: [] };
|
|
87
52
|
}
|
|
53
|
+
return parseGeneratorSchema(selectedGenerator.inputSchema);
|
|
54
|
+
}, [selectedGenerator]);
|
|
88
55
|
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
const artifactSlots = useMemo(() => {
|
|
57
|
+
return parsedSchema.artifactSlots.map((slot) => ({
|
|
58
|
+
name: slot.fieldName,
|
|
59
|
+
type: slot.artifactType,
|
|
60
|
+
required: slot.required,
|
|
61
|
+
}));
|
|
62
|
+
}, [parsedSchema.artifactSlots]);
|
|
91
63
|
|
|
92
|
-
const artifactSlots = getArtifactSlots();
|
|
93
64
|
const needsArtifactInputs = artifactSlots.length > 0;
|
|
94
65
|
|
|
66
|
+
// Initialize settings with defaults when generator changes
|
|
67
|
+
const defaultSettings = useMemo(() => {
|
|
68
|
+
const defaults: Record<string, unknown> = {};
|
|
69
|
+
parsedSchema.settingsFields.forEach((field) => {
|
|
70
|
+
if (field.default !== undefined) {
|
|
71
|
+
defaults[field.fieldName] = field.default;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return defaults;
|
|
75
|
+
}, [parsedSchema.settingsFields]);
|
|
76
|
+
|
|
77
|
+
// Reset settings when generator changes or defaultSettings change
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setSettings(defaultSettings);
|
|
80
|
+
}, [selectedGenerator, defaultSettings]);
|
|
81
|
+
|
|
95
82
|
const handleSubmit = () => {
|
|
96
83
|
if (!selectedGenerator || !prompt.trim()) return;
|
|
97
84
|
|
|
@@ -99,13 +86,14 @@ export function GenerationInput({
|
|
|
99
86
|
generatorName: selectedGenerator.name,
|
|
100
87
|
prompt: prompt.trim(),
|
|
101
88
|
artifacts: selectedArtifacts,
|
|
102
|
-
settings
|
|
89
|
+
settings,
|
|
103
90
|
});
|
|
104
91
|
|
|
105
92
|
// Reset form
|
|
106
93
|
setPrompt("");
|
|
107
94
|
setSelectedArtifacts(new Map());
|
|
108
95
|
setAttachedImage(null);
|
|
96
|
+
setSettings(defaultSettings);
|
|
109
97
|
};
|
|
110
98
|
|
|
111
99
|
const handleSelectArtifact = (
|
|
@@ -241,9 +229,123 @@ export function GenerationInput({
|
|
|
241
229
|
{/* Settings panel (collapsed by default) */}
|
|
242
230
|
{showSettings && (
|
|
243
231
|
<div className="px-4 py-4 border-t border-gray-200 bg-gray-50">
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
232
|
+
{parsedSchema.settingsFields.length === 0 ? (
|
|
233
|
+
<p className="text-sm text-gray-600">
|
|
234
|
+
No additional settings available for this generator
|
|
235
|
+
</p>
|
|
236
|
+
) : (
|
|
237
|
+
<div>
|
|
238
|
+
<h3 className="text-sm font-medium text-gray-900 mb-4">
|
|
239
|
+
Generator Settings
|
|
240
|
+
</h3>
|
|
241
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
242
|
+
{parsedSchema.settingsFields.map((field) => (
|
|
243
|
+
<div key={field.fieldName} className="space-y-1.5">
|
|
244
|
+
<label
|
|
245
|
+
htmlFor={field.fieldName}
|
|
246
|
+
className="block text-sm font-medium text-gray-700"
|
|
247
|
+
>
|
|
248
|
+
{field.title}
|
|
249
|
+
</label>
|
|
250
|
+
{field.description && (
|
|
251
|
+
<p className="text-xs text-gray-500">{field.description}</p>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Slider control */}
|
|
255
|
+
{field.type === "slider" && (
|
|
256
|
+
<div className="space-y-1">
|
|
257
|
+
<input
|
|
258
|
+
id={field.fieldName}
|
|
259
|
+
type="range"
|
|
260
|
+
min={field.min}
|
|
261
|
+
max={field.max}
|
|
262
|
+
step={field.step || (field.isInteger ? 1 : 0.01)}
|
|
263
|
+
value={
|
|
264
|
+
(settings[field.fieldName] as number) ?? field.default
|
|
265
|
+
}
|
|
266
|
+
onChange={(e) =>
|
|
267
|
+
setSettings({
|
|
268
|
+
...settings,
|
|
269
|
+
[field.fieldName]: field.isInteger
|
|
270
|
+
? parseInt(e.target.value)
|
|
271
|
+
: parseFloat(e.target.value),
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-orange-500"
|
|
275
|
+
/>
|
|
276
|
+
<div className="flex justify-between text-xs text-gray-600">
|
|
277
|
+
<span>{field.min}</span>
|
|
278
|
+
<span className="font-medium">
|
|
279
|
+
{String(settings[field.fieldName] ?? field.default ?? field.min)}
|
|
280
|
+
</span>
|
|
281
|
+
<span>{field.max}</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{/* Dropdown control */}
|
|
287
|
+
{field.type === "dropdown" && (
|
|
288
|
+
<select
|
|
289
|
+
id={field.fieldName}
|
|
290
|
+
value={(settings[field.fieldName] as string) ?? field.default}
|
|
291
|
+
onChange={(e) =>
|
|
292
|
+
setSettings({
|
|
293
|
+
...settings,
|
|
294
|
+
[field.fieldName]: e.target.value,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
className="block w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
298
|
+
>
|
|
299
|
+
{field.options.map((option) => (
|
|
300
|
+
<option key={option} value={option}>
|
|
301
|
+
{option}
|
|
302
|
+
</option>
|
|
303
|
+
))}
|
|
304
|
+
</select>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Number input control */}
|
|
308
|
+
{field.type === "number" && (
|
|
309
|
+
<input
|
|
310
|
+
id={field.fieldName}
|
|
311
|
+
type="number"
|
|
312
|
+
min={field.min}
|
|
313
|
+
max={field.max}
|
|
314
|
+
step={field.isInteger ? 1 : "any"}
|
|
315
|
+
value={(settings[field.fieldName] as number) ?? field.default}
|
|
316
|
+
onChange={(e) =>
|
|
317
|
+
setSettings({
|
|
318
|
+
...settings,
|
|
319
|
+
[field.fieldName]: field.isInteger
|
|
320
|
+
? parseInt(e.target.value)
|
|
321
|
+
: parseFloat(e.target.value),
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
className="block w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{/* Text input control */}
|
|
329
|
+
{field.type === "text" && (
|
|
330
|
+
<input
|
|
331
|
+
id={field.fieldName}
|
|
332
|
+
type="text"
|
|
333
|
+
value={(settings[field.fieldName] as string) ?? field.default ?? ""}
|
|
334
|
+
onChange={(e) =>
|
|
335
|
+
setSettings({
|
|
336
|
+
...settings,
|
|
337
|
+
[field.fieldName]: e.target.value,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
pattern={field.pattern}
|
|
341
|
+
className="block w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
))}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
247
349
|
</div>
|
|
248
350
|
)}
|
|
249
351
|
</div>
|