@weirdfingers/baseboards 0.6.2 → 0.7.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/dist/index.js +54 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +2 -0
- package/templates/api/.env.example +3 -0
- package/templates/api/config/generators.yaml +58 -0
- package/templates/api/pyproject.toml +1 -1
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/api/endpoints/storage.py +85 -4
- package/templates/api/src/boards/api/endpoints/uploads.py +1 -2
- package/templates/api/src/boards/database/connection.py +98 -58
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +176 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +195 -0
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +14 -0
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +219 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +208 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +216 -0
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +177 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +178 -0
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +155 -0
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +18 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +168 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +159 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +180 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +190 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +191 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +13 -6
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +212 -0
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +208 -0
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +11 -0
- package/templates/api/src/boards/generators/implementations/kie/base.py +316 -0
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +190 -0
- package/templates/api/src/boards/generators/implementations/kie/utils.py +98 -0
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +8 -0
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +161 -0
- package/templates/api/src/boards/graphql/resolvers/upload.py +1 -1
- package/templates/web/package.json +4 -1
- package/templates/web/src/app/boards/[boardId]/page.tsx +156 -24
- package/templates/web/src/app/globals.css +3 -0
- package/templates/web/src/app/layout.tsx +15 -5
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +9 -9
- package/templates/web/src/components/boards/ArtifactPreview.tsx +34 -18
- package/templates/web/src/components/boards/GenerationGrid.tsx +101 -7
- package/templates/web/src/components/boards/GenerationInput.tsx +21 -21
- package/templates/web/src/components/boards/GeneratorSelector.tsx +232 -30
- package/templates/web/src/components/boards/UploadArtifact.tsx +385 -75
- package/templates/web/src/components/header.tsx +3 -1
- package/templates/web/src/components/theme-provider.tsx +10 -0
- package/templates/web/src/components/theme-toggle.tsx +75 -0
- package/templates/web/src/components/ui/alert-dialog.tsx +157 -0
- package/templates/web/src/components/ui/toast.tsx +128 -0
- package/templates/web/src/components/ui/toaster.tsx +35 -0
- package/templates/web/src/components/ui/use-toast.ts +186 -0
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useParams } from "next/navigation";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useBoard,
|
|
7
|
+
useGenerators,
|
|
8
|
+
useGeneration,
|
|
9
|
+
GeneratorSelectionProvider,
|
|
10
|
+
} from "@weirdfingers/boards";
|
|
6
11
|
import { GenerationGrid } from "@/components/boards/GenerationGrid";
|
|
7
12
|
import { GenerationInput } from "@/components/boards/GenerationInput";
|
|
8
13
|
import { UploadArtifact } from "@/components/boards/UploadArtifact";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Pencil, Check, X } from "lucide-react";
|
|
9
16
|
|
|
10
17
|
export default function BoardPage() {
|
|
11
18
|
const params = useParams();
|
|
@@ -16,8 +23,16 @@ export default function BoardPage() {
|
|
|
16
23
|
loading: boardLoading,
|
|
17
24
|
error: boardError,
|
|
18
25
|
refresh: refreshBoard,
|
|
26
|
+
updateBoard,
|
|
19
27
|
} = useBoard(boardId);
|
|
20
28
|
|
|
29
|
+
// State for inline title editing
|
|
30
|
+
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
|
|
31
|
+
const [editedTitle, setEditedTitle] = React.useState("");
|
|
32
|
+
const [titleError, setTitleError] = React.useState<string | null>(null);
|
|
33
|
+
const [isUpdatingTitle, setIsUpdatingTitle] = React.useState(false);
|
|
34
|
+
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
21
36
|
// Fetch available generators
|
|
22
37
|
const {
|
|
23
38
|
generators,
|
|
@@ -26,13 +41,72 @@ export default function BoardPage() {
|
|
|
26
41
|
} = useGenerators();
|
|
27
42
|
|
|
28
43
|
// Use generation hook for submitting generations and real-time progress
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
const { submit, isGenerating, progress } = useGeneration();
|
|
45
|
+
|
|
46
|
+
// Auto-focus input when entering edit mode
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (isEditingTitle && titleInputRef.current) {
|
|
49
|
+
titleInputRef.current.focus();
|
|
50
|
+
titleInputRef.current.select();
|
|
51
|
+
}
|
|
52
|
+
}, [isEditingTitle]);
|
|
53
|
+
|
|
54
|
+
// Handlers for title editing
|
|
55
|
+
const handleEditTitle = () => {
|
|
56
|
+
if (board) {
|
|
57
|
+
setEditedTitle(board.title);
|
|
58
|
+
setTitleError(null);
|
|
59
|
+
setIsEditingTitle(true);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleCancelEdit = () => {
|
|
64
|
+
setIsEditingTitle(false);
|
|
65
|
+
setEditedTitle("");
|
|
66
|
+
setTitleError(null);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleSaveTitle = async () => {
|
|
70
|
+
const trimmedTitle = editedTitle.trim();
|
|
71
|
+
|
|
72
|
+
// Validation
|
|
73
|
+
if (!trimmedTitle) {
|
|
74
|
+
setTitleError("Title cannot be empty");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (trimmedTitle === board?.title) {
|
|
79
|
+
// No changes, just exit edit mode
|
|
80
|
+
handleCancelEdit();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setIsUpdatingTitle(true);
|
|
85
|
+
setTitleError(null);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await updateBoard({ title: trimmedTitle });
|
|
89
|
+
setIsEditingTitle(false);
|
|
90
|
+
setEditedTitle("");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Failed to update board title:", error);
|
|
93
|
+
setTitleError(
|
|
94
|
+
error instanceof Error ? error.message : "Failed to update title"
|
|
95
|
+
);
|
|
96
|
+
} finally {
|
|
97
|
+
setIsUpdatingTitle(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
102
|
+
if (e.key === "Enter") {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
handleSaveTitle();
|
|
105
|
+
} else if (e.key === "Escape") {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
handleCancelEdit();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
36
110
|
|
|
37
111
|
// Refresh board when a generation completes or fails
|
|
38
112
|
// MUST be before conditional returns to satisfy Rules of Hooks
|
|
@@ -84,11 +158,11 @@ export default function BoardPage() {
|
|
|
84
158
|
console.error("[BoardPage] Board error:", boardError);
|
|
85
159
|
return (
|
|
86
160
|
<div className="flex items-center justify-center min-h-screen">
|
|
87
|
-
<div className="bg-
|
|
88
|
-
<h2 className="text-
|
|
161
|
+
<div className="bg-destructive/10 border border-destructive/50 rounded-lg p-6 max-w-lg">
|
|
162
|
+
<h2 className="text-destructive text-xl font-semibold mb-2">
|
|
89
163
|
Error Loading Board
|
|
90
164
|
</h2>
|
|
91
|
-
<p className="text-
|
|
165
|
+
<p className="text-destructive/90">{boardError.message}</p>
|
|
92
166
|
</div>
|
|
93
167
|
</div>
|
|
94
168
|
);
|
|
@@ -97,11 +171,11 @@ export default function BoardPage() {
|
|
|
97
171
|
console.error("[BoardPage] Generators error:", generatorsError);
|
|
98
172
|
return (
|
|
99
173
|
<div className="flex items-center justify-center min-h-screen">
|
|
100
|
-
<div className="bg-
|
|
101
|
-
<h2 className="text-
|
|
174
|
+
<div className="bg-destructive/10 border border-destructive/50 rounded-lg p-6 max-w-lg">
|
|
175
|
+
<h2 className="text-destructive text-xl font-semibold mb-2">
|
|
102
176
|
Error Loading Generators
|
|
103
177
|
</h2>
|
|
104
|
-
<p className="text-
|
|
178
|
+
<p className="text-destructive/90">{generatorsError.message}</p>
|
|
105
179
|
</div>
|
|
106
180
|
</div>
|
|
107
181
|
);
|
|
@@ -111,7 +185,7 @@ export default function BoardPage() {
|
|
|
111
185
|
if (boardLoading || !board) {
|
|
112
186
|
return (
|
|
113
187
|
<div className="flex items-center justify-center min-h-screen">
|
|
114
|
-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-
|
|
188
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
115
189
|
</div>
|
|
116
190
|
);
|
|
117
191
|
}
|
|
@@ -167,14 +241,71 @@ export default function BoardPage() {
|
|
|
167
241
|
|
|
168
242
|
return (
|
|
169
243
|
<GeneratorSelectionProvider>
|
|
170
|
-
<main className="min-h-screen bg-
|
|
244
|
+
<main className="min-h-screen bg-muted/30">
|
|
171
245
|
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
|
172
246
|
{/* Header */}
|
|
173
247
|
<div className="mb-6 flex items-start justify-between">
|
|
174
|
-
<div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
<div className="flex-1">
|
|
249
|
+
{isEditingTitle ? (
|
|
250
|
+
<div className="space-y-2">
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
252
|
+
<input
|
|
253
|
+
ref={titleInputRef}
|
|
254
|
+
type="text"
|
|
255
|
+
value={editedTitle}
|
|
256
|
+
onChange={(e) => setEditedTitle(e.target.value)}
|
|
257
|
+
onKeyDown={handleTitleKeyDown}
|
|
258
|
+
disabled={isUpdatingTitle}
|
|
259
|
+
className="text-3xl font-bold text-foreground border-2 border-border rounded px-2 py-1 focus:outline-none focus:border-primary disabled:opacity-50 disabled:cursor-not-allowed flex-1"
|
|
260
|
+
placeholder="Enter board title"
|
|
261
|
+
aria-label="Edit board title"
|
|
262
|
+
aria-invalid={!!titleError}
|
|
263
|
+
/>
|
|
264
|
+
<Button
|
|
265
|
+
onClick={handleSaveTitle}
|
|
266
|
+
disabled={isUpdatingTitle}
|
|
267
|
+
size="icon"
|
|
268
|
+
variant="default"
|
|
269
|
+
aria-label="Save title"
|
|
270
|
+
>
|
|
271
|
+
<Check className="h-4 w-4" />
|
|
272
|
+
</Button>
|
|
273
|
+
<Button
|
|
274
|
+
onClick={handleCancelEdit}
|
|
275
|
+
disabled={isUpdatingTitle}
|
|
276
|
+
size="icon"
|
|
277
|
+
variant="outline"
|
|
278
|
+
aria-label="Cancel editing"
|
|
279
|
+
>
|
|
280
|
+
<X className="h-4 w-4" />
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
{titleError && (
|
|
284
|
+
<p className="text-sm text-destructive" role="alert">
|
|
285
|
+
{titleError}
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<div className="flex items-center gap-2">
|
|
291
|
+
<h1 className="text-3xl font-bold text-foreground">
|
|
292
|
+
{board.title}
|
|
293
|
+
</h1>
|
|
294
|
+
<Button
|
|
295
|
+
onClick={handleEditTitle}
|
|
296
|
+
size="icon"
|
|
297
|
+
variant="ghost"
|
|
298
|
+
className="h-8 w-8"
|
|
299
|
+
aria-label="Edit board title"
|
|
300
|
+
>
|
|
301
|
+
<Pencil className="h-4 w-4" />
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
{board.description && !isEditingTitle && (
|
|
306
|
+
<p className="text-muted-foreground mt-2">
|
|
307
|
+
{board.description}
|
|
308
|
+
</p>
|
|
178
309
|
)}
|
|
179
310
|
</div>
|
|
180
311
|
<UploadArtifact
|
|
@@ -192,18 +323,19 @@ export default function BoardPage() {
|
|
|
192
323
|
onGenerationClick={() => {
|
|
193
324
|
// TODO: Open generation detail modal
|
|
194
325
|
}}
|
|
326
|
+
onRemoveSuccess={refreshBoard}
|
|
195
327
|
/>
|
|
196
328
|
</div>
|
|
197
329
|
|
|
198
330
|
{/* Generation Input */}
|
|
199
331
|
<div id="generation-input" className="sticky bottom-6 z-10">
|
|
200
332
|
{generatorsLoading ? (
|
|
201
|
-
<div className="bg-
|
|
202
|
-
<p className="text-
|
|
333
|
+
<div className="bg-background rounded-lg shadow-lg p-6 text-center">
|
|
334
|
+
<p className="text-muted-foreground">Loading generators...</p>
|
|
203
335
|
</div>
|
|
204
336
|
) : generators.length === 0 ? (
|
|
205
|
-
<div className="bg-
|
|
206
|
-
<p className="text-
|
|
337
|
+
<div className="bg-background rounded-lg shadow-lg p-6 text-center">
|
|
338
|
+
<p className="text-muted-foreground">No generators available</p>
|
|
207
339
|
</div>
|
|
208
340
|
) : (
|
|
209
341
|
<GenerationInput
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
--color-accent: var(--accent);
|
|
24
24
|
--color-accent-foreground: var(--accent-foreground);
|
|
25
25
|
--color-destructive: var(--destructive);
|
|
26
|
+
--color-success: var(--success);
|
|
26
27
|
--color-border: var(--border);
|
|
27
28
|
--color-input: var(--input);
|
|
28
29
|
--color-ring: var(--ring);
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
--accent: oklch(0.97 0 0);
|
|
59
60
|
--accent-foreground: oklch(0.205 0 0);
|
|
60
61
|
--destructive: oklch(0.577 0.245 27.325);
|
|
62
|
+
--success: oklch(0.55 0.15 145);
|
|
61
63
|
--border: oklch(0.922 0 0);
|
|
62
64
|
--input: oklch(0.922 0 0);
|
|
63
65
|
--ring: oklch(0.708 0 0);
|
|
@@ -92,6 +94,7 @@
|
|
|
92
94
|
--accent: oklch(0.269 0 0);
|
|
93
95
|
--accent-foreground: oklch(0.985 0 0);
|
|
94
96
|
--destructive: oklch(0.704 0.191 22.216);
|
|
97
|
+
--success: oklch(0.70 0.15 145);
|
|
95
98
|
--border: oklch(1 0 0 / 10%);
|
|
96
99
|
--input: oklch(1 0 0 / 15%);
|
|
97
100
|
--ring: oklch(0.556 0 0);
|
|
@@ -2,6 +2,8 @@ import React from "react";
|
|
|
2
2
|
import "./globals.css";
|
|
3
3
|
import { Providers } from "./providers";
|
|
4
4
|
import { Header } from "@/components/header";
|
|
5
|
+
import { Toaster } from "@/components/ui/toaster";
|
|
6
|
+
import { ThemeProvider } from "@/components/theme-provider";
|
|
5
7
|
|
|
6
8
|
export default function RootLayout({
|
|
7
9
|
children,
|
|
@@ -9,12 +11,20 @@ export default function RootLayout({
|
|
|
9
11
|
children: React.ReactNode;
|
|
10
12
|
}) {
|
|
11
13
|
return (
|
|
12
|
-
<html lang="en">
|
|
14
|
+
<html lang="en" suppressHydrationWarning>
|
|
13
15
|
<body>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
<ThemeProvider
|
|
17
|
+
attribute="class"
|
|
18
|
+
defaultTheme="system"
|
|
19
|
+
enableSystem
|
|
20
|
+
disableTransitionOnChange
|
|
21
|
+
>
|
|
22
|
+
<Providers>
|
|
23
|
+
<Header />
|
|
24
|
+
{children}
|
|
25
|
+
<Toaster />
|
|
26
|
+
</Providers>
|
|
27
|
+
</ThemeProvider>
|
|
18
28
|
</body>
|
|
19
29
|
</html>
|
|
20
30
|
);
|
|
@@ -110,7 +110,7 @@ export function ArtifactInputSlots({
|
|
|
110
110
|
<div key={slot.name} className="relative">
|
|
111
111
|
{selectedArtifact ? (
|
|
112
112
|
// Show selected artifact
|
|
113
|
-
<div className="border-2 border-
|
|
113
|
+
<div className="border-2 border-primary rounded-lg p-4 bg-primary/5">
|
|
114
114
|
<div className="flex items-start gap-3">
|
|
115
115
|
<div className="flex-shrink-0">
|
|
116
116
|
{selectedArtifact.thumbnailUrl ||
|
|
@@ -127,7 +127,7 @@ export function ArtifactInputSlots({
|
|
|
127
127
|
height={64}
|
|
128
128
|
/>
|
|
129
129
|
) : (
|
|
130
|
-
<div className="w-16 h-16 bg-
|
|
130
|
+
<div className="w-16 h-16 bg-muted rounded flex items-center justify-center">
|
|
131
131
|
{getIcon(slot.type)}
|
|
132
132
|
</div>
|
|
133
133
|
)}
|
|
@@ -139,13 +139,13 @@ export function ArtifactInputSlots({
|
|
|
139
139
|
{slot.type} {selectedArtifact.id.substring(0, 7)}
|
|
140
140
|
</span>
|
|
141
141
|
</div>
|
|
142
|
-
<p className="text-xs text-
|
|
142
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
143
143
|
{slot.name.replace(/_/g, " ")}
|
|
144
144
|
</p>
|
|
145
145
|
</div>
|
|
146
146
|
<button
|
|
147
147
|
onClick={() => onSelectArtifact(slot.name, null)}
|
|
148
|
-
className="flex-shrink-0 p-1 hover:bg-
|
|
148
|
+
className="flex-shrink-0 p-1 hover:bg-primary/10 rounded"
|
|
149
149
|
>
|
|
150
150
|
<X className="w-4 h-4" />
|
|
151
151
|
</button>
|
|
@@ -156,8 +156,8 @@ export function ArtifactInputSlots({
|
|
|
156
156
|
<div
|
|
157
157
|
className={`border-2 border-dashed rounded-lg p-6 transition-all ${
|
|
158
158
|
dragOverSlot === slot.name
|
|
159
|
-
? "border-
|
|
160
|
-
: "border-
|
|
159
|
+
? "border-primary bg-primary/5"
|
|
160
|
+
: "border-border hover:border-border/80"
|
|
161
161
|
}`}
|
|
162
162
|
onDragOver={(e) => handleDragOver(e, slot.type, slot.name)}
|
|
163
163
|
onDragLeave={handleDragLeave}
|
|
@@ -165,7 +165,7 @@ export function ArtifactInputSlots({
|
|
|
165
165
|
>
|
|
166
166
|
<div className="flex flex-col items-center justify-center text-center">
|
|
167
167
|
<div className="mb-2">{getIcon(slot.type)}</div>
|
|
168
|
-
<p className="text-sm font-medium text-
|
|
168
|
+
<p className="text-sm font-medium text-foreground mb-1">
|
|
169
169
|
{dragOverSlot === slot.name
|
|
170
170
|
? `Drop ${slot.type} here`
|
|
171
171
|
: `Add a ${slot.type}`}
|
|
@@ -180,7 +180,7 @@ export function ArtifactInputSlots({
|
|
|
180
180
|
onSelectArtifact(slot.name, artifact);
|
|
181
181
|
}
|
|
182
182
|
}}
|
|
183
|
-
className="mt-2 px-3 py-1.5 text-sm border border-
|
|
183
|
+
className="mt-2 px-3 py-1.5 text-sm border border-border rounded bg-background"
|
|
184
184
|
>
|
|
185
185
|
<option value="">Select from board...</option>
|
|
186
186
|
{matchingArtifacts.map((artifact) => (
|
|
@@ -191,7 +191,7 @@ export function ArtifactInputSlots({
|
|
|
191
191
|
))}
|
|
192
192
|
</select>
|
|
193
193
|
) : (
|
|
194
|
-
<p className="text-xs text-
|
|
194
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
195
195
|
No {slot.type} artifacts in this board yet.
|
|
196
196
|
</p>
|
|
197
197
|
)}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Pause,
|
|
14
14
|
RotateCcw,
|
|
15
15
|
GitBranch,
|
|
16
|
+
Trash2,
|
|
16
17
|
} from "lucide-react";
|
|
17
18
|
import Image from "next/image";
|
|
18
19
|
import { useRouter } from "next/navigation";
|
|
@@ -35,6 +36,7 @@ interface ArtifactPreviewProps {
|
|
|
35
36
|
canAddToSlot?: boolean;
|
|
36
37
|
onDownload?: () => void;
|
|
37
38
|
onPreview?: () => void;
|
|
39
|
+
onDelete?: () => void;
|
|
38
40
|
artifactId?: string;
|
|
39
41
|
prompt?: string | null;
|
|
40
42
|
}
|
|
@@ -50,6 +52,7 @@ export function ArtifactPreview({
|
|
|
50
52
|
canAddToSlot = false,
|
|
51
53
|
onDownload,
|
|
52
54
|
onPreview,
|
|
55
|
+
onDelete,
|
|
53
56
|
artifactId,
|
|
54
57
|
prompt,
|
|
55
58
|
}: ArtifactPreviewProps) {
|
|
@@ -112,11 +115,11 @@ export function ArtifactPreview({
|
|
|
112
115
|
if (isFailed) {
|
|
113
116
|
return (
|
|
114
117
|
<div className="flex flex-col items-center justify-center h-full p-4 text-center">
|
|
115
|
-
<div className="text-
|
|
118
|
+
<div className="text-destructive mb-2">
|
|
116
119
|
{status === "CANCELLED" ? "Cancelled" : "Failed"}
|
|
117
120
|
</div>
|
|
118
121
|
{errorMessage && (
|
|
119
|
-
<p className="text-sm text-
|
|
122
|
+
<p className="text-sm text-muted-foreground">{errorMessage}</p>
|
|
120
123
|
)}
|
|
121
124
|
</div>
|
|
122
125
|
);
|
|
@@ -125,7 +128,7 @@ export function ArtifactPreview({
|
|
|
125
128
|
if (isLoading) {
|
|
126
129
|
return (
|
|
127
130
|
<div className="flex items-center justify-center h-full">
|
|
128
|
-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-
|
|
131
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
129
132
|
</div>
|
|
130
133
|
);
|
|
131
134
|
}
|
|
@@ -144,8 +147,8 @@ export function ArtifactPreview({
|
|
|
144
147
|
);
|
|
145
148
|
}
|
|
146
149
|
return (
|
|
147
|
-
<div className="flex items-center justify-center h-full bg-
|
|
148
|
-
<ImageIcon className="w-12 h-12 text-
|
|
150
|
+
<div className="flex items-center justify-center h-full bg-muted/50">
|
|
151
|
+
<ImageIcon className="w-12 h-12 text-muted-foreground" />
|
|
149
152
|
</div>
|
|
150
153
|
);
|
|
151
154
|
|
|
@@ -198,8 +201,8 @@ export function ArtifactPreview({
|
|
|
198
201
|
);
|
|
199
202
|
}
|
|
200
203
|
return (
|
|
201
|
-
<div className="flex items-center justify-center h-full bg-
|
|
202
|
-
<FileVideo className="w-12 h-12 text-
|
|
204
|
+
<div className="flex items-center justify-center h-full bg-muted/50">
|
|
205
|
+
<FileVideo className="w-12 h-12 text-muted-foreground" />
|
|
203
206
|
</div>
|
|
204
207
|
);
|
|
205
208
|
|
|
@@ -277,8 +280,8 @@ export function ArtifactPreview({
|
|
|
277
280
|
|
|
278
281
|
default:
|
|
279
282
|
return (
|
|
280
|
-
<div className="flex items-center justify-center h-full bg-
|
|
281
|
-
<span className="text-
|
|
283
|
+
<div className="flex items-center justify-center h-full bg-muted/50">
|
|
284
|
+
<span className="text-muted-foreground">Unknown type</span>
|
|
282
285
|
</div>
|
|
283
286
|
);
|
|
284
287
|
}
|
|
@@ -286,7 +289,7 @@ export function ArtifactPreview({
|
|
|
286
289
|
|
|
287
290
|
return (
|
|
288
291
|
<div
|
|
289
|
-
className="relative aspect-square rounded-lg overflow-hidden border border-
|
|
292
|
+
className="relative aspect-square rounded-lg overflow-hidden border border-border group"
|
|
290
293
|
draggable={isComplete && !!artifactId && canAddToSlot}
|
|
291
294
|
onDragStart={(e) => {
|
|
292
295
|
if (isComplete && artifactId) {
|
|
@@ -310,9 +313,8 @@ export function ArtifactPreview({
|
|
|
310
313
|
{renderContent()}
|
|
311
314
|
</div>
|
|
312
315
|
|
|
313
|
-
{/* Bottom overlay with controls - show for all artifacts
|
|
314
|
-
|
|
315
|
-
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
316
|
+
{/* Bottom overlay with controls - show for all artifacts */}
|
|
317
|
+
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
316
318
|
<div className="flex items-center justify-between gap-2">
|
|
317
319
|
{/* Drag handle - only for completed artifacts */}
|
|
318
320
|
{isComplete && (
|
|
@@ -353,7 +355,7 @@ export function ArtifactPreview({
|
|
|
353
355
|
)}
|
|
354
356
|
|
|
355
357
|
{/* More options menu - show for all artifacts */}
|
|
356
|
-
{(onPreview || onDownload || artifactId) && (
|
|
358
|
+
{(onPreview || onDownload || artifactId || onDelete) && (
|
|
357
359
|
<DropdownMenu>
|
|
358
360
|
<DropdownMenuTrigger asChild>
|
|
359
361
|
<button
|
|
@@ -377,10 +379,10 @@ export function ArtifactPreview({
|
|
|
377
379
|
View Lineage
|
|
378
380
|
</DropdownMenuItem>
|
|
379
381
|
)}
|
|
380
|
-
{artifactId && (onPreview || onDownload) && (
|
|
382
|
+
{artifactId && isComplete && (onPreview || onDownload) && (
|
|
381
383
|
<DropdownMenuSeparator />
|
|
382
384
|
)}
|
|
383
|
-
{onPreview && (
|
|
385
|
+
{isComplete && onPreview && (
|
|
384
386
|
<DropdownMenuItem
|
|
385
387
|
onClick={(e) => {
|
|
386
388
|
e.stopPropagation();
|
|
@@ -392,7 +394,7 @@ export function ArtifactPreview({
|
|
|
392
394
|
Preview
|
|
393
395
|
</DropdownMenuItem>
|
|
394
396
|
)}
|
|
395
|
-
{onDownload && (
|
|
397
|
+
{isComplete && onDownload && (
|
|
396
398
|
<DropdownMenuItem
|
|
397
399
|
onClick={(e) => {
|
|
398
400
|
e.stopPropagation();
|
|
@@ -404,13 +406,27 @@ export function ArtifactPreview({
|
|
|
404
406
|
Download
|
|
405
407
|
</DropdownMenuItem>
|
|
406
408
|
)}
|
|
409
|
+
{onDelete && (
|
|
410
|
+
<>
|
|
411
|
+
<DropdownMenuSeparator />
|
|
412
|
+
<DropdownMenuItem
|
|
413
|
+
onClick={(e) => {
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
onDelete();
|
|
416
|
+
}}
|
|
417
|
+
className="cursor-pointer text-destructive focus:text-destructive"
|
|
418
|
+
>
|
|
419
|
+
<Trash2 className="w-4 h-4 mr-2" />
|
|
420
|
+
Delete
|
|
421
|
+
</DropdownMenuItem>
|
|
422
|
+
</>
|
|
423
|
+
)}
|
|
407
424
|
</DropdownMenuContent>
|
|
408
425
|
</DropdownMenu>
|
|
409
426
|
)}
|
|
410
427
|
</div>
|
|
411
428
|
</div>
|
|
412
429
|
</div>
|
|
413
|
-
)}
|
|
414
430
|
</div>
|
|
415
431
|
);
|
|
416
432
|
}
|