@weirdfingers/baseboards 0.2.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 (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,120 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --radius-sm: calc(var(--radius) - 4px);
8
+ --radius-md: calc(var(--radius) - 2px);
9
+ --radius-lg: var(--radius);
10
+ --radius-xl: calc(var(--radius) + 4px);
11
+ --color-background: var(--background);
12
+ --color-foreground: var(--foreground);
13
+ --color-card: var(--card);
14
+ --color-card-foreground: var(--card-foreground);
15
+ --color-popover: var(--popover);
16
+ --color-popover-foreground: var(--popover-foreground);
17
+ --color-primary: var(--primary);
18
+ --color-primary-foreground: var(--primary-foreground);
19
+ --color-secondary: var(--secondary);
20
+ --color-secondary-foreground: var(--secondary-foreground);
21
+ --color-muted: var(--muted);
22
+ --color-muted-foreground: var(--muted-foreground);
23
+ --color-accent: var(--accent);
24
+ --color-accent-foreground: var(--accent-foreground);
25
+ --color-destructive: var(--destructive);
26
+ --color-border: var(--border);
27
+ --color-input: var(--input);
28
+ --color-ring: var(--ring);
29
+ --color-chart-1: var(--chart-1);
30
+ --color-chart-2: var(--chart-2);
31
+ --color-chart-3: var(--chart-3);
32
+ --color-chart-4: var(--chart-4);
33
+ --color-chart-5: var(--chart-5);
34
+ --color-sidebar: var(--sidebar);
35
+ --color-sidebar-foreground: var(--sidebar-foreground);
36
+ --color-sidebar-primary: var(--sidebar-primary);
37
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38
+ --color-sidebar-accent: var(--sidebar-accent);
39
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40
+ --color-sidebar-border: var(--sidebar-border);
41
+ --color-sidebar-ring: var(--sidebar-ring);
42
+ }
43
+
44
+ :root {
45
+ --radius: 0.625rem;
46
+ --background: oklch(1 0 0);
47
+ --foreground: oklch(0.145 0 0);
48
+ --card: oklch(1 0 0);
49
+ --card-foreground: oklch(0.145 0 0);
50
+ --popover: oklch(1 0 0);
51
+ --popover-foreground: oklch(0.145 0 0);
52
+ --primary: oklch(0.205 0 0);
53
+ --primary-foreground: oklch(0.985 0 0);
54
+ --secondary: oklch(0.97 0 0);
55
+ --secondary-foreground: oklch(0.205 0 0);
56
+ --muted: oklch(0.97 0 0);
57
+ --muted-foreground: oklch(0.556 0 0);
58
+ --accent: oklch(0.97 0 0);
59
+ --accent-foreground: oklch(0.205 0 0);
60
+ --destructive: oklch(0.577 0.245 27.325);
61
+ --border: oklch(0.922 0 0);
62
+ --input: oklch(0.922 0 0);
63
+ --ring: oklch(0.708 0 0);
64
+ --chart-1: oklch(0.646 0.222 41.116);
65
+ --chart-2: oklch(0.6 0.118 184.704);
66
+ --chart-3: oklch(0.398 0.07 227.392);
67
+ --chart-4: oklch(0.828 0.189 84.429);
68
+ --chart-5: oklch(0.769 0.188 70.08);
69
+ --sidebar: oklch(0.985 0 0);
70
+ --sidebar-foreground: oklch(0.145 0 0);
71
+ --sidebar-primary: oklch(0.205 0 0);
72
+ --sidebar-primary-foreground: oklch(0.985 0 0);
73
+ --sidebar-accent: oklch(0.97 0 0);
74
+ --sidebar-accent-foreground: oklch(0.205 0 0);
75
+ --sidebar-border: oklch(0.922 0 0);
76
+ --sidebar-ring: oklch(0.708 0 0);
77
+ }
78
+
79
+ .dark {
80
+ --background: oklch(0.145 0 0);
81
+ --foreground: oklch(0.985 0 0);
82
+ --card: oklch(0.205 0 0);
83
+ --card-foreground: oklch(0.985 0 0);
84
+ --popover: oklch(0.205 0 0);
85
+ --popover-foreground: oklch(0.985 0 0);
86
+ --primary: oklch(0.922 0 0);
87
+ --primary-foreground: oklch(0.205 0 0);
88
+ --secondary: oklch(0.269 0 0);
89
+ --secondary-foreground: oklch(0.985 0 0);
90
+ --muted: oklch(0.269 0 0);
91
+ --muted-foreground: oklch(0.708 0 0);
92
+ --accent: oklch(0.269 0 0);
93
+ --accent-foreground: oklch(0.985 0 0);
94
+ --destructive: oklch(0.704 0.191 22.216);
95
+ --border: oklch(1 0 0 / 10%);
96
+ --input: oklch(1 0 0 / 15%);
97
+ --ring: oklch(0.556 0 0);
98
+ --chart-1: oklch(0.488 0.243 264.376);
99
+ --chart-2: oklch(0.696 0.17 162.48);
100
+ --chart-3: oklch(0.769 0.188 70.08);
101
+ --chart-4: oklch(0.627 0.265 303.9);
102
+ --chart-5: oklch(0.645 0.246 16.439);
103
+ --sidebar: oklch(0.205 0 0);
104
+ --sidebar-foreground: oklch(0.985 0 0);
105
+ --sidebar-primary: oklch(0.488 0.243 264.376);
106
+ --sidebar-primary-foreground: oklch(0.985 0 0);
107
+ --sidebar-accent: oklch(0.269 0 0);
108
+ --sidebar-accent-foreground: oklch(0.985 0 0);
109
+ --sidebar-border: oklch(1 0 0 / 10%);
110
+ --sidebar-ring: oklch(0.556 0 0);
111
+ }
112
+
113
+ @layer base {
114
+ * {
115
+ @apply border-border outline-ring/50;
116
+ }
117
+ body {
118
+ @apply bg-background text-foreground;
119
+ }
120
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import "./globals.css";
3
+ import { Providers } from "./providers";
4
+ import { Header } from "@/components/header";
5
+
6
+ export default function RootLayout({
7
+ children,
8
+ }: {
9
+ children: React.ReactNode;
10
+ }) {
11
+ return (
12
+ <html lang="en">
13
+ <body>
14
+ <Providers>
15
+ <Header />
16
+ {children}
17
+ </Providers>
18
+ </body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useBoards } from "@weirdfingers/boards";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import Link from "next/link";
7
+
8
+ export default function Home() {
9
+ const { boards, createBoard } = useBoards();
10
+
11
+ return (
12
+ <main className="container mx-auto p-4">
13
+ <div className="flex justify-between items-center mb-4">
14
+ <h1 className="text-2xl font-bold">Boards</h1>
15
+ <Button onClick={() => createBoard({ title: "New Board" })}>
16
+ Create Board
17
+ </Button>
18
+ </div>
19
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
20
+ {boards.map((board) => (
21
+ <Link href={`/boards/${board.id}`} key={board.id}>
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>{board.title}</CardTitle>
25
+ </CardHeader>
26
+ <CardContent>
27
+ <p>{board.description || "No description"}</p>
28
+ </CardContent>
29
+ </Card>
30
+ </Link>
31
+ ))}
32
+ </div>
33
+ </main>
34
+ );
35
+ }
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { BoardsProvider, NoAuthProvider } from "@weirdfingers/boards";
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8088";
8
+
9
+ return (
10
+ <BoardsProvider
11
+ authProvider={new NoAuthProvider()}
12
+ apiUrl={apiUrl}
13
+ // graphqlUrl will default to `${apiUrl}/graphql`
14
+ >
15
+ {children}
16
+ </BoardsProvider>
17
+ );
18
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { FileVideo, Volume2, X } from "lucide-react";
4
+ import Image from "next/image";
5
+
6
+ interface Generation {
7
+ id: string;
8
+ artifactType: string;
9
+ storageUrl?: string | null;
10
+ thumbnailUrl?: string | null;
11
+ }
12
+
13
+ interface ArtifactSlot {
14
+ name: string;
15
+ type: string; // "audio", "video", "image", etc.
16
+ required: boolean;
17
+ }
18
+
19
+ interface ArtifactInputSlotsProps {
20
+ slots: ArtifactSlot[];
21
+ selectedArtifacts: Map<string, Generation>;
22
+ availableArtifacts: Generation[];
23
+ onSelectArtifact: (slotName: string, artifact: Generation | null) => void;
24
+ }
25
+
26
+ export function ArtifactInputSlots({
27
+ slots,
28
+ selectedArtifacts,
29
+ availableArtifacts,
30
+ onSelectArtifact,
31
+ }: ArtifactInputSlotsProps) {
32
+ const getIcon = (type: string) => {
33
+ switch (type.toLowerCase()) {
34
+ case "video":
35
+ return <FileVideo className="w-5 h-5" />;
36
+ case "audio":
37
+ return <Volume2 className="w-5 h-5" />;
38
+ default:
39
+ return <FileVideo className="w-5 h-5" />;
40
+ }
41
+ };
42
+
43
+ const getFilteredArtifacts = (slotType: string) => {
44
+ return availableArtifacts.filter(
45
+ (artifact) =>
46
+ artifact.artifactType.toLowerCase() === slotType.toLowerCase()
47
+ );
48
+ };
49
+
50
+ return (
51
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
52
+ {slots.map((slot) => {
53
+ const selectedArtifact = selectedArtifacts.get(slot.name);
54
+ const matchingArtifacts = getFilteredArtifacts(slot.type);
55
+
56
+ return (
57
+ <div key={slot.name} className="relative">
58
+ {selectedArtifact ? (
59
+ // Show selected artifact
60
+ <div className="border-2 border-yellow-500 rounded-lg p-4 bg-yellow-50">
61
+ <div className="flex items-start gap-3">
62
+ <div className="flex-shrink-0">
63
+ {selectedArtifact.thumbnailUrl ||
64
+ selectedArtifact.storageUrl ? (
65
+ <Image
66
+ src={
67
+ selectedArtifact.thumbnailUrl ||
68
+ selectedArtifact.storageUrl ||
69
+ ""
70
+ }
71
+ alt={`${slot.type} preview`}
72
+ className="w-16 h-16 object-cover rounded"
73
+ width={64}
74
+ height={64}
75
+ />
76
+ ) : (
77
+ <div className="w-16 h-16 bg-gray-200 rounded flex items-center justify-center">
78
+ {getIcon(slot.type)}
79
+ </div>
80
+ )}
81
+ </div>
82
+ <div className="flex-1 min-w-0">
83
+ <div className="flex items-center gap-2">
84
+ {getIcon(slot.type)}
85
+ <span className="font-medium text-sm capitalize">
86
+ {slot.type} {selectedArtifact.id.substring(0, 7)}
87
+ </span>
88
+ </div>
89
+ <p className="text-xs text-gray-600 mt-1">
90
+ {slot.name.replace(/_/g, " ")}
91
+ </p>
92
+ </div>
93
+ <button
94
+ onClick={() => onSelectArtifact(slot.name, null)}
95
+ className="flex-shrink-0 p-1 hover:bg-yellow-200 rounded"
96
+ >
97
+ <X className="w-4 h-4" />
98
+ </button>
99
+ </div>
100
+ </div>
101
+ ) : (
102
+ // Show slot placeholder
103
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-gray-400 transition-colors">
104
+ <div className="flex flex-col items-center justify-center text-center">
105
+ <div className="mb-2">{getIcon(slot.type)}</div>
106
+ <p className="text-sm font-medium text-gray-700 mb-1">
107
+ Add a {slot.type}
108
+ </p>
109
+ {matchingArtifacts.length > 0 ? (
110
+ <select
111
+ onChange={(e) => {
112
+ const artifact = matchingArtifacts.find(
113
+ (a) => a.id === e.target.value
114
+ );
115
+ if (artifact) {
116
+ onSelectArtifact(slot.name, artifact);
117
+ }
118
+ }}
119
+ className="mt-2 px-3 py-1.5 text-sm border border-gray-300 rounded bg-white"
120
+ >
121
+ <option value="">Select from board...</option>
122
+ {matchingArtifacts.map((artifact) => (
123
+ <option key={artifact.id} value={artifact.id}>
124
+ {artifact.artifactType} -{" "}
125
+ {artifact.id.substring(0, 8)}
126
+ </option>
127
+ ))}
128
+ </select>
129
+ ) : (
130
+ <p className="text-xs text-gray-500 mt-1">
131
+ No {slot.type} artifacts in this board yet
132
+ </p>
133
+ )}
134
+ </div>
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ })}
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,125 @@
1
+ import { FileVideo, Volume2, FileText, Image as ImageIcon } from "lucide-react";
2
+ import Image from "next/image";
3
+
4
+ interface ArtifactPreviewProps {
5
+ artifactType: string;
6
+ storageUrl?: string | null;
7
+ thumbnailUrl?: string | null;
8
+ status: string;
9
+ errorMessage?: string | null;
10
+ onClick?: () => void;
11
+ }
12
+
13
+ export function ArtifactPreview({
14
+ artifactType,
15
+ storageUrl,
16
+ thumbnailUrl,
17
+ status,
18
+ errorMessage,
19
+ onClick,
20
+ }: ArtifactPreviewProps) {
21
+ const isLoading = status === "PENDING" || status === "PROCESSING";
22
+ const isFailed = status === "FAILED" || status === "CANCELLED";
23
+
24
+ // Determine which URL to use for preview
25
+ const previewUrl = thumbnailUrl || storageUrl;
26
+
27
+ const renderContent = () => {
28
+ if (isFailed) {
29
+ return (
30
+ <div className="flex flex-col items-center justify-center h-full p-4 text-center">
31
+ <div className="text-red-500 mb-2">
32
+ {status === "CANCELLED" ? "Cancelled" : "Failed"}
33
+ </div>
34
+ {errorMessage && (
35
+ <p className="text-sm text-gray-500">{errorMessage}</p>
36
+ )}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ if (isLoading) {
42
+ return (
43
+ <div className="flex items-center justify-center h-full">
44
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ switch (artifactType) {
50
+ case "IMAGE":
51
+ if (previewUrl) {
52
+ return (
53
+ <Image
54
+ src={previewUrl}
55
+ alt="Generated image"
56
+ className="w-full h-full object-cover"
57
+ width={512}
58
+ height={512}
59
+ />
60
+ );
61
+ }
62
+ return (
63
+ <div className="flex items-center justify-center h-full bg-gray-100">
64
+ <ImageIcon className="w-12 h-12 text-gray-400" />
65
+ </div>
66
+ );
67
+
68
+ case "VIDEO":
69
+ return (
70
+ <div className="relative w-full h-full">
71
+ {previewUrl ? (
72
+ <Image
73
+ src={previewUrl}
74
+ alt="Video thumbnail"
75
+ className="w-full h-full object-cover"
76
+ width={512}
77
+ height={512}
78
+ />
79
+ ) : (
80
+ <div className="flex items-center justify-center h-full bg-gray-100">
81
+ <FileVideo className="w-12 h-12 text-gray-400" />
82
+ </div>
83
+ )}
84
+ <div className="absolute top-2 left-2 bg-black/50 rounded p-1">
85
+ <FileVideo className="w-5 h-5 text-white" />
86
+ </div>
87
+ </div>
88
+ );
89
+
90
+ case "AUDIO":
91
+ return (
92
+ <div className="flex flex-col items-center justify-center h-full bg-gradient-to-br from-blue-900 to-blue-700">
93
+ <Volume2 className="w-12 h-12 text-white mb-2" />
94
+ <span className="text-white text-sm">Audio file</span>
95
+ </div>
96
+ );
97
+
98
+ case "TEXT":
99
+ return (
100
+ <div className="flex flex-col items-center justify-center h-full bg-gradient-to-br from-gray-700 to-gray-900">
101
+ <FileText className="w-12 h-12 text-white mb-2" />
102
+ <span className="text-white text-sm">Text</span>
103
+ </div>
104
+ );
105
+
106
+ default:
107
+ return (
108
+ <div className="flex items-center justify-center h-full bg-gray-100">
109
+ <span className="text-gray-400">Unknown type</span>
110
+ </div>
111
+ );
112
+ }
113
+ };
114
+
115
+ return (
116
+ <div
117
+ className={`relative aspect-square rounded-lg overflow-hidden border border-gray-200 ${
118
+ onClick ? "cursor-pointer hover:opacity-80 transition-opacity" : ""
119
+ }`}
120
+ onClick={onClick}
121
+ >
122
+ {renderContent()}
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,45 @@
1
+ import { ArtifactPreview } from "./ArtifactPreview";
2
+
3
+ interface Generation {
4
+ id: string;
5
+ artifactType: string;
6
+ storageUrl?: string | null;
7
+ thumbnailUrl?: string | null;
8
+ status: string;
9
+ errorMessage?: string | null;
10
+ createdAt: string;
11
+ }
12
+
13
+ interface GenerationGridProps {
14
+ generations: Generation[];
15
+ onGenerationClick?: (generation: Generation) => void;
16
+ }
17
+
18
+ export function GenerationGrid({
19
+ generations,
20
+ onGenerationClick,
21
+ }: GenerationGridProps) {
22
+ if (generations.length === 0) {
23
+ return (
24
+ <div className="flex items-center justify-center py-12 text-gray-500">
25
+ <p>No generations yet. Create your first one below!</p>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
32
+ {generations.map((generation) => (
33
+ <ArtifactPreview
34
+ key={generation.id}
35
+ artifactType={generation.artifactType}
36
+ storageUrl={generation.storageUrl}
37
+ thumbnailUrl={generation.thumbnailUrl}
38
+ status={generation.status}
39
+ errorMessage={generation.errorMessage}
40
+ onClick={() => onGenerationClick?.(generation)}
41
+ />
42
+ ))}
43
+ </div>
44
+ );
45
+ }