@weirdfingers/baseboards 0.5.3 → 0.6.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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/templates/api/alembic/env.py +9 -1
  4. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +107 -49
  5. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +7 -3
  6. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +57 -1
  7. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +134 -0
  8. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +8 -5
  9. package/templates/api/config/generators.yaml +111 -0
  10. package/templates/api/src/boards/__init__.py +1 -1
  11. package/templates/api/src/boards/api/app.py +2 -1
  12. package/templates/api/src/boards/api/endpoints/tenant_registration.py +1 -1
  13. package/templates/api/src/boards/api/endpoints/uploads.py +150 -0
  14. package/templates/api/src/boards/auth/factory.py +1 -1
  15. package/templates/api/src/boards/dbmodels/__init__.py +8 -22
  16. package/templates/api/src/boards/generators/artifact_resolution.py +45 -12
  17. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +16 -1
  18. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +171 -0
  19. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +167 -0
  20. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +194 -0
  21. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +209 -0
  22. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +206 -0
  23. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +237 -0
  24. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +1 -1
  25. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +30 -0
  26. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +220 -0
  27. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +173 -0
  28. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +227 -0
  29. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +203 -0
  30. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +230 -0
  31. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +204 -0
  32. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +221 -0
  33. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +177 -0
  34. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +182 -0
  35. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +167 -0
  36. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +299 -0
  37. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +190 -0
  38. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +226 -0
  39. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +249 -0
  40. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +244 -0
  41. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +42 -0
  42. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +209 -0
  43. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +161 -0
  44. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +222 -0
  45. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +152 -0
  46. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +197 -0
  47. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +173 -0
  48. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +221 -0
  49. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +175 -0
  50. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +153 -0
  51. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +172 -0
  52. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +175 -0
  53. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +163 -0
  54. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +155 -0
  55. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +174 -0
  56. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +194 -0
  57. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +1 -1
  58. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +158 -0
  59. package/templates/api/src/boards/graphql/access_control.py +1 -1
  60. package/templates/api/src/boards/graphql/mutations/root.py +16 -4
  61. package/templates/api/src/boards/graphql/resolvers/board.py +0 -2
  62. package/templates/api/src/boards/graphql/resolvers/generation.py +10 -233
  63. package/templates/api/src/boards/graphql/resolvers/lineage.py +381 -0
  64. package/templates/api/src/boards/graphql/resolvers/upload.py +463 -0
  65. package/templates/api/src/boards/graphql/types/generation.py +62 -26
  66. package/templates/api/src/boards/middleware.py +1 -1
  67. package/templates/api/src/boards/storage/factory.py +2 -2
  68. package/templates/api/src/boards/tenant_isolation.py +9 -9
  69. package/templates/api/src/boards/workers/actors.py +10 -1
  70. package/templates/web/package.json +1 -1
  71. package/templates/web/src/app/boards/[boardId]/page.tsx +14 -5
  72. package/templates/web/src/app/lineage/[generationId]/page.tsx +233 -0
  73. package/templates/web/src/components/boards/ArtifactPreview.tsx +20 -1
  74. package/templates/web/src/components/boards/UploadArtifact.tsx +253 -0
@@ -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.5.3",
17
+ "@weirdfingers/boards": "^0.6.1",
18
18
  "class-variance-authority": "^0.7.1",
19
19
  "clsx": "^2.0.0",
20
20
  "graphql": "^16.11.0",
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
5
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
+ import { UploadArtifact } from "@/components/boards/UploadArtifact";
8
9
 
