Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c3dd0ea
feat: Introduce fal.ai adapter for image and video generation
tombeckenham Jan 20, 2026
c8ec110
Remove settings.local.json from commits. Only settings.json should be…
tombeckenham Jan 20, 2026
42035eb
Use the api key from the config in preference to env
tombeckenham Jan 20, 2026
83d4767
Moved @tanstack/ai just to peerDependencies.
tombeckenham Jan 20, 2026
4bcd449
Corrected package version, typescript types
tombeckenham Jan 23, 2026
841c5b6
ci: apply automated fixes
autofix-ci[bot] Jan 23, 2026
e4f5029
fix up the PR
AlemTuzlak Jan 23, 2026
0295f19
Merge branch 'main' into feat/fal-ai-adapter
AlemTuzlak Jan 23, 2026
d1706c7
Add fetch fal models and compare scripts to show what is out of sync.…
tombeckenham Jan 25, 2026
bf2518e
Corrected comments and linting issues
tombeckenham Jan 25, 2026
ec0e5a4
feat(ai-fal): implement fal.ai adapter with OpenAPI schema generation
tombeckenham Jan 26, 2026
80c91cd
Updated scripts to use heyapi
tombeckenham Jan 28, 2026
abd9e00
Correct lint issues
tombeckenham Jan 28, 2026
a9ae262
Updated script to use heyapi to generate types and zod schema
tombeckenham Jan 28, 2026
c5d9405
Refactor fal.ai scripts and update package.json
tombeckenham Jan 28, 2026
77806d3
fix(ai-fal): simplify endpoint map generation by deriving output type…
tombeckenham Jan 28, 2026
69a9418
fix(ai-fal): improve generated endpoint maps with proper types and fo…
tombeckenham Jan 28, 2026
060ef3a
Updated image and video to use the new types
tombeckenham Jan 28, 2026
2d98480
Merge remote-tracking branch 'upstream/main' into feat/fal-ai-adapter
tombeckenham Jan 28, 2026
9beb9fa
refactor(ai-fal): update file paths and improve directory handling in…
tombeckenham Jan 28, 2026
f9ced90
refactor(ai-fal): consolidate categories and add file upload type tra…
tombeckenham Feb 2, 2026
15183e7
refactor(ai-fal): update file data types to string across schemas
tombeckenham Feb 2, 2026
6a62a1c
Removed openapi stuff
tombeckenham Feb 3, 2026
028c193
Corrected image generation and simplified to make use of image size t…
tombeckenham Feb 3, 2026
158f97f
Corrected and simplified video adapter
tombeckenham Feb 3, 2026
17126fb
Updated image gen options
tombeckenham Feb 3, 2026
ed2e1ea
Add fal example app
tombeckenham Feb 3, 2026
6431aa2
Uses model options to remain type safe
tombeckenham Feb 3, 2026
882b82f
Corrected some model options
tombeckenham Feb 3, 2026
18d1e4c
Resolved code rabbit suggestions
tombeckenham Feb 3, 2026
3c151c9
Remove json flles we no longer use
tombeckenham Feb 4, 2026
47932b9
Reformatted openrouter file I shouldn't have modifed
tombeckenham Feb 4, 2026
59cade5
Removed files we no longer use from gitignore
tombeckenham Feb 4, 2026
8aff7fb
Merge remote-tracking branch 'upstream/main' into feat/fal-ai-adapter
tombeckenham Feb 4, 2026
d5c0200
Merge branch 'main' into feat/fal-ai-adapter
tombeckenham Feb 8, 2026
9d0d630
Updated lock
tombeckenham Feb 8, 2026
d737b6c
Updated to fal 1.9
tombeckenham Feb 8, 2026
96db314
Corrected tests and packages
tombeckenham Feb 9, 2026
35ce529
Figured out how to do the template strings for sizes
tombeckenham Feb 12, 2026
14353b1
Thread size type through ImageGenerationOptions to adapters
tombeckenham Feb 13, 2026
1dc1c57
Refactor fal adapter API to use core generateImage/generateVideo func…
tombeckenham Feb 13, 2026
cf47c31
Add modelSizeByName to VideoAdapter alongside modelProviderOptionsByName
tombeckenham Feb 13, 2026
14f4d15
Merge branch 'main' into feat/fal-ai-adapter
tombeckenham Feb 13, 2026
c3fb2b3
Corrected test issues
tombeckenham Feb 13, 2026
4367294
Uploading lock
tombeckenham Feb 13, 2026
c6422c2
ci: apply automated fixes
autofix-ci[bot] Feb 13, 2026
c43ddb1
Merge branch 'main' into feat/fal-ai-adapter
AlemTuzlak Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .claude/settings.local.json

This file was deleted.

5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ test-traces
playwright-report
test-results

STATUS_*.md
STATUS_*.md

# Only .claude.settings.json should be committed
.claude/settings.local.json
4 changes: 4 additions & 0 deletions examples/ts-react-fal/.env.example
Original file line number Diff line number Diff line change
@@ -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=
34 changes: 34 additions & 0 deletions examples/ts-react-fal/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
17 changes: 17 additions & 0 deletions examples/ts-react-fal/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Link } from '@tanstack/react-router'

export default function Header() {
return (
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
<h1 className="text-xl font-semibold">
<Link to="/" className="flex items-center gap-3">
<span className="text-2xl">🎨</span>
<span>TanStack AI Visual</span>
</Link>
</h1>
<span className="ml-4 text-sm text-gray-400">
Image & Video Generation with fal.ai
</span>
</header>
)
}
209 changes: 209 additions & 0 deletions examples/ts-react-fal/src/components/ImageGenerator.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('all')
const [isLoading, setIsLoading] = useState(false)
const [results, setResults] = useState<Record<string, ModelResult>>({})

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<string, ModelResult> = {}
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 (
<div className="space-y-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Model
</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
disabled={isLoading}
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50"
>
<option value="all">All Models</option>
{IMAGE_MODELS.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{currentModel && selectedModel !== 'all' && (
<p className="mt-1 text-xs text-gray-500">
{currentModel.description}
</p>
)}
</div>

<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-300">Prompt</label>
<button
onClick={() => setPrompt(getRandomImagePrompt())}
disabled={isLoading}
className="flex items-center gap-1.5 px-3 py-1 text-xs font-medium text-purple-400 hover:text-purple-300 bg-purple-500/10 hover:bg-purple-500/20 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Shuffle className="w-3.5 h-3.5" />
Shuffle
</button>
</div>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the image you want to generate..."
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
rows={3}
disabled={isLoading}
/>
</div>

<button
onClick={handleGenerate}
disabled={isLoading || !prompt.trim()}
className="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-700 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Generating...
</>
) : (
<>
<ImageIcon className="w-5 h-5" />
Generate Image
</>
)}
</button>
</div>

{Object.keys(results).length > 0 && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-white">
{selectedModel === 'all' ? 'Generated Images' : 'Generated Image'}
</h3>
{Object.entries(results).map(([modelId, modelResult]) => {
const model = IMAGE_MODELS.find((m) => m.id === modelId)
return (
<div key={modelId} className="space-y-2">
{selectedModel === 'all' && (
<h4 className="text-sm font-medium text-gray-300">
{model?.name ?? modelId}
</h4>
)}
{modelResult.status === 'loading' && (
<div className="flex items-center gap-2 p-4 bg-gray-800 rounded-lg border border-gray-700">
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
<span className="text-gray-400">Generating...</span>
</div>
)}
{modelResult.status === 'error' && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400">
{modelResult.error}
</div>
)}
{modelResult.status === 'success' &&
modelResult.result &&
modelResult.result.images.length > 0 && (
<div className="rounded-lg overflow-hidden border border-gray-700">
<img
src={modelResult.result.images[0]?.url}
alt={`Generated by ${model?.name ?? modelId}`}
className="w-full h-auto"
/>
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
Loading
Loading