@weirdfingers/baseboards 0.4.1 → 0.5.1
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/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/api/config/generators.yaml +9 -0
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/config.py +7 -7
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +2 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +179 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +183 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +172 -0
- package/templates/api/src/boards/jobs/repository.py +3 -3
- package/templates/api/src/boards/workers/context.py +7 -3
- package/templates/web/package.json +1 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +44 -64
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +67 -3
- package/templates/web/src/components/boards/ArtifactPreview.tsx +292 -20
- package/templates/web/src/components/boards/GenerationGrid.tsx +51 -11
- package/templates/web/src/components/boards/GenerationInput.tsx +26 -23
- package/templates/web/src/components/boards/GeneratorSelector.tsx +10 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Veo 3.1 image-to-video generator.
|
|
3
|
+
|
|
4
|
+
Converts static images into animated videos based on text prompts using
|
|
5
|
+
Google's Veo 3.1 technology via fal.ai.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/veo3.1/image-to-video model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/veo3.1/image-to-video
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....artifacts import ImageArtifact
|
|
17
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Veo31ImageToVideoInput(BaseModel):
|
|
21
|
+
"""Input schema for Veo 3.1 image-to-video generation.
|
|
22
|
+
|
|
23
|
+
Artifact fields (image) are automatically detected via type introspection
|
|
24
|
+
and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
prompt: str = Field(description="Text prompt describing the desired video content and motion")
|
|
28
|
+
image: ImageArtifact = Field(
|
|
29
|
+
description="Input image to animate. Should be 720p or higher in 16:9 or 9:16 aspect ratio"
|
|
30
|
+
)
|
|
31
|
+
aspect_ratio: Literal["9:16", "16:9"] = Field(
|
|
32
|
+
default="16:9",
|
|
33
|
+
description="Aspect ratio of the generated video",
|
|
34
|
+
)
|
|
35
|
+
duration: Literal["4s", "6s", "8s"] = Field(
|
|
36
|
+
default="8s",
|
|
37
|
+
description="Duration of the generated video in seconds",
|
|
38
|
+
)
|
|
39
|
+
generate_audio: bool = Field(
|
|
40
|
+
default=True,
|
|
41
|
+
description="Whether to generate audio for the video. Disabling uses 50% fewer credits",
|
|
42
|
+
)
|
|
43
|
+
resolution: Literal["720p", "1080p"] = Field(
|
|
44
|
+
default="720p",
|
|
45
|
+
description="Resolution of the generated video",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FalVeo31ImageToVideoGenerator(BaseGenerator):
|
|
50
|
+
"""Generator for creating videos from static images using Google Veo 3.1."""
|
|
51
|
+
|
|
52
|
+
name = "fal-veo31-image-to-video"
|
|
53
|
+
description = "Fal: Veo 3.1 - Convert images to videos with text-guided animation"
|
|
54
|
+
artifact_type = "video"
|
|
55
|
+
|
|
56
|
+
def get_input_schema(self) -> type[Veo31ImageToVideoInput]:
|
|
57
|
+
"""Return the input schema for this generator."""
|
|
58
|
+
return Veo31ImageToVideoInput
|
|
59
|
+
|
|
60
|
+
async def generate(
|
|
61
|
+
self, inputs: Veo31ImageToVideoInput, context: GeneratorExecutionContext
|
|
62
|
+
) -> GeneratorResult:
|
|
63
|
+
"""Generate video using fal.ai veo3.1/image-to-video."""
|
|
64
|
+
# Check for API key
|
|
65
|
+
if not os.getenv("FAL_KEY"):
|
|
66
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
67
|
+
|
|
68
|
+
# Import fal_client
|
|
69
|
+
try:
|
|
70
|
+
import fal_client
|
|
71
|
+
except ImportError as e:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
"fal.ai SDK is required for FalVeo31ImageToVideoGenerator. "
|
|
74
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
75
|
+
) from e
|
|
76
|
+
|
|
77
|
+
# Upload image artifact to Fal's public storage
|
|
78
|
+
# Fal API requires publicly accessible URLs, but our storage_url might be:
|
|
79
|
+
# - Localhost URLs (not publicly accessible)
|
|
80
|
+
# - Private S3 buckets (not publicly accessible)
|
|
81
|
+
# So we upload to Fal's temporary storage first
|
|
82
|
+
from ..utils import upload_artifacts_to_fal
|
|
83
|
+
|
|
84
|
+
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
85
|
+
|
|
86
|
+
# Prepare arguments for fal.ai API
|
|
87
|
+
arguments = {
|
|
88
|
+
"prompt": inputs.prompt,
|
|
89
|
+
"image_url": image_urls[0],
|
|
90
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
91
|
+
"duration": inputs.duration,
|
|
92
|
+
"generate_audio": inputs.generate_audio,
|
|
93
|
+
"resolution": inputs.resolution,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Submit async job
|
|
97
|
+
handler = await fal_client.submit_async(
|
|
98
|
+
"fal-ai/veo3.1/image-to-video",
|
|
99
|
+
arguments=arguments,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Store external job ID
|
|
103
|
+
await context.set_external_job_id(handler.request_id)
|
|
104
|
+
|
|
105
|
+
# Stream progress updates
|
|
106
|
+
from .....progress.models import ProgressUpdate
|
|
107
|
+
|
|
108
|
+
event_count = 0
|
|
109
|
+
async for event in handler.iter_events(with_logs=True):
|
|
110
|
+
event_count += 1
|
|
111
|
+
# Sample every 3rd event to avoid spam
|
|
112
|
+
if event_count % 3 == 0:
|
|
113
|
+
# Extract logs if available
|
|
114
|
+
logs = getattr(event, "logs", None)
|
|
115
|
+
if logs:
|
|
116
|
+
# Join log entries into a single message
|
|
117
|
+
if isinstance(logs, list):
|
|
118
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
119
|
+
else:
|
|
120
|
+
message = str(logs)
|
|
121
|
+
|
|
122
|
+
if message:
|
|
123
|
+
await context.publish_progress(
|
|
124
|
+
ProgressUpdate(
|
|
125
|
+
job_id=handler.request_id,
|
|
126
|
+
status="processing",
|
|
127
|
+
progress=50.0,
|
|
128
|
+
phase="processing",
|
|
129
|
+
message=message,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Get final result
|
|
134
|
+
result = await handler.get()
|
|
135
|
+
|
|
136
|
+
# Extract video from result
|
|
137
|
+
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
138
|
+
video_data = result.get("video")
|
|
139
|
+
if not video_data:
|
|
140
|
+
raise ValueError("No video returned from fal.ai API")
|
|
141
|
+
|
|
142
|
+
video_url = video_data.get("url")
|
|
143
|
+
if not video_url:
|
|
144
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
145
|
+
|
|
146
|
+
# Calculate video dimensions based on resolution and aspect ratio
|
|
147
|
+
if inputs.resolution == "720p":
|
|
148
|
+
if inputs.aspect_ratio == "16:9":
|
|
149
|
+
width, height = 1280, 720
|
|
150
|
+
else: # 9:16
|
|
151
|
+
width, height = 720, 1280
|
|
152
|
+
else: # 1080p
|
|
153
|
+
if inputs.aspect_ratio == "16:9":
|
|
154
|
+
width, height = 1920, 1080
|
|
155
|
+
else: # 9:16
|
|
156
|
+
width, height = 1080, 1920
|
|
157
|
+
|
|
158
|
+
# Parse duration from "Xs" format
|
|
159
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
160
|
+
|
|
161
|
+
artifact = await context.store_video_result(
|
|
162
|
+
storage_url=video_url,
|
|
163
|
+
format="mp4",
|
|
164
|
+
width=width,
|
|
165
|
+
height=height,
|
|
166
|
+
duration=duration_seconds,
|
|
167
|
+
output_index=0,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return GeneratorResult(outputs=[artifact])
|
|
171
|
+
|
|
172
|
+
async def estimate_cost(self, inputs: Veo31ImageToVideoInput) -> float:
|
|
173
|
+
"""Estimate cost for this generation in USD.
|
|
174
|
+
|
|
175
|
+
Note: Pricing information not available in Fal documentation.
|
|
176
|
+
Using placeholder value that should be updated with actual pricing.
|
|
177
|
+
"""
|
|
178
|
+
# TODO: Update with actual pricing from Fal when available
|
|
179
|
+
# Base cost, with 50% reduction if audio is disabled
|
|
180
|
+
base_cost = 0.15 # Placeholder estimate
|
|
181
|
+
if not inputs.generate_audio:
|
|
182
|
+
return base_cost * 0.5
|
|
183
|
+
return base_cost
|
package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Veo 3.1 reference-to-video generator.
|
|
3
|
+
|
|
4
|
+
Generates videos from multiple reference images to maintain consistent subject
|
|
5
|
+
appearance while creating dynamic video content based on text prompts.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/veo3.1/reference-to-video model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/veo3.1/reference-to-video
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....artifacts import ImageArtifact
|
|
17
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Veo31ReferenceToVideoInput(BaseModel):
|
|
21
|
+
"""Input schema for Veo 3.1 reference-to-video generation.
|
|
22
|
+
|
|
23
|
+
Artifact fields (image_urls) are automatically detected via type
|
|
24
|
+
introspection and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
image_urls: list[ImageArtifact] = Field(
|
|
28
|
+
description="URLs of reference images for consistent subject appearance"
|
|
29
|
+
)
|
|
30
|
+
prompt: str = Field(description="Text description of desired video content")
|
|
31
|
+
duration: Literal["8s"] = Field(
|
|
32
|
+
default="8s",
|
|
33
|
+
description="Duration of the generated video in seconds (currently only 8s is supported)",
|
|
34
|
+
)
|
|
35
|
+
resolution: Literal["720p", "1080p"] = Field(
|
|
36
|
+
default="720p",
|
|
37
|
+
description="Resolution of the generated video",
|
|
38
|
+
)
|
|
39
|
+
generate_audio: bool = Field(
|
|
40
|
+
default=True,
|
|
41
|
+
description="Whether to generate audio for the video. Disabling uses 50% fewer credits",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FalVeo31ReferenceToVideoGenerator(BaseGenerator):
|
|
46
|
+
"""Generator for creating videos from reference images using Google Veo 3.1."""
|
|
47
|
+
|
|
48
|
+
name = "fal-veo31-reference-to-video"
|
|
49
|
+
description = "Fal: Veo 3.1 - Generate videos from reference images with consistent subjects"
|
|
50
|
+
artifact_type = "video"
|
|
51
|
+
|
|
52
|
+
def get_input_schema(self) -> type[Veo31ReferenceToVideoInput]:
|
|
53
|
+
"""Return the input schema for this generator."""
|
|
54
|
+
return Veo31ReferenceToVideoInput
|
|
55
|
+
|
|
56
|
+
async def generate(
|
|
57
|
+
self, inputs: Veo31ReferenceToVideoInput, context: GeneratorExecutionContext
|
|
58
|
+
) -> GeneratorResult:
|
|
59
|
+
"""Generate video using fal.ai veo3.1/reference-to-video."""
|
|
60
|
+
# Check for API key
|
|
61
|
+
if not os.getenv("FAL_KEY"):
|
|
62
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
63
|
+
|
|
64
|
+
# Import fal_client
|
|
65
|
+
try:
|
|
66
|
+
import fal_client
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"fal.ai SDK is required for FalVeo31ReferenceToVideoGenerator. "
|
|
70
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
# Upload image artifacts to Fal's public storage
|
|
74
|
+
# Fal API requires publicly accessible URLs, but our storage_url might be:
|
|
75
|
+
# - Localhost URLs (not publicly accessible)
|
|
76
|
+
# - Private S3 buckets (not publicly accessible)
|
|
77
|
+
# So we upload to Fal's temporary storage first
|
|
78
|
+
from ..utils import upload_artifacts_to_fal
|
|
79
|
+
|
|
80
|
+
reference_image_urls = await upload_artifacts_to_fal(inputs.image_urls, context)
|
|
81
|
+
|
|
82
|
+
# Prepare arguments for fal.ai API
|
|
83
|
+
arguments = {
|
|
84
|
+
"image_urls": reference_image_urls,
|
|
85
|
+
"prompt": inputs.prompt,
|
|
86
|
+
"duration": inputs.duration,
|
|
87
|
+
"resolution": inputs.resolution,
|
|
88
|
+
"generate_audio": inputs.generate_audio,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Submit async job
|
|
92
|
+
handler = await fal_client.submit_async(
|
|
93
|
+
"fal-ai/veo3.1/reference-to-video",
|
|
94
|
+
arguments=arguments,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Store external job ID
|
|
98
|
+
await context.set_external_job_id(handler.request_id)
|
|
99
|
+
|
|
100
|
+
# Stream progress updates
|
|
101
|
+
from .....progress.models import ProgressUpdate
|
|
102
|
+
|
|
103
|
+
event_count = 0
|
|
104
|
+
async for event in handler.iter_events(with_logs=True):
|
|
105
|
+
event_count += 1
|
|
106
|
+
# Sample every 3rd event to avoid spam
|
|
107
|
+
if event_count % 3 == 0:
|
|
108
|
+
# Extract logs if available
|
|
109
|
+
logs = getattr(event, "logs", None)
|
|
110
|
+
if logs:
|
|
111
|
+
# Join log entries into a single message
|
|
112
|
+
if isinstance(logs, list):
|
|
113
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
114
|
+
else:
|
|
115
|
+
message = str(logs)
|
|
116
|
+
|
|
117
|
+
if message:
|
|
118
|
+
await context.publish_progress(
|
|
119
|
+
ProgressUpdate(
|
|
120
|
+
job_id=handler.request_id,
|
|
121
|
+
status="processing",
|
|
122
|
+
progress=50.0,
|
|
123
|
+
phase="processing",
|
|
124
|
+
message=message,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Get final result
|
|
129
|
+
result = await handler.get()
|
|
130
|
+
|
|
131
|
+
# Extract video from result
|
|
132
|
+
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
133
|
+
video_data = result.get("video")
|
|
134
|
+
if not video_data:
|
|
135
|
+
raise ValueError("No video returned from fal.ai API")
|
|
136
|
+
|
|
137
|
+
video_url = video_data.get("url")
|
|
138
|
+
if not video_url:
|
|
139
|
+
raise ValueError("Video missing URL in fal.ai response")
|
|
140
|
+
|
|
141
|
+
# Store video result
|
|
142
|
+
# Note: Fal API doesn't provide video dimensions/duration in the response,
|
|
143
|
+
# so we'll use defaults based on input parameters
|
|
144
|
+
width = 1280 if inputs.resolution == "720p" else 1920
|
|
145
|
+
height = 720 if inputs.resolution == "720p" else 1080
|
|
146
|
+
|
|
147
|
+
# Parse duration from "8s" format
|
|
148
|
+
duration_seconds = int(inputs.duration.rstrip("s"))
|
|
149
|
+
|
|
150
|
+
artifact = await context.store_video_result(
|
|
151
|
+
storage_url=video_url,
|
|
152
|
+
format="mp4",
|
|
153
|
+
width=width,
|
|
154
|
+
height=height,
|
|
155
|
+
duration=duration_seconds,
|
|
156
|
+
output_index=0,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return GeneratorResult(outputs=[artifact])
|
|
160
|
+
|
|
161
|
+
async def estimate_cost(self, inputs: Veo31ReferenceToVideoInput) -> float:
|
|
162
|
+
"""Estimate cost for this generation in USD.
|
|
163
|
+
|
|
164
|
+
Note: Pricing information not available in Fal documentation.
|
|
165
|
+
Using placeholder value that should be updated with actual pricing.
|
|
166
|
+
"""
|
|
167
|
+
# TODO: Update with actual pricing from Fal when available
|
|
168
|
+
# Base cost, with 50% reduction if audio is disabled
|
|
169
|
+
base_cost = 0.15 # Placeholder estimate
|
|
170
|
+
if not inputs.generate_audio:
|
|
171
|
+
return base_cost * 0.5
|
|
172
|
+
return base_cost
|
|
@@ -115,7 +115,7 @@ async def create_batch_generation(
|
|
|
115
115
|
input_params: dict,
|
|
116
116
|
batch_id: str,
|
|
117
117
|
batch_index: int,
|
|
118
|
-
) ->
|
|
118
|
+
) -> str:
|
|
119
119
|
"""Create a batch generation record for multi-output generators.
|
|
120
120
|
|
|
121
121
|
This creates a new generation record that is part of a batch, with
|
|
@@ -133,7 +133,7 @@ async def create_batch_generation(
|
|
|
133
133
|
batch_index: Index of this output in the batch
|
|
134
134
|
|
|
135
135
|
Returns:
|
|
136
|
-
|
|
136
|
+
ID of the created generation record
|
|
137
137
|
"""
|
|
138
138
|
gen = Generations()
|
|
139
139
|
gen.tenant_id = tenant_id
|
|
@@ -150,4 +150,4 @@ async def create_batch_generation(
|
|
|
150
150
|
}
|
|
151
151
|
session.add(gen)
|
|
152
152
|
await session.flush()
|
|
153
|
-
return gen
|
|
153
|
+
return str(gen.id)
|
|
@@ -6,7 +6,12 @@ from uuid import UUID, uuid4
|
|
|
6
6
|
|
|
7
7
|
from ..database.connection import get_async_session
|
|
8
8
|
from ..generators import resolution
|
|
9
|
-
from ..generators.artifacts import
|
|
9
|
+
from ..generators.artifacts import (
|
|
10
|
+
AudioArtifact,
|
|
11
|
+
ImageArtifact,
|
|
12
|
+
TextArtifact,
|
|
13
|
+
VideoArtifact,
|
|
14
|
+
)
|
|
10
15
|
from ..jobs import repository as jobs_repo
|
|
11
16
|
from ..logging import get_logger
|
|
12
17
|
from ..progress.models import ProgressUpdate
|
|
@@ -319,7 +324,7 @@ class GeneratorExecutionContext:
|
|
|
319
324
|
|
|
320
325
|
# Create new batch generation record
|
|
321
326
|
async with get_async_session() as session:
|
|
322
|
-
|
|
327
|
+
batch_gen_id = await jobs_repo.create_batch_generation(
|
|
323
328
|
session,
|
|
324
329
|
tenant_id=UUID(self.tenant_id),
|
|
325
330
|
board_id=UUID(self.board_id),
|
|
@@ -331,7 +336,6 @@ class GeneratorExecutionContext:
|
|
|
331
336
|
batch_index=output_index,
|
|
332
337
|
)
|
|
333
338
|
await session.commit()
|
|
334
|
-
batch_gen_id = str(batch_gen.id)
|
|
335
339
|
|
|
336
340
|
self._batch_generations.append(batch_gen_id)
|
|
337
341
|
logger.info(
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
15
15
|
"@radix-ui/react-slot": "^1.2.3",
|
|
16
16
|
"@tailwindcss/postcss": "^4.1.13",
|
|
17
|
-
"@weirdfingers/boards": "^0.
|
|
17
|
+
"@weirdfingers/boards": "^0.5.1",
|
|
18
18
|
"class-variance-authority": "^0.7.1",
|
|
19
19
|
"clsx": "^2.0.0",
|
|
20
20
|
"graphql": "^16.11.0",
|
|
@@ -2,32 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useParams } from "next/navigation";
|
|
5
|
-
import { useBoard, useGenerators, useGeneration } from "@weirdfingers/boards";
|
|
5
|
+
import { useBoard, useGenerators, useGeneration, GeneratorSelectionProvider } from "@weirdfingers/boards";
|
|
6
6
|
import { GenerationGrid } from "@/components/boards/GenerationGrid";
|
|
7
7
|
import { GenerationInput } from "@/components/boards/GenerationInput";
|
|
8
8
|
|
|
9
9
|
export default function BoardPage() {
|
|
10
10
|
const params = useParams();
|
|
11
11
|
const boardId = params.boardId as string;
|
|
12
|
-
console.log("[BoardPage] Rendering with boardId:", boardId);
|
|
13
12
|
|
|
14
|
-
const boardHookResult = useBoard(boardId);
|
|
15
|
-
console.log("[BoardPage] useBoard result:", boardHookResult);
|
|
16
13
|
const {
|
|
17
14
|
board,
|
|
18
15
|
loading: boardLoading,
|
|
19
16
|
error: boardError,
|
|
20
17
|
refresh: refreshBoard,
|
|
21
|
-
} =
|
|
18
|
+
} = useBoard(boardId);
|
|
22
19
|
|
|
23
20
|
// Fetch available generators
|
|
24
|
-
const generatorsHookResult = useGenerators();
|
|
25
|
-
console.log("[BoardPage] useGenerators result:", generatorsHookResult);
|
|
26
21
|
const {
|
|
27
22
|
generators,
|
|
28
23
|
loading: generatorsLoading,
|
|
29
24
|
error: generatorsError,
|
|
30
|
-
} =
|
|
25
|
+
} = useGenerators();
|
|
31
26
|
|
|
32
27
|
// Use generation hook for submitting generations and real-time progress
|
|
33
28
|
const {
|
|
@@ -38,10 +33,6 @@ export default function BoardPage() {
|
|
|
38
33
|
result,
|
|
39
34
|
} = useGeneration();
|
|
40
35
|
|
|
41
|
-
console.log("[BoardPage] board:", board);
|
|
42
|
-
console.log("[BoardPage] boardError:", boardError);
|
|
43
|
-
console.log("[BoardPage] board is null/undefined?", !board);
|
|
44
|
-
|
|
45
36
|
// Refresh board when a generation completes or fails
|
|
46
37
|
// MUST be before conditional returns to satisfy Rules of Hooks
|
|
47
38
|
React.useEffect(() => {
|
|
@@ -49,10 +40,6 @@ export default function BoardPage() {
|
|
|
49
40
|
progress &&
|
|
50
41
|
(progress.status === "completed" || progress.status === "failed")
|
|
51
42
|
) {
|
|
52
|
-
console.log(
|
|
53
|
-
"[BoardPage] Generation finished, refreshing board:",
|
|
54
|
-
progress.status
|
|
55
|
-
);
|
|
56
43
|
refreshBoard();
|
|
57
44
|
}
|
|
58
45
|
}, [progress, refreshBoard]);
|
|
@@ -121,12 +108,6 @@ export default function BoardPage() {
|
|
|
121
108
|
|
|
122
109
|
// Handle loading state
|
|
123
110
|
if (boardLoading || !board) {
|
|
124
|
-
console.log(
|
|
125
|
-
"[BoardPage] Showing loading spinner - boardLoading:",
|
|
126
|
-
boardLoading,
|
|
127
|
-
"board:",
|
|
128
|
-
board
|
|
129
|
-
);
|
|
130
111
|
return (
|
|
131
112
|
<div className="flex items-center justify-center min-h-screen">
|
|
132
113
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
|
@@ -134,8 +115,6 @@ export default function BoardPage() {
|
|
|
134
115
|
);
|
|
135
116
|
}
|
|
136
117
|
|
|
137
|
-
console.log("[BoardPage] Board loaded successfully:", board);
|
|
138
|
-
|
|
139
118
|
// Filter completed generations that can be used as inputs
|
|
140
119
|
const availableArtifacts = generations.filter(
|
|
141
120
|
(gen) => gen.status === "COMPLETED" && gen.storageUrl
|
|
@@ -186,47 +165,48 @@ export default function BoardPage() {
|
|
|
186
165
|
};
|
|
187
166
|
|
|
188
167
|
return (
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
{/* Generation Input */}
|
|
211
|
-
<div className="sticky bottom-6 z-10">
|
|
212
|
-
{generatorsLoading ? (
|
|
213
|
-
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
214
|
-
<p className="text-gray-500">Loading generators...</p>
|
|
215
|
-
</div>
|
|
216
|
-
) : generators.length === 0 ? (
|
|
217
|
-
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
218
|
-
<p className="text-gray-500">No generators available</p>
|
|
219
|
-
</div>
|
|
220
|
-
) : (
|
|
221
|
-
<GenerationInput
|
|
222
|
-
generators={generators}
|
|
223
|
-
availableArtifacts={availableArtifacts}
|
|
224
|
-
onSubmit={handleGenerationSubmit}
|
|
225
|
-
isGenerating={isGenerating}
|
|
168
|
+
<GeneratorSelectionProvider>
|
|
169
|
+
<main className="min-h-screen bg-gray-50">
|
|
170
|
+
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
|
171
|
+
{/* Header */}
|
|
172
|
+
<div className="mb-6">
|
|
173
|
+
<h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
|
|
174
|
+
{board.description && (
|
|
175
|
+
<p className="text-gray-600 mt-2">{board.description}</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Generation Grid */}
|
|
180
|
+
<div className="mb-8">
|
|
181
|
+
<GenerationGrid
|
|
182
|
+
generations={generations}
|
|
183
|
+
onGenerationClick={() => {
|
|
184
|
+
// TODO: Open generation detail modal
|
|
185
|
+
}}
|
|
226
186
|
/>
|
|
227
|
-
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Generation Input */}
|
|
190
|
+
<div id="generation-input" className="sticky bottom-6 z-10">
|
|
191
|
+
{generatorsLoading ? (
|
|
192
|
+
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
193
|
+
<p className="text-gray-500">Loading generators...</p>
|
|
194
|
+
</div>
|
|
195
|
+
) : generators.length === 0 ? (
|
|
196
|
+
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
197
|
+
<p className="text-gray-500">No generators available</p>
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<GenerationInput
|
|
201
|
+
generators={generators}
|
|
202
|
+
availableArtifacts={availableArtifacts}
|
|
203
|
+
onSubmit={handleGenerationSubmit}
|
|
204
|
+
isGenerating={isGenerating}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
228
208
|
</div>
|
|
229
|
-
</
|
|
230
|
-
</
|
|
209
|
+
</main>
|
|
210
|
+
</GeneratorSelectionProvider>
|
|
231
211
|
);
|
|
232
212
|
}
|