diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index fd03337a..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(pnpm install:*)", - "Bash(node -e:*)", - "Bash(pnpm start:*)", - "Bash(pnpm test:lib:*)", - "Bash(pnpm typecheck:*)", - "Bash(pnpm build:*)", - "Bash(find:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 34054660..e7f8be05 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,7 @@ test-traces playwright-report test-results -STATUS_*.md \ No newline at end of file +STATUS_*.md + +# Only .claude.settings.json should be committed +.claude/settings.local.json \ No newline at end of file diff --git a/examples/ts-react-fal/.env.example b/examples/ts-react-fal/.env.example new file mode 100644 index 00000000..ae840165 --- /dev/null +++ b/examples/ts-react-fal/.env.example @@ -0,0 +1,4 @@ +# Duplicate the .env.example file and rename it to .env.local, then add your FAL_KEY. + +# Sign up for an account at https://fal.ai, and add $20 of credits to your account to get started. +FAL_KEY= diff --git a/examples/ts-react-fal/package.json b/examples/ts-react-fal/package.json new file mode 100644 index 00000000..2284e5a9 --- /dev/null +++ b/examples/ts-react-fal/package.json @@ -0,0 +1,34 @@ +{ + "name": "ts-react-fal", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "serve": "vite preview", + "test": "exit 0", + "test:types": "tsc" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "@tanstack/ai": "workspace:*", + "@tanstack/ai-fal": "workspace:*", + "@tanstack/react-router": "^1.158.4", + "@tanstack/react-start": "^1.159.0", + "@tanstack/router-plugin": "^1.158.4", + "lucide-react": "^0.561.0", + "nitro": "3.0.1-alpha.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "tailwindcss": "^4.1.18", + "vite-tsconfig-paths": "^5.1.4" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "typescript": "5.9.3", + "vite": "^7.2.7" + } +} diff --git a/examples/ts-react-fal/src/components/Header.tsx b/examples/ts-react-fal/src/components/Header.tsx new file mode 100644 index 00000000..6cf2dde8 --- /dev/null +++ b/examples/ts-react-fal/src/components/Header.tsx @@ -0,0 +1,17 @@ +import { Link } from '@tanstack/react-router' + +export default function Header() { + return ( +
+

+ + 🎨 + TanStack AI Visual + +

+ + Image & Video Generation with fal.ai + +
+ ) +} diff --git a/examples/ts-react-fal/src/components/ImageGenerator.tsx b/examples/ts-react-fal/src/components/ImageGenerator.tsx new file mode 100644 index 00000000..9207551a --- /dev/null +++ b/examples/ts-react-fal/src/components/ImageGenerator.tsx @@ -0,0 +1,209 @@ +import { useState } from 'react' +import { ImageIcon, Loader2, Shuffle } from 'lucide-react' +import type { ImageGenerationResult } from '@tanstack/ai' + +import { generateImageFn } from '@/lib/server-functions' +import { getRandomImagePrompt } from '@/lib/prompts' +import { IMAGE_MODELS } from '@/lib/models' + +interface ImageGeneratorProps { + onImageGenerated?: (imageUrl: string) => void +} + +type ModelResult = { + status: 'loading' | 'success' | 'error' + result?: ImageGenerationResult + error?: string +} + +export default function ImageGenerator({ + onImageGenerated, +}: ImageGeneratorProps) { + const [prompt, setPrompt] = useState('') + const [selectedModel, setSelectedModel] = useState('all') + const [isLoading, setIsLoading] = useState(false) + const [results, setResults] = useState>({}) + + const currentModel = IMAGE_MODELS.find((m) => m.id === selectedModel) + + const handleGenerate = async () => { + if (!prompt.trim()) return + + setIsLoading(true) + setResults({}) + + if (selectedModel === 'all') { + // Initialize all models as loading + const initialResults: Record = {} + for (const model of IMAGE_MODELS) { + initialResults[model.id] = { status: 'loading' } + } + setResults(initialResults) + + // Fire all requests in parallel + const promises = IMAGE_MODELS.map(async (model) => { + try { + const response = await generateImageFn({ + data: { prompt, model: model.id }, + }) + setResults((prev) => ({ + ...prev, + [model.id]: { status: 'success', result: response }, + })) + const imageUrl = response.images[0]?.url + if (imageUrl) { + onImageGenerated?.(imageUrl) + } + } catch (err) { + setResults((prev) => ({ + ...prev, + [model.id]: { + status: 'error', + error: + err instanceof Error ? err.message : 'Failed to generate image', + }, + })) + } + }) + + await Promise.allSettled(promises) + setIsLoading(false) + } else { + // Single model generation + setResults({ [selectedModel]: { status: 'loading' } }) + + try { + const response = await generateImageFn({ + data: { prompt, model: selectedModel }, + }) + setResults({ [selectedModel]: { status: 'success', result: response } }) + const imageUrl = response.images[0]?.url + if (imageUrl) { + onImageGenerated?.(imageUrl) + } + } catch (err) { + setResults({ + [selectedModel]: { + status: 'error', + error: + err instanceof Error ? err.message : 'Failed to generate image', + }, + }) + } finally { + setIsLoading(false) + } + } + } + + return ( +
+
+
+ + + {currentModel && selectedModel !== 'all' && ( +

+ {currentModel.description} +

+ )} +
+ +
+
+ + +
+