9
10
  export default function BoardPage() {
10
11
  const params = useParams();
@@ -169,11 +170,19 @@ export default function BoardPage() {
169
170
  <main className="min-h-screen bg-gray-50">
170
171
  <div className="container mx-auto px-4 py-6 max-w-7xl">
171
172
  {/* 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
- )}
173
+ <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>
178
+ )}
179
+ </div>
180
+ <UploadArtifact
181
+ boardId={boardId}
182
+ onUploadComplete={() => {
183
+ refreshBoard();
184
+ }}
185
+ />
177
186
  </div>
178
187
 
179
188
  {/* Generation Grid */}
@@ -0,0 +1,233 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useParams, useRouter } from "next/navigation";
5
+ import { useLineage, type AncestryNode, type DescendantNode } from "@weirdfingers/boards";
6
+ import { Card } from "@/components/ui/card";
7
+ import { Button } from "@/components/ui/button";
8
+ import { ArrowLeft, Loader2 } from "lucide-react";
9
+
10
+ export default function LineageExplorerPage() {
11
+ const params = useParams();
12
+ const router = useRouter();
13
+ const generationId = params.generationId as string;
14
+
15
+ const { ancestry, descendants, loading, error } = useLineage(generationId, {
16
+ maxDepth: 10,
17
+ });
18
+
19
+ if (loading) {
20
+ return (
21
+ <div className="container mx-auto p-6 flex items-center justify-center min-h-screen">
22
+ <div className="flex items-center gap-2">
23
+ <Loader2 className="h-6 w-6 animate-spin" />
24
+ <span>Loading lineage...</span>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ if (error) {
31
+ return (
32
+ <div className="container mx-auto p-6">
33
+ <Card className="p-6 border-red-500">
34
+ <h2 className="text-xl font-bold text-red-600 mb-2">Error</h2>
35
+ <p className="text-red-600">{error.message}</p>
36
+ <Button
37
+ onClick={() => router.back()}
38
+ variant="outline"
39
+ className="mt-4"
40
+ >
41
+ <ArrowLeft className="mr-2 h-4 w-4" />
42
+ Go Back
43
+ </Button>
44
+ </Card>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <div className="container mx-auto p-6">
51
+ <div className="mb-6">
52
+ <Button
53
+ onClick={() => router.back()}
54
+ variant="outline"
55
+ className="mb-4"
56
+ >
57
+ <ArrowLeft className="mr-2 h-4 w-4" />
58
+ Back
59
+ </Button>
60
+ <h1 className="text-3xl font-bold">Artifact Lineage Explorer</h1>
61
+ <p className="text-muted-foreground mt-2">
62
+ Explore the ancestry and descendants of generation {generationId}
63
+ </p>
64
+ </div>
65
+
66
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
67
+ {/* Ancestry Section */}
68
+ <Card className="p-6">
69
+ <h2 className="text-2xl font-bold mb-4">Ancestry</h2>
70
+ <p className="text-muted-foreground mb-4">
71
+ Shows all parent generations that contributed to this artifact
72
+ </p>
73
+ {ancestry ? (
74
+ <AncestryTree node={ancestry} currentGenerationId={generationId} />
75
+ ) : (
76
+ <p className="text-muted-foreground">No ancestry data available</p>
77
+ )}
78
+ </Card>
79
+
80
+ {/* Descendants Section */}
81
+ <Card className="p-6">
82
+ <h2 className="text-2xl font-bold mb-4">Descendants</h2>
83
+ <p className="text-muted-foreground mb-4">
84
+ Shows all child generations that used this artifact as input
85
+ </p>
86
+ {descendants ? (
87
+ <DescendantTree node={descendants} currentGenerationId={generationId} />
88
+ ) : (
89
+ <p className="text-muted-foreground">
90
+ No descendants data available
91
+ </p>
92
+ )}
93
+ </Card>
94
+ </div>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ interface AncestryTreeProps {
100
+ node: AncestryNode;
101
+ currentGenerationId: string;
102
+ }
103
+
104
+ function AncestryTree({ node, currentGenerationId }: AncestryTreeProps) {
105
+ const router = useRouter();
106
+ const isCurrentGeneration = node.generation.id === currentGenerationId;
107
+
108
+ return (
109
+ <div className="space-y-2">
110
+ <div
111
+ className={`p-3 rounded-lg border ${
112
+ isCurrentGeneration
113
+ ? "bg-primary/10 border-primary"
114
+ : "bg-card hover:bg-accent cursor-pointer"
115
+ }`}
116
+ onClick={() => {
117
+ if (!isCurrentGeneration) {
118
+ router.push(`/lineage/${node.generation.id}`);
119
+ }
120
+ }}
121
+ >
122
+ <div className="flex items-start justify-between">
123
+ <div className="flex-1">
124
+ <div className="flex items-center gap-2">
125
+ <span className="font-mono text-sm text-muted-foreground">
126
+ Depth: {node.depth}
127
+ </span>
128
+ {node.role && (
129
+ <span className="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
130
+ {node.role}
131
+ </span>
132
+ )}
133
+ </div>
134
+ <p className="font-semibold mt-1">{node.generation.generatorName}</p>
135
+ <p className="text-sm text-muted-foreground">
136
+ {node.generation.artifactType} • {node.generation.status}
137
+ </p>
138
+ <p className="text-xs text-muted-foreground font-mono mt-1">
139
+ {node.generation.id.slice(0, 8)}...
140
+ </p>
141
+ </div>
142
+ {node.generation.thumbnailUrl && (
143
+ <img
144
+ src={node.generation.thumbnailUrl}
145
+ alt="Thumbnail"
146
+ className="w-16 h-16 object-cover rounded"
147
+ />
148
+ )}
149
+ </div>
150
+ </div>
151
+
152
+ {node.parents && node.parents.length > 0 && (
153
+ <div className="ml-6 pl-4 border-l-2 border-muted space-y-2">
154
+ {node.parents.map((parent, idx) => (
155
+ <AncestryTree
156
+ key={`${parent.generation.id}-${idx}`}
157
+ node={parent}
158
+ currentGenerationId={currentGenerationId}
159
+ />
160
+ ))}
161
+ </div>
162
+ )}
163
+ </div>
164
+ );
165
+ }
166
+
167
+ interface DescendantTreeProps {
168
+ node: DescendantNode;
169
+ currentGenerationId: string;
170
+ }
171
+
172
+ function DescendantTree({ node, currentGenerationId }: DescendantTreeProps) {
173
+ const router = useRouter();
174
+ const isCurrentGeneration = node.generation.id === currentGenerationId;
175
+
176
+ return (
177
+ <div className="space-y-2">
178
+ <div
179
+ className={`p-3 rounded-lg border ${
180
+ isCurrentGeneration
181
+ ? "bg-primary/10 border-primary"
182
+ : "bg-card hover:bg-accent cursor-pointer"
183
+ }`}
184
+ onClick={() => {
185
+ if (!isCurrentGeneration) {
186
+ router.push(`/lineage/${node.generation.id}`);
187
+ }
188
+ }}
189
+ >
190
+ <div className="flex items-start justify-between">
191
+ <div className="flex-1">
192
+ <div className="flex items-center gap-2">
193
+ <span className="font-mono text-sm text-muted-foreground">
194
+ Depth: {node.depth}
195
+ </span>
196
+ {node.role && (
197
+ <span className="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">
198
+ {node.role}
199
+ </span>
200
+ )}
201
+ </div>
202
+ <p className="font-semibold mt-1">{node.generation.generatorName}</p>
203
+ <p className="text-sm text-muted-foreground">
204
+ {node.generation.artifactType} • {node.generation.status}
205
+ </p>
206
+ <p className="text-xs text-muted-foreground font-mono mt-1">
207
+ {node.generation.id.slice(0, 8)}...
208
+ </p>
209
+ </div>
210
+ {node.generation.thumbnailUrl && (
211
+ <img
212
+ src={node.generation.thumbnailUrl}
213
+ alt="Thumbnail"
214
+ className="w-16 h-16 object-cover rounded"
215
+ />
216
+ )}
217
+ </div>
218
+ </div>
219
+
220
+ {node.children && node.children.length > 0 && (
221
+ <div className="ml-6 pl-4 border-l-2 border-muted space-y-2">
222
+ {node.children.map((child, idx) => (
223
+ <DescendantTree
224
+ key={`${child.generation.id}-${idx}`}
225
+ node={child}
226
+ currentGenerationId={currentGenerationId}
227
+ />
228
+ ))}
229
+ </div>
230
+ )}
231
+ </div>
232
+ );
233
+ }
@@ -12,13 +12,16 @@ import {
12
12
  Play,
13
13
  Pause,
14
14
  RotateCcw,
15
+ GitBranch,
15
16
  } from "lucide-react";
16
17
  import Image from "next/image";
18
+ import { useRouter } from "next/navigation";
17
19
  import {
18
20
  DropdownMenu,
19
21
  DropdownMenuContent,
20
22
  DropdownMenuItem,
21
23
  DropdownMenuTrigger,
24
+ DropdownMenuSeparator,
22
25
  } from "@/components/ui/dropdown-menu";
23
26
 
24
27
  interface ArtifactPreviewProps {
@@ -50,6 +53,7 @@ export function ArtifactPreview({
50
53
  artifactId,
51
54
  prompt,
52
55
  }: ArtifactPreviewProps) {
56
+ const router = useRouter();
53
57
  const [isPlaying, setIsPlaying] = React.useState(false);
54
58
  const [currentTime, setCurrentTime] = React.useState(0);
55
59
  const [duration, setDuration] = React.useState(0);
@@ -349,7 +353,7 @@ export function ArtifactPreview({
349
353
  )}
350
354
 
351
355
  {/* More options menu - show for all artifacts */}
352
- {(onPreview || onDownload) && (
356
+ {(onPreview || onDownload || artifactId) && (
353
357
  <DropdownMenu>
354
358
  <DropdownMenuTrigger asChild>
355
359
  <button
@@ -361,6 +365,21 @@ export function ArtifactPreview({
361
365
  </button>
362
366
  </DropdownMenuTrigger>
363
367
  <DropdownMenuContent align="end" className="w-40">
368
+ {artifactId && (
369
+ <DropdownMenuItem
370
+ onClick={(e) => {
371
+ e.stopPropagation();
372
+ router.push(`/lineage/${artifactId}`);
373
+ }}
374
+ className="cursor-pointer"
375
+ >
376
+ <GitBranch className="w-4 h-4 mr-2" />
377
+ View Lineage
378
+ </DropdownMenuItem>
379
+ )}
380
+ {artifactId && (onPreview || onDownload) && (
381
+ <DropdownMenuSeparator />
382
+ )}
364
383
  {onPreview && (
365
384
  <DropdownMenuItem
366
385
  onClick={(e) => {
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useState, useRef } from "react";
4
+ import { useUpload, ArtifactType } from "@weirdfingers/boards";
5
+
6
+ interface UploadArtifactProps {
7
+ boardId: string;
8
+ onUploadComplete?: (generationId: string) => void;
9
+ }
10
+
11
+ export function UploadArtifact({ boardId, onUploadComplete }: UploadArtifactProps) {
12
+ const { upload, isUploading, progress, error } = useUpload();
13
+ const [isOpen, setIsOpen] = useState(false);
14
+ const [urlInput, setUrlInput] = useState("");
15
+ const [dragActive, setDragActive] = useState(false);
16
+ const fileInputRef = useRef<HTMLInputElement>(null);
17
+
18
+ const handleFileUpload = useCallback(
19
+ async (file: File) => {
20
+ // Detect artifact type from file
21
+ const type = file.type;
22
+ let artifactType: ArtifactType = ArtifactType.IMAGE;
23
+
24
+ if (type.startsWith("image/")) {
25
+ artifactType = ArtifactType.IMAGE;
26
+ } else if (type.startsWith("video/")) {
27
+ artifactType = ArtifactType.VIDEO;
28
+ } else if (type.startsWith("audio/")) {
29
+ artifactType = ArtifactType.AUDIO;
30
+ } else if (type.startsWith("text/")) {
31
+ artifactType = ArtifactType.TEXT;
32
+ }
33
+
34
+ try {
35
+ const result = await upload({
36
+ boardId,
37
+ artifactType,
38
+ source: file,
39
+ });
40
+ onUploadComplete?.(result.id);
41
+ setIsOpen(false);
42
+ } catch (err) {
43
+ console.error("Upload failed:", err);
44
+ }
45
+ },
46
+ [upload, boardId, onUploadComplete]
47
+ );
48
+
49
+ const handleUrlUpload = useCallback(async () => {
50
+ if (!urlInput.trim()) return;
51
+
52
+ try {
53
+ // Default to image for URL uploads
54
+ const result = await upload({
55
+ boardId,
56
+ artifactType: ArtifactType.IMAGE,
57
+ source: urlInput.trim(),
58
+ });
59
+ onUploadComplete?.(result.id);
60
+ setUrlInput("");
61
+ setIsOpen(false);
62
+ } catch (err) {
63
+ console.error("URL upload failed:", err);
64
+ }
65
+ }, [upload, boardId, urlInput, onUploadComplete]);
66
+
67
+ const handleDrop = useCallback(
68
+ (e: React.DragEvent) => {
69
+ e.preventDefault();
70
+ setDragActive(false);
71
+
72
+ const files = e.dataTransfer.files;
73
+ if (files.length > 0) {
74
+ handleFileUpload(files[0]);
75
+ }
76
+ },
77
+ [handleFileUpload]
78
+ );
79
+
80
+ const handleDragOver = (e: React.DragEvent) => {
81
+ e.preventDefault();
82
+ setDragActive(true);
83
+ };
84
+
85
+ const handleDragLeave = () => {
86
+ setDragActive(false);
87
+ };
88
+
89
+ const handlePaste = useCallback(
90
+ async (e: React.ClipboardEvent) => {
91
+ const items = Array.from(e.clipboardData.items);
92
+
93
+ // Check for image in clipboard
94
+ for (const item of items) {
95
+ if (item.type.startsWith("image/")) {
96
+ const file = item.getAsFile();
97
+ if (file) {
98
+ e.preventDefault();
99
+ await handleFileUpload(file);
100
+ return;
101
+ }
102
+ }
103
+ }
104
+ },
105
+ [handleFileUpload]
106
+ );
107
+
108
+ return (
109
+ <div className="relative">
110
+ <button
111
+ onClick={() => setIsOpen(true)}
112
+ className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
113
+ >
114
+ <svg
115
+ className="w-5 h-5"
116
+ fill="none"
117
+ stroke="currentColor"
118
+ viewBox="0 0 24 24"
119
+ >
120
+ <path
121
+ strokeLinecap="round"
122
+ strokeLinejoin="round"
123
+ strokeWidth={2}
124
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
125
+ />
126
+ </svg>
127
+ Upload
128
+ </button>
129
+
130
+ {isOpen && (
131
+ <div className="absolute right-0 top-full mt-2 w-96 bg-white rounded-lg shadow-xl p-6 z-50 border border-gray-200">
132
+ <div className="flex items-center justify-between mb-4">
133
+ <h3 className="text-lg font-semibold text-gray-900">Upload Artifact</h3>
134
+ <button
135
+ onClick={() => setIsOpen(false)}
136
+ className="text-gray-400 hover:text-gray-600"
137
+ >
138
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
140
+ </svg>
141
+ </button>
142
+ </div>
143
+
144
+ {/* Drag and drop zone */}
145
+ <div
146
+ onDrop={handleDrop}
147
+ onDragOver={handleDragOver}
148
+ onDragLeave={handleDragLeave}
149
+ className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
150
+ dragActive
151
+ ? "border-orange-500 bg-orange-50"
152
+ : "border-gray-300 hover:border-gray-400"
153
+ }`}
154
+ >
155
+ <input
156
+ ref={fileInputRef}
157
+ type="file"
158
+ onChange={(e) => {
159
+ const file = e.target.files?.[0];
160
+ if (file) handleFileUpload(file);
161
+ }}
162
+ className="hidden"
163
+ accept="image/*,video/*,audio/*,text/*"
164
+ />
165
+
166
+ <div className="flex flex-col items-center gap-2">
167
+ <svg
168
+ className="w-12 h-12 text-gray-400"
169
+ fill="none"
170
+ stroke="currentColor"
171
+ viewBox="0 0 24 24"
172
+ >
173
+ <path
174
+ strokeLinecap="round"
175
+ strokeLinejoin="round"
176
+ strokeWidth={2}
177
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
178
+ />
179
+ </svg>
180
+
181
+ <div>
182
+ <button
183
+ onClick={() => fileInputRef.current?.click()}
184
+ className="text-orange-600 hover:text-orange-700 font-medium"
185
+ >
186
+ Choose a file
187
+ </button>
188
+ <span className="text-gray-500"> or drag and drop here</span>
189
+ </div>
190
+
191
+ <p className="text-sm text-gray-500">
192
+ Images, videos, audio, and text files (max 100MB)
193
+ </p>
194
+ </div>
195
+ </div>
196
+
197
+ {/* URL input */}
198
+ <div className="mt-4">
199
+ <label className="block text-sm font-medium text-gray-700 mb-2">
200
+ Or paste a URL or image
201
+ </label>
202
+ <div className="flex gap-2">
203
+ <input
204
+ type="text"
205
+ value={urlInput}
206
+ onChange={(e) => setUrlInput(e.target.value)}
207
+ onPaste={handlePaste}
208
+ onKeyDown={(e) => {
209
+ if (e.key === "Enter") {
210
+ handleUrlUpload();
211
+ }
212
+ }}
213
+ placeholder="https://example.com/image.jpg or paste an image"
214
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
215
+ disabled={isUploading}
216
+ />
217
+ <button
218
+ onClick={handleUrlUpload}
219
+ disabled={!urlInput.trim() || isUploading}
220
+ className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
221
+ >
222
+ Upload
223
+ </button>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Progress bar */}
228
+ {isUploading && (
229
+ <div className="mt-4">
230
+ <div className="flex items-center justify-between text-sm text-gray-600 mb-2">
231
+ <span>Uploading...</span>
232
+ <span>{Math.round(progress)}%</span>
233
+ </div>
234
+ <div className="w-full bg-gray-200 rounded-full h-2">
235
+ <div
236
+ className="bg-orange-500 h-2 rounded-full transition-all duration-300"
237
+ style={{ width: `${progress}%` }}
238
+ />
239
+ </div>
240
+ </div>
241
+ )}
242
+
243
+ {/* Error message */}
244
+ {error && (
245
+ <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
246
+ <p className="text-red-800 text-sm">{error.message}</p>
247
+ </div>
248
+ )}
249
+ </div>
250
+ )}
251
+ </div>
252
+ );
253
+ }