@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.
Files changed (57) hide show
  1. package/dist/index.js +54 -28
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/README.md +2 -0
  5. package/templates/api/.env.example +3 -0
  6. package/templates/api/config/generators.yaml +58 -0
  7. package/templates/api/pyproject.toml +1 -1
  8. package/templates/api/src/boards/__init__.py +1 -1
  9. package/templates/api/src/boards/api/endpoints/storage.py +85 -4
  10. package/templates/api/src/boards/api/endpoints/uploads.py +1 -2
  11. package/templates/api/src/boards/database/connection.py +98 -58
  12. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
  13. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +176 -0
  14. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +195 -0
  15. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +14 -0
  16. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +219 -0
  17. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +208 -0
  18. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +216 -0
  19. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +177 -0
  20. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +178 -0
  21. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +155 -0
  22. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +180 -0
  23. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +18 -0
  24. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +168 -0
  25. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +159 -0
  26. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +180 -0
  27. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +190 -0
  28. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +190 -0
  29. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +191 -0
  30. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +13 -6
  31. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +212 -0
  32. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +208 -0
  33. package/templates/api/src/boards/generators/implementations/kie/__init__.py +11 -0
  34. package/templates/api/src/boards/generators/implementations/kie/base.py +316 -0
  35. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +3 -0
  36. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +190 -0
  37. package/templates/api/src/boards/generators/implementations/kie/utils.py +98 -0
  38. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +8 -0
  39. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +161 -0
  40. package/templates/api/src/boards/graphql/resolvers/upload.py +1 -1
  41. package/templates/web/package.json +4 -1
  42. package/templates/web/src/app/boards/[boardId]/page.tsx +156 -24
  43. package/templates/web/src/app/globals.css +3 -0
  44. package/templates/web/src/app/layout.tsx +15 -5
  45. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +9 -9
  46. package/templates/web/src/components/boards/ArtifactPreview.tsx +34 -18
  47. package/templates/web/src/components/boards/GenerationGrid.tsx +101 -7
  48. package/templates/web/src/components/boards/GenerationInput.tsx +21 -21
  49. package/templates/web/src/components/boards/GeneratorSelector.tsx +232 -30
  50. package/templates/web/src/components/boards/UploadArtifact.tsx +385 -75
  51. package/templates/web/src/components/header.tsx +3 -1
  52. package/templates/web/src/components/theme-provider.tsx +10 -0
  53. package/templates/web/src/components/theme-toggle.tsx +75 -0
  54. package/templates/web/src/components/ui/alert-dialog.tsx +157 -0
  55. package/templates/web/src/components/ui/toast.tsx +128 -0
  56. package/templates/web/src/components/ui/toaster.tsx +35 -0
  57. 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 { useBoard, useGenerators, useGeneration, GeneratorSelectionProvider } from "@weirdfingers/boards";
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
- submit,
31
- isGenerating,
32
- progress,
33
- error: generationError,
34
- result,
35
- } = useGeneration();
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-red-50 border border-red-200 rounded-lg p-6 max-w-lg">
88
- <h2 className="text-red-800 text-xl font-semibold mb-2">
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-red-600">{boardError.message}</p>
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-red-50 border border-red-200 rounded-lg p-6 max-w-lg">
101
- <h2 className="text-red-800 text-xl font-semibold mb-2">
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-red-600">{generatorsError.message}</p>
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-gray-900"></div>
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-gray-50">
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
- <h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
176
- {board.description && (
177
- <p className="text-gray-600 mt-2">{board.description}</p>
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-white rounded-lg shadow-lg p-6 text-center">
202
- <p className="text-gray-500">Loading generators...</p>
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-white rounded-lg shadow-lg p-6 text-center">
206
- <p className="text-gray-500">No generators available</p>
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
- <Providers>
15
- <Header />
16
- {children}
17
- </Providers>
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-yellow-500 rounded-lg p-4 bg-yellow-50">
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-gray-200 rounded flex items-center justify-center">
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-gray-600 mt-1">
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-yellow-200 rounded"
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-orange-500 bg-orange-50"
160
- : "border-gray-300 hover:border-gray-400"
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-gray-700 mb-1">
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-gray-300 rounded bg-white"
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-gray-500 mt-1">
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-red-500 mb-2">
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-gray-500">{errorMessage}</p>
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-gray-900"></div>
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-gray-100">
148
- <ImageIcon className="w-12 h-12 text-gray-400" />
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-gray-100">
202
- <FileVideo className="w-12 h-12 text-gray-400" />
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-gray-100">
281
- <span className="text-gray-400">Unknown type</span>
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-gray-200 group"
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 when not loading/failed */}
314
- {!isLoading && !isFailed && (
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
  }