@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,109 @@
1
+ # Docker Compose Configuration for Baseboards
2
+ name: ${PROJECT_NAME:-baseboards}
3
+
4
+ services:
5
+ db:
6
+ image: postgres:16
7
+ env_file: docker/.env
8
+ volumes:
9
+ - db-data:/var/lib/postgresql/data
10
+ healthcheck:
11
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
12
+ interval: 5s
13
+ timeout: 5s
14
+ retries: 20
15
+ networks:
16
+ - internal
17
+
18
+ cache:
19
+ image: redis:7
20
+ command: ["redis-server", "--appendonly", "yes"]
21
+ healthcheck:
22
+ test: ["CMD", "redis-cli", "ping"]
23
+ interval: 5s
24
+ timeout: 3s
25
+ retries: 20
26
+ networks:
27
+ - internal
28
+
29
+ api:
30
+ build:
31
+ context: ./api
32
+ dockerfile: Dockerfile
33
+ image: baseboards-api:local
34
+ env_file:
35
+ - docker/.env
36
+ - api/.env
37
+ depends_on:
38
+ db:
39
+ condition: service_healthy
40
+ cache:
41
+ condition: service_healthy
42
+ ports:
43
+ - "${API_PORT:-8800}:8800"
44
+ healthcheck:
45
+ test: ["CMD", "curl", "-f", "http://localhost:8800/health"]
46
+ interval: 30s
47
+ timeout: 3s
48
+ retries: 50
49
+ networks:
50
+ - internal
51
+
52
+ worker:
53
+ build:
54
+ context: ./api
55
+ dockerfile: Dockerfile
56
+ image: baseboards-api:local
57
+ command:
58
+ [
59
+ "boards-worker",
60
+ "--log-level",
61
+ "debug",
62
+ "--processes",
63
+ "1",
64
+ "--threads",
65
+ "1",
66
+ ]
67
+ env_file:
68
+ - docker/.env
69
+ - api/.env
70
+ depends_on:
71
+ db:
72
+ condition: service_healthy
73
+ cache:
74
+ condition: service_healthy
75
+ networks:
76
+ - internal
77
+
78
+ web:
79
+ build:
80
+ context: ./web
81
+ dockerfile: Dockerfile
82
+ args:
83
+ - NEXT_PUBLIC_API_URL=http://localhost:${API_PORT:-8800}
84
+ - NEXT_PUBLIC_GRAPHQL_URL=http://localhost:${API_PORT:-8800}/graphql
85
+ image: baseboards-web:local
86
+ env_file: web/.env
87
+ environment:
88
+ - NEXT_PUBLIC_API_URL=http://localhost:${API_PORT:-8800}
89
+ - NEXT_PUBLIC_GRAPHQL_URL=http://localhost:${API_PORT:-8800}/graphql
90
+ - INTERNAL_API_URL=http://api:8800
91
+ depends_on:
92
+ api:
93
+ condition: service_healthy
94
+ ports:
95
+ - "${WEB_PORT:-3300}:3000"
96
+ healthcheck:
97
+ test: ["CMD", "curl", "-f", "http://localhost:3000/"]
98
+ interval: 30s
99
+ timeout: 3s
100
+ retries: 50
101
+ networks:
102
+ - internal
103
+
104
+ networks:
105
+ internal:
106
+ driver: bridge
107
+
108
+ volumes:
109
+ db-data:
@@ -0,0 +1,23 @@
1
+ # Docker Compose Environment Variables
2
+ # Generated by CLI - DO NOT EDIT MANUALLY
3
+
4
+ # Project configuration
5
+ PROJECT_NAME=baseboards
6
+
7
+ # Service ports
8
+ WEB_PORT=3300
9
+ API_PORT=8800
10
+
11
+ # PostgreSQL configuration
12
+ POSTGRES_USER=baseboards
13
+ POSTGRES_PASSWORD=REPLACE_WITH_GENERATED_PASSWORD
14
+ POSTGRES_DB=baseboards
15
+
16
+ # Database URL (for API container)
17
+ BOARDS_DATABASE_URL=postgresql://baseboards:REPLACE_WITH_GENERATED_PASSWORD@db:5432/baseboards
18
+
19
+ # Redis URL (for API container)
20
+ BOARDS_REDIS_URL=redis://cache:6379/0
21
+
22
+ # Image version (matches CLI version)
23
+ VERSION=latest
@@ -0,0 +1,28 @@
1
+ # Frontend Environment Variables
2
+
3
+ # API Configuration
4
+ NEXT_PUBLIC_API_URL=http://localhost:8800
5
+ NEXT_PUBLIC_GRAPHQL_URL=http://localhost:8800/graphql
6
+
7
+ # Internal API URL for Docker
8
+ # When running in Docker, this URL is used for server-side requests (like image optimization)
9
+ # to access the API via the internal Docker network instead of localhost
10
+ INTERNAL_API_URL=http://api:8800
11
+
12
+ # Auth Provider
13
+ # For local development, 'none' allows unauthenticated access
14
+ NEXT_PUBLIC_AUTH_PROVIDER=none
15
+
16
+ # Clerk
17
+ # NEXT_PUBLIC_AUTH_PROVIDER=clerk
18
+ # NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
19
+
20
+ # Supabase
21
+ # NEXT_PUBLIC_AUTH_PROVIDER=supabase
22
+ # NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
23
+ # NEXT_PUBLIC_SUPABASE_ANON_KEY=...
24
+
25
+ # Auth0
26
+ # NEXT_PUBLIC_AUTH_PROVIDER=auth0
27
+ # NEXT_PUBLIC_AUTH0_DOMAIN=...
28
+ # NEXT_PUBLIC_AUTH0_CLIENT_ID=...
@@ -0,0 +1,51 @@
1
+ FROM node:20-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+
8
+ # Copy package files
9
+ COPY package.json package-lock.json* pnpm-lock.yaml* ./
10
+ RUN corepack enable pnpm && pnpm install
11
+
12
+ # Rebuild the source code only when needed
13
+ FROM base AS builder
14
+ WORKDIR /app
15
+ COPY --from=deps /app/node_modules ./node_modules
16
+ COPY . .
17
+
18
+ # Accept build args for Next.js public env vars
19
+ ARG NEXT_PUBLIC_API_URL
20
+ ARG NEXT_PUBLIC_GRAPHQL_URL
21
+ ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
22
+ ENV NEXT_PUBLIC_GRAPHQL_URL=${NEXT_PUBLIC_GRAPHQL_URL}
23
+
24
+ RUN corepack enable pnpm && pnpm run build
25
+
26
+ # Production image, copy all the files and run next
27
+ FROM base AS runner
28
+ WORKDIR /app
29
+
30
+ ENV NODE_ENV=production
31
+
32
+ RUN addgroup --system --gid 1001 nodejs
33
+ RUN adduser --system --uid 1001 nextjs
34
+
35
+ # Enable pnpm for dev mode (must be done as root before USER switch)
36
+ RUN corepack enable pnpm
37
+
38
+ # Automatically leverage output traces to reduce image size
39
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
40
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
41
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
42
+ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
43
+
44
+ USER nextjs
45
+
46
+ EXPOSE 3000
47
+
48
+ ENV PORT=3000
49
+ ENV HOSTNAME="0.0.0.0"
50
+
51
+ CMD ["node", "server.js"]
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Custom image loader for Next.js
3
+ * Rewrites NEXT_PUBLIC_API_URL to INTERNAL_API_URL in the image URL query parameter
4
+ * so that when Next.js's server-side image optimizer fetches the image,
5
+ * it uses the internal Docker network URL instead of localhost
6
+ */
7
+ export default function imageLoader({ src, width, quality }) {
8
+ const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;
9
+ const internalApiUrl = process.env.INTERNAL_API_URL;
10
+
11
+ // If we have an internal API URL and the src uses the public one, rewrite it
12
+ let targetSrc = src;
13
+ if (internalApiUrl && publicApiUrl && src.includes(publicApiUrl)) {
14
+ targetSrc = src.replace(publicApiUrl, internalApiUrl);
15
+ }
16
+
17
+ return `/_next/image?url=${encodeURIComponent(targetSrc)}&w=${width}&q=${quality || 75}`;
18
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -0,0 +1,36 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: "standalone",
4
+ reactStrictMode: true,
5
+ transpilePackages: ["@weirdfingers/boards"],
6
+ images: {
7
+ unoptimized: true, // Disable server-side optimization for Docker compatibility
8
+ // Use custom loader when INTERNAL_API_URL is set (Docker environment)
9
+ // This allows server-side image optimization to fetch from internal network
10
+ ...(process.env.INTERNAL_API_URL && {
11
+ loader: "custom",
12
+ loaderFile: "./imageLoader.js",
13
+ }),
14
+ remotePatterns: [
15
+ {
16
+ protocol: "http",
17
+ hostname: "localhost",
18
+ port: "8800",
19
+ pathname: "/api/storage/**",
20
+ },
21
+ // Allow internal Docker hostname for image optimization
22
+ ...(process.env.INTERNAL_API_URL
23
+ ? [
24
+ {
25
+ protocol: "http",
26
+ hostname: "api",
27
+ port: "8800",
28
+ pathname: "/api/storage/**",
29
+ },
30
+ ]
31
+ : []),
32
+ ],
33
+ },
34
+ };
35
+
36
+ module.exports = nextConfig;
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "baseboards",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p ${PORT:-3033}",
7
+ "build": "next build",
8
+ "start": "next start -p ${PORT:-3033}",
9
+ "lint": "next lint",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@radix-ui/react-navigation-menu": "^1.2.14",
14
+ "@radix-ui/react-slot": "^1.2.3",
15
+ "@tailwindcss/postcss": "^4.1.13",
16
+ "@weirdfingers/boards": "^0.2.0",
17
+ "class-variance-authority": "^0.7.1",
18
+ "clsx": "^2.0.0",
19
+ "graphql": "^16.11.0",
20
+ "lucide-react": "^0.544.0",
21
+ "next": "14.2.5",
22
+ "react": "^18.3.1",
23
+ "react-dom": "^18.3.1",
24
+ "tailwind-merge": "^3.3.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.14.10",
28
+ "@types/react": "^18.3.3",
29
+ "@types/react-dom": "^18.3.0",
30
+ "eslint": "^8.57.0",
31
+ "eslint-config-next": "14.2.5",
32
+ "postcss": "^8.5.6",
33
+ "tailwindcss": "^4.1.13",
34
+ "tw-animate-css": "^1.4.0",
35
+ "typescript": "^5.5.3"
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
Binary file
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useParams } from "next/navigation";
5
+ import { useBoard, useGenerators, useGeneration } from "@weirdfingers/boards";
6
+ import { GenerationGrid } from "@/components/boards/GenerationGrid";
7
+ import { GenerationInput } from "@/components/boards/GenerationInput";
8
+
9
+ export default function BoardPage() {
10
+ const params = useParams();
11
+ const boardId = params.boardId as string;
12
+ console.log("[BoardPage] Rendering with boardId:", boardId);
13
+
14
+ const boardHookResult = useBoard(boardId);
15
+ console.log("[BoardPage] useBoard result:", boardHookResult);
16
+ const {
17
+ board,
18
+ loading: boardLoading,
19
+ error: boardError,
20
+ refresh: refreshBoard,
21
+ } = boardHookResult;
22
+
23
+ // Fetch available generators
24
+ const generatorsHookResult = useGenerators();
25
+ console.log("[BoardPage] useGenerators result:", generatorsHookResult);
26
+ const {
27
+ generators,
28
+ loading: generatorsLoading,
29
+ error: generatorsError,
30
+ } = generatorsHookResult;
31
+
32
+ // Use generation hook for submitting generations and real-time progress
33
+ const {
34
+ submit,
35
+ isGenerating,
36
+ progress,
37
+ error: generationError,
38
+ result,
39
+ } = useGeneration();
40
+
41
+ console.log("[BoardPage] board:", board);
42
+ console.log("[BoardPage] boardError:", boardError);
43
+ console.log("[BoardPage] board is null/undefined?", !board);
44
+
45
+ // Refresh board when a generation completes or fails
46
+ // MUST be before conditional returns to satisfy Rules of Hooks
47
+ React.useEffect(() => {
48
+ if (
49
+ progress &&
50
+ (progress.status === "completed" || progress.status === "failed")
51
+ ) {
52
+ console.log(
53
+ "[BoardPage] Generation finished, refreshing board:",
54
+ progress.status
55
+ );
56
+ refreshBoard();
57
+ }
58
+ }, [progress, refreshBoard]);
59
+
60
+ // Merge database generations with live progress for real-time updates
61
+ // MUST be before conditional returns to satisfy Rules of Hooks
62
+ const generations = React.useMemo(() => {
63
+ const dbGenerations = board?.generations || [];
64
+
65
+ // If we have live progress, update the matching generation's status
66
+ if (progress) {
67
+ return dbGenerations.map((gen) => {
68
+ if (gen.id === progress.jobId) {
69
+ // Map SSE status to GraphQL status format (lowercase to UPPERCASE)
70
+ const statusMap: Record<string, string> = {
71
+ queued: "PENDING",
72
+ processing: "PROCESSING",
73
+ completed: "COMPLETED",
74
+ failed: "FAILED",
75
+ cancelled: "CANCELLED",
76
+ };
77
+
78
+ return {
79
+ ...gen,
80
+ status: statusMap[progress.status] || gen.status,
81
+ errorMessage:
82
+ progress.status === "failed"
83
+ ? progress.message || "Generation failed"
84
+ : gen.errorMessage,
85
+ };
86
+ }
87
+ return gen;
88
+ });
89
+ }
90
+
91
+ return dbGenerations;
92
+ }, [board?.generations, progress]);
93
+
94
+ // Handle board error
95
+ if (boardError) {
96
+ console.error("[BoardPage] Board error:", boardError);
97
+ return (
98
+ <div className="flex items-center justify-center min-h-screen">
99
+ <div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-lg">
100
+ <h2 className="text-red-800 text-xl font-semibold mb-2">
101
+ Error Loading Board
102
+ </h2>
103
+ <p className="text-red-600">{boardError.message}</p>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+ if (generatorsError) {
109
+ console.error("[BoardPage] Generators error:", generatorsError);
110
+ return (
111
+ <div className="flex items-center justify-center min-h-screen">
112
+ <div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-lg">
113
+ <h2 className="text-red-800 text-xl font-semibold mb-2">
114
+ Error Loading Generators
115
+ </h2>
116
+ <p className="text-red-600">{generatorsError.message}</p>
117
+ </div>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ // Handle loading state
123
+ if (boardLoading || !board) {
124
+ console.log(
125
+ "[BoardPage] Showing loading spinner - boardLoading:",
126
+ boardLoading,
127
+ "board:",
128
+ board
129
+ );
130
+ return (
131
+ <div className="flex items-center justify-center min-h-screen">
132
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ console.log("[BoardPage] Board loaded successfully:", board);
138
+
139
+ // Filter completed generations that can be used as inputs
140
+ const availableArtifacts = generations.filter(
141
+ (gen) => gen.status === "COMPLETED" && gen.storageUrl
142
+ );
143
+
144
+ const handleGenerationSubmit = async (params: {
145
+ generatorName: string;
146
+ prompt: string;
147
+ artifacts: Map<string, unknown>;
148
+ settings: Record<string, unknown>;
149
+ }) => {
150
+ try {
151
+ // Find the selected generator to validate it exists
152
+ const selectedGenerator = generators.find(
153
+ (g) => g.name === params.generatorName
154
+ );
155
+
156
+ if (!selectedGenerator) {
157
+ throw new Error(`Generator ${params.generatorName} not found`);
158
+ }
159
+
160
+ // Build inputs object with prompt and artifact references
161
+ const inputs: Record<string, unknown> = {
162
+ prompt: params.prompt,
163
+ };
164
+
165
+ // Add artifact inputs (convert artifact objects to their IDs)
166
+ params.artifacts.forEach((artifact, slotName) => {
167
+ if (artifact && typeof artifact === "object" && "id" in artifact) {
168
+ inputs[slotName] = artifact.id;
169
+ } else if (artifact) {
170
+ inputs[slotName] = artifact;
171
+ }
172
+ });
173
+
174
+ // Submit generation using the hook
175
+ // Provider is typically encoded in the generator name or use "default"
176
+ await submit({
177
+ boardId,
178
+ model: params.generatorName,
179
+ artifactType: selectedGenerator.artifactType,
180
+ inputs: inputs as { prompt: string; [key: string]: unknown },
181
+ options: params.settings,
182
+ });
183
+ } catch (error) {
184
+ console.error("Failed to create generation:", error);
185
+ }
186
+ };
187
+
188
+ return (
189
+ <main className="min-h-screen bg-gray-50">
190
+ <div className="container mx-auto px-4 py-6 max-w-7xl">
191
+ {/* Header */}
192
+ <div className="mb-6">
193
+ <h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
194
+ {board.description && (
195
+ <p className="text-gray-600 mt-2">{board.description}</p>
196
+ )}
197
+ </div>
198
+
199
+ {/* Generation Grid */}
200
+ <div className="mb-8">
201
+ <GenerationGrid
202
+ generations={generations}
203
+ onGenerationClick={(gen) => {
204
+ console.log("Clicked generation:", gen);
205
+ // TODO: Open generation detail modal
206
+ }}
207
+ />
208
+ </div>
209
+
210
+ {/* Generation Input */}
211
+ <div className="sticky bottom-6 z-10">
212
+ {generatorsLoading ? (
213
+ <div className="bg-white rounded-lg shadow-lg p-6 text-center">
214
+ <p className="text-gray-500">Loading generators...</p>
215
+ </div>
216
+ ) : generators.length === 0 ? (
217
+ <div className="bg-white rounded-lg shadow-lg p-6 text-center">
218
+ <p className="text-gray-500">No generators available</p>
219
+ </div>
220
+ ) : (
221
+ <GenerationInput
222
+ generators={generators}
223
+ availableArtifacts={availableArtifacts}
224
+ onSubmit={handleGenerationSubmit}
225
+ isGenerating={isGenerating}
226
+ />
227
+ )}
228
+ </div>
229
+ </div>
230
+ </main>
231
+ );
232
+ }