@tanstack/cta-framework-react-cra 0.44.2 → 0.45.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 (97) hide show
  1. package/{examples/tanchat/assets/src/components/example-AIAssistant.tsx → add-ons/ai/assets/src/components/demo-AIAssistant.tsx} +6 -8
  2. package/{examples/tanchat/assets/src/components/example-GuitarRecommendation.tsx → add-ons/ai/assets/src/components/demo-GuitarRecommendation.tsx} +2 -2
  3. package/{examples/tanchat/assets/src/lib/example.ai-hook.ts → add-ons/ai/assets/src/lib/demo-ai-hook.ts} +9 -9
  4. package/{examples/tanchat/assets/src/lib/example.guitar-tools.ts → add-ons/ai/assets/src/lib/demo-guitar-tools.ts} +1 -1
  5. package/{examples/tanchat/assets/src/routes/demo/tanchat.tsx → add-ons/ai/assets/src/routes/demo/ai-chat.tsx} +28 -148
  6. package/{examples/tanchat/assets/src/routes/demo/image.tsx → add-ons/ai/assets/src/routes/demo/ai-image.tsx} +2 -50
  7. package/add-ons/ai/assets/src/routes/demo/ai-structured.tsx +310 -0
  8. package/{examples/tanchat/assets/src/routes/demo/api.tanchat.ts → add-ons/ai/assets/src/routes/demo/api.ai.chat.ts} +16 -6
  9. package/{examples/tanchat/assets/src/routes/demo/api.image.ts → add-ons/ai/assets/src/routes/demo/api.ai.image.ts} +3 -5
  10. package/add-ons/ai/assets/src/routes/demo/api.ai.structured.ts +136 -0
  11. package/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts +89 -0
  12. package/{examples/tanchat/assets/src/routes/demo/api.tts.ts → add-ons/ai/assets/src/routes/demo/api.ai.tts.ts} +1 -1
  13. package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/$guitarId.tsx +3 -2
  14. package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/index.tsx +3 -2
  15. package/add-ons/ai/info.json +46 -0
  16. package/{examples/tanchat → add-ons/ai}/package.json +1 -1
  17. package/examples/events/README.md +110 -0
  18. package/examples/events/assets/content/speakers/andre-costa.md +22 -0
  19. package/examples/events/assets/content/speakers/hans-mueller.md +22 -0
  20. package/examples/events/assets/content/speakers/isabella-martinez.md +22 -0
  21. package/examples/events/assets/content/speakers/kenji-nakamura.md +22 -0
  22. package/examples/events/assets/content/speakers/marie-dubois.md +20 -0
  23. package/examples/events/assets/content/speakers/priya-sharma.md +22 -0
  24. package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
  25. package/examples/events/assets/content/talks/french-macaron-mastery.md +39 -0
  26. package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md +39 -0
  27. package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md +39 -0
  28. package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md +36 -0
  29. package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md +32 -0
  30. package/examples/events/assets/content/talks/the-science-of-sugar.md +39 -0
  31. package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md +39 -0
  32. package/examples/events/assets/content-collections.ts +56 -0
  33. package/examples/events/assets/public/background-1.jpg +0 -0
  34. package/examples/events/assets/public/background-2.jpg +0 -0
  35. package/examples/events/assets/public/background-3.jpg +0 -0
  36. package/examples/events/assets/public/background-4.jpg +0 -0
  37. package/examples/events/assets/public/conference-logo.png +0 -0
  38. package/examples/events/assets/public/favicon.ico +0 -0
  39. package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
  40. package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
  41. package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
  42. package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
  43. package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
  44. package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
  45. package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
  46. package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
  47. package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
  48. package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
  49. package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
  50. package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
  51. package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
  52. package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
  53. package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
  54. package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
  55. package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
  56. package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
  57. package/examples/events/assets/src/components/RemyButton.tsx +18 -0
  58. package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
  59. package/examples/events/assets/src/components/TalkCard.tsx +77 -0
  60. package/examples/events/assets/src/components/ui/card.tsx +92 -0
  61. package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
  62. package/examples/events/assets/src/lib/conference-tools.ts +210 -0
  63. package/examples/events/assets/src/lib/utils.ts +6 -0
  64. package/examples/events/assets/src/routes/api.remy-chat.ts +121 -0
  65. package/examples/events/assets/src/routes/index.tsx +192 -0
  66. package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
  67. package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
  68. package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
  69. package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
  70. package/examples/events/assets/src/routes/talks.index.tsx +40 -0
  71. package/examples/events/assets/src/styles.css +194 -0
  72. package/examples/events/info.json +63 -0
  73. package/examples/events/package.json +23 -0
  74. package/package.json +2 -2
  75. package/examples/tanchat/assets/src/lib/model-selection.ts +0 -78
  76. package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +0 -55
  77. package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +0 -35
  78. package/examples/tanchat/assets/src/routes/demo/api.structured.ts +0 -168
  79. package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +0 -89
  80. package/examples/tanchat/assets/src/routes/demo/structured.tsx +0 -460
  81. package/examples/tanchat/info.json +0 -46
  82. /package/{examples/tanchat → add-ons/ai}/README.md +0 -0
  83. /package/{examples/tanchat → add-ons/ai}/assets/_dot_env.local.append +0 -0
  84. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-flowers.jpg +0 -0
  85. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-motherboard.jpg +0 -0
  86. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-racing.jpg +0 -0
  87. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-steamer-trunk.jpg +0 -0
  88. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-superhero.jpg +0 -0
  89. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-traveling.jpg +0 -0
  90. /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-video-games.jpg +0 -0
  91. /package/{examples/tanchat → add-ons/ai}/assets/public/example-ukelele-tanstack.jpg +0 -0
  92. /package/{examples/tanchat/assets/src/data/example-guitars.ts → add-ons/ai/assets/src/data/demo-guitars.ts} +0 -0
  93. /package/{examples/tanchat/assets/src/hooks/useAudioRecorder.ts → add-ons/ai/assets/src/hooks/demo-useAudioRecorder.ts} +0 -0
  94. /package/{examples/tanchat/assets/src/hooks/useTTS.ts → add-ons/ai/assets/src/hooks/demo-useTTS.ts} +0 -0
  95. /package/{examples/tanchat → add-ons/ai}/assets/src/lib/ai-devtools.tsx +0 -0
  96. /package/{examples/tanchat/assets/src/routes/demo/tanchat.css → add-ons/ai/assets/src/routes/demo/ai-chat.css} +0 -0
  97. /package/{examples/tanchat → add-ons/ai}/small-logo.svg +0 -0
@@ -0,0 +1,310 @@
1
+ import { useState } from 'react'
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ChefHat, Clock, Users, Gauge } from 'lucide-react'
4
+ import { Streamdown } from 'streamdown'
5
+
6
+ import type { Recipe } from './api.ai.structured'
7
+
8
+ type Mode = 'structured' | 'oneshot'
9
+
10
+ const SAMPLE_RECIPES = [
11
+ 'Homemade Margherita Pizza',
12
+ 'Thai Green Curry',
13
+ 'Classic Beef Bourguignon',
14
+ 'Chocolate Lava Cake',
15
+ 'Crispy Korean Fried Chicken',
16
+ 'Fresh Spring Rolls with Peanut Sauce',
17
+ 'Creamy Mushroom Risotto',
18
+ 'Authentic Pad Thai',
19
+ ]
20
+
21
+ function RecipeCard({ recipe }: { recipe: Recipe }) {
22
+ const difficultyColors = {
23
+ easy: 'bg-green-500/20 text-green-400',
24
+ medium: 'bg-yellow-500/20 text-yellow-400',
25
+ hard: 'bg-red-500/20 text-red-400',
26
+ }
27
+
28
+ return (
29
+ <div className="space-y-6">
30
+ {/* Header */}
31
+ <div>
32
+ <h3 className="text-2xl font-bold text-white mb-2">{recipe.name}</h3>
33
+ <p className="text-gray-400">{recipe.description}</p>
34
+ </div>
35
+
36
+ {/* Meta info */}
37
+ <div className="flex flex-wrap gap-4">
38
+ <div className="flex items-center gap-2 text-gray-300">
39
+ <Clock className="w-4 h-4 text-orange-400" />
40
+ <span className="text-sm">Prep: {recipe.prepTime}</span>
41
+ </div>
42
+ <div className="flex items-center gap-2 text-gray-300">
43
+ <Clock className="w-4 h-4 text-orange-400" />
44
+ <span className="text-sm">Cook: {recipe.cookTime}</span>
45
+ </div>
46
+ <div className="flex items-center gap-2 text-gray-300">
47
+ <Users className="w-4 h-4 text-orange-400" />
48
+ <span className="text-sm">{recipe.servings} servings</span>
49
+ </div>
50
+ <div
51
+ className={`flex items-center gap-2 px-2 py-1 rounded-full ${
52
+ difficultyColors[recipe.difficulty]
53
+ }`}
54
+ >
55
+ <Gauge className="w-4 h-4" />
56
+ <span className="text-sm capitalize">{recipe.difficulty}</span>
57
+ </div>
58
+ </div>
59
+
60
+ {/* Ingredients */}
61
+ <div>
62
+ <h4 className="text-lg font-semibold text-white mb-3">Ingredients</h4>
63
+ <ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
64
+ {recipe.ingredients.map((ing, idx) => (
65
+ <li key={idx} className="flex items-start gap-2 text-gray-300">
66
+ <span className="text-orange-400">•</span>
67
+ <span>
68
+ <span className="font-medium">{ing.amount}</span> {ing.item}
69
+ {ing.notes && (
70
+ <span className="text-gray-500 text-sm"> ({ing.notes})</span>
71
+ )}
72
+ </span>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ </div>
77
+
78
+ {/* Instructions */}
79
+ <div>
80
+ <h4 className="text-lg font-semibold text-white mb-3">Instructions</h4>
81
+ <ol className="space-y-3">
82
+ {recipe.instructions.map((step, idx) => (
83
+ <li key={idx} className="flex gap-3 text-gray-300">
84
+ <span className="flex-shrink-0 w-6 h-6 bg-orange-500/20 text-orange-400 rounded-full flex items-center justify-center text-sm font-medium">
85
+ {idx + 1}
86
+ </span>
87
+ <span>{step}</span>
88
+ </li>
89
+ ))}
90
+ </ol>
91
+ </div>
92
+
93
+ {/* Tips */}
94
+ {recipe.tips && recipe.tips.length > 0 && (
95
+ <div>
96
+ <h4 className="text-lg font-semibold text-white mb-3">Tips</h4>
97
+ <ul className="space-y-2">
98
+ {recipe.tips.map((tip, idx) => (
99
+ <li key={idx} className="flex items-start gap-2 text-gray-300">
100
+ <span className="text-yellow-400">*</span>
101
+ <span>{tip}</span>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ </div>
106
+ )}
107
+
108
+ {/* Nutrition */}
109
+ {recipe.nutritionPerServing && (
110
+ <div>
111
+ <h4 className="text-lg font-semibold text-white mb-3">
112
+ Nutrition (per serving)
113
+ </h4>
114
+ <div className="flex flex-wrap gap-4 text-sm">
115
+ {recipe.nutritionPerServing.calories && (
116
+ <span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
117
+ {recipe.nutritionPerServing.calories} cal
118
+ </span>
119
+ )}
120
+ {recipe.nutritionPerServing.protein && (
121
+ <span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
122
+ Protein: {recipe.nutritionPerServing.protein}
123
+ </span>
124
+ )}
125
+ {recipe.nutritionPerServing.carbs && (
126
+ <span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
127
+ Carbs: {recipe.nutritionPerServing.carbs}
128
+ </span>
129
+ )}
130
+ {recipe.nutritionPerServing.fat && (
131
+ <span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
132
+ Fat: {recipe.nutritionPerServing.fat}
133
+ </span>
134
+ )}
135
+ </div>
136
+ </div>
137
+ )}
138
+ </div>
139
+ )
140
+ }
141
+
142
+ function StructuredPage() {
143
+ const [recipeName, setRecipeName] = useState('')
144
+ const [result, setResult] = useState<{
145
+ mode: Mode
146
+ recipe?: Recipe
147
+ markdown?: string
148
+ provider: string
149
+ model: string
150
+ } | null>(null)
151
+ const [isLoading, setIsLoading] = useState(false)
152
+ const [error, setError] = useState<string | null>(null)
153
+
154
+ const handleGenerate = async (mode: Mode) => {
155
+ if (!recipeName.trim()) return
156
+
157
+ setIsLoading(true)
158
+ setError(null)
159
+ setResult(null)
160
+
161
+ try {
162
+ const response = await fetch('/demo/api/ai/structured', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ recipeName, mode }),
166
+ })
167
+
168
+ const data = await response.json()
169
+
170
+ if (!response.ok) {
171
+ throw new Error(data.error || 'Failed to generate recipe')
172
+ }
173
+
174
+ setResult(data)
175
+ } catch (err: any) {
176
+ setError(err.message)
177
+ } finally {
178
+ setIsLoading(false)
179
+ }
180
+ }
181
+
182
+ const canExecute = !!(!isLoading && recipeName.trim() && !error)
183
+
184
+ return (
185
+ <div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6">
186
+ <div className="max-w-6xl mx-auto">
187
+ <div className="flex items-center gap-3 mb-6">
188
+ <ChefHat className="w-8 h-8 text-orange-500" />
189
+ <h1 className="text-2xl font-bold text-white">
190
+ One-Shot & Structured Output
191
+ </h1>
192
+ </div>
193
+
194
+ <p className="text-gray-400 mb-6">
195
+ Compare two output modes:{' '}
196
+ <strong className="text-orange-400">One-Shot</strong> returns freeform
197
+ markdown, while{' '}
198
+ <strong className="text-orange-400">Structured</strong> returns
199
+ validated JSON conforming to a Zod schema.
200
+ </p>
201
+
202
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
203
+ <div>
204
+ <label className="block text-sm font-medium text-gray-300 mb-2">
205
+ Recipe Name
206
+ </label>
207
+ <input
208
+ type="text"
209
+ value={recipeName}
210
+ onChange={(e) => setRecipeName(e.target.value)}
211
+ disabled={isLoading}
212
+ placeholder="e.g., Chocolate Chip Cookies"
213
+ className="w-full rounded-lg border border-orange-500/20 bg-gray-800 px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-orange-500/50"
214
+ />
215
+
216
+ <div className="mt-2">
217
+ <label className="block text-sm font-medium text-gray-300 mb-2">
218
+ Quick Picks
219
+ </label>
220
+ <div className="flex flex-wrap gap-2">
221
+ {SAMPLE_RECIPES.map((name) => (
222
+ <button
223
+ key={name}
224
+ onClick={() => setRecipeName(name)}
225
+ disabled={isLoading}
226
+ className="px-2 py-1 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg border border-gray-700 transition-colors"
227
+ >
228
+ {name}
229
+ </button>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <div>
236
+ <div className="grid grid-cols-2 gap-2">
237
+ <button
238
+ onClick={() => handleGenerate('oneshot')}
239
+ disabled={!canExecute}
240
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors text-white ${
241
+ !canExecute ? 'bg-gray-600' : 'bg-orange-500'
242
+ }`}
243
+ >
244
+ One-Shot (Markdown)
245
+ </button>
246
+ <button
247
+ onClick={() => handleGenerate('structured')}
248
+ disabled={!canExecute}
249
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors text-white ${
250
+ !canExecute ? 'bg-gray-600' : 'bg-blue-500'
251
+ }`}
252
+ >
253
+ Structured (JSON)
254
+ </button>
255
+ </div>
256
+ </div>
257
+ </div>
258
+
259
+ {/* Output Panel */}
260
+ <div className="mt-5 lg:col-span-2 bg-gray-800 rounded-lg p-6 border border-orange-500/20">
261
+ <div className="flex items-center justify-between mb-4">
262
+ <h2 className="text-lg font-semibold text-white">
263
+ Generated Recipe
264
+ </h2>
265
+ {result && (
266
+ <span
267
+ className={`px-2 py-1 rounded text-xs font-medium ${
268
+ result.mode === 'structured'
269
+ ? 'bg-purple-500/20 text-purple-400'
270
+ : 'bg-blue-500/20 text-blue-400'
271
+ }`}
272
+ >
273
+ {result.mode === 'structured' ? 'Structured JSON' : 'Markdown'}
274
+ </span>
275
+ )}
276
+ </div>
277
+
278
+ {error && (
279
+ <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 mb-4">
280
+ {error}
281
+ </div>
282
+ )}
283
+
284
+ {result ? (
285
+ <div className="space-y-4">
286
+ {result.mode === 'structured' && result.recipe ? (
287
+ <RecipeCard recipe={result.recipe} />
288
+ ) : result.markdown ? (
289
+ <div className="prose prose-invert max-w-none">
290
+ <Streamdown>{result.markdown}</Streamdown>
291
+ </div>
292
+ ) : null}
293
+ </div>
294
+ ) : !error && !isLoading ? (
295
+ <div className="flex flex-col items-center justify-center h-64 text-gray-500">
296
+ <ChefHat className="w-16 h-16 mb-4 opacity-50" />
297
+ <p>
298
+ Enter a recipe name and click "Generate Recipe" to get started.
299
+ </p>
300
+ </div>
301
+ ) : null}
302
+ </div>
303
+ </div>
304
+ </div>
305
+ )
306
+ }
307
+
308
+ export const Route = createFileRoute('/demo/ai-structured')({
309
+ component: StructuredPage,
310
+ })
@@ -5,8 +5,7 @@ import { openaiText } from '@tanstack/ai-openai'
5
5
  import { geminiText } from '@tanstack/ai-gemini'
6
6
  import { ollamaText } from '@tanstack/ai-ollama'
7
7
 
8
- import { getGuitars, recommendGuitarToolDef } from '@/lib/example.guitar-tools'
9
- import type { Provider } from '@/lib/model-selection'
8
+ import { getGuitars, recommendGuitarToolDef } from '@/lib/demo-guitar-tools'
10
9
 
11
10
  const SYSTEM_PROMPT = `You are a helpful assistant for a store that sells guitars.
12
11
 
@@ -25,7 +24,7 @@ IMPORTANT:
25
24
  - Do NOT describe the guitar yourself - let the recommendGuitar tool do it
26
25
  `
27
26
 
28
- export const Route = createFileRoute('/demo/api/tanchat')({
27
+ export const Route = createFileRoute('/demo/api/ai/chat')({
29
28
  server: {
30
29
  handlers: {
31
30
  POST: async ({ request }) => {
@@ -42,9 +41,20 @@ export const Route = createFileRoute('/demo/api/tanchat')({
42
41
  try {
43
42
  const body = await request.json()
44
43
  const { messages } = body
45
- const data = body.data || {}
46
- const provider: Provider = data.provider || 'anthropic'
47
- const model: string = data.model || 'claude-haiku-4-5'
44
+
45
+ // Determine the best available provider
46
+ let provider: string = 'ollama'
47
+ let model: string = 'mistral:7b'
48
+ if (process.env.ANTHROPIC_API_KEY) {
49
+ provider = 'anthropic'
50
+ model = 'claude-haiku-4-5'
51
+ } else if (process.env.OPENAI_API_KEY) {
52
+ provider = 'openai'
53
+ model = 'gpt-4o'
54
+ } else if (process.env.GEMINI_API_KEY) {
55
+ provider = 'gemini'
56
+ model = 'gemini-2.0-flash-exp'
57
+ }
48
58
 
49
59
  // Adapter factory pattern for multi-vendor support
50
60
  const adapterConfig = {
@@ -2,14 +2,12 @@ import { createFileRoute } from '@tanstack/react-router'
2
2
  import { generateImage, createImageOptions } from '@tanstack/ai'
3
3
  import { openaiImage } from '@tanstack/ai-openai'
4
4
 
5
- export const Route = createFileRoute('/demo/api/image')({
5
+ export const Route = createFileRoute('/demo/api/ai/image')({
6
6
  server: {
7
7
  handlers: {
8
8
  POST: async ({ request }) => {
9
9
  const body = await request.json()
10
10
  const { prompt, numberOfImages = 1, size = '1024x1024' } = body
11
- const data = body.data || {}
12
- const model: string = data.model || body.model || 'gpt-image-1'
13
11
 
14
12
  if (!prompt || prompt.trim().length === 0) {
15
13
  return new Response(
@@ -37,7 +35,7 @@ export const Route = createFileRoute('/demo/api/image')({
37
35
 
38
36
  try {
39
37
  const options = createImageOptions({
40
- adapter: openaiImage((model || 'gpt-image-1') as any),
38
+ adapter: openaiImage('gpt-image-1'),
41
39
  })
42
40
 
43
41
  const result = await generateImage({
@@ -50,7 +48,7 @@ export const Route = createFileRoute('/demo/api/image')({
50
48
  return new Response(
51
49
  JSON.stringify({
52
50
  images: result.images,
53
- model,
51
+ model: 'gpt-image-1',
54
52
  }),
55
53
  {
56
54
  status: 200,
@@ -0,0 +1,136 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { chat } from '@tanstack/ai'
3
+ import { openaiText } from '@tanstack/ai-openai'
4
+ import { z } from 'zod'
5
+
6
+ // Schema for structured recipe output
7
+ const RecipeSchema = z.object({
8
+ name: z.string().describe('The name of the recipe'),
9
+ description: z.string().describe('A brief description of the dish'),
10
+ prepTime: z.string().describe('Preparation time (e.g., "15 minutes")'),
11
+ cookTime: z.string().describe('Cooking time (e.g., "30 minutes")'),
12
+ servings: z.number().describe('Number of servings'),
13
+ difficulty: z.enum(['easy', 'medium', 'hard']).describe('Difficulty level'),
14
+ ingredients: z
15
+ .array(
16
+ z.object({
17
+ item: z.string().describe('Ingredient name'),
18
+ amount: z.string().describe('Amount needed (e.g., "2 cups")'),
19
+ notes: z.string().optional().describe('Optional preparation notes'),
20
+ }),
21
+ )
22
+ .describe('List of ingredients'),
23
+ instructions: z
24
+ .array(z.string())
25
+ .describe('Step-by-step cooking instructions'),
26
+ tips: z.array(z.string()).optional().describe('Optional cooking tips'),
27
+ nutritionPerServing: z
28
+ .object({
29
+ calories: z.number().optional(),
30
+ protein: z.string().optional(),
31
+ carbs: z.string().optional(),
32
+ fat: z.string().optional(),
33
+ })
34
+ .optional()
35
+ .describe('Nutritional information per serving'),
36
+ })
37
+
38
+ export type Recipe = z.infer<typeof RecipeSchema>
39
+
40
+ export const Route = createFileRoute('/demo/api/ai/structured')({
41
+ server: {
42
+ handlers: {
43
+ POST: async ({ request }) => {
44
+ const body = await request.json()
45
+ const { recipeName, mode = 'structured' } = body
46
+
47
+ if (!recipeName || recipeName.trim().length === 0) {
48
+ return new Response(
49
+ JSON.stringify({
50
+ error: 'Recipe name is required',
51
+ }),
52
+ {
53
+ status: 400,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ },
56
+ )
57
+ }
58
+
59
+ try {
60
+ if (mode === 'structured') {
61
+ // Structured output mode - returns validated object
62
+ const result = await chat({
63
+ adapter: openaiText('gpt-4o'),
64
+ messages: [
65
+ {
66
+ role: 'user',
67
+ content: `Generate a complete recipe for: ${recipeName}. Include all ingredients with amounts, step-by-step instructions, prep/cook times, and difficulty level.`,
68
+ },
69
+ ],
70
+ outputSchema: RecipeSchema,
71
+ } as any)
72
+
73
+ return new Response(
74
+ JSON.stringify({
75
+ mode: 'structured',
76
+ recipe: result,
77
+ provider: 'openai',
78
+ model: 'gpt-4o',
79
+ }),
80
+ {
81
+ status: 200,
82
+ headers: { 'Content-Type': 'application/json' },
83
+ },
84
+ )
85
+ } else {
86
+ // One-shot markdown mode - returns text
87
+ const markdown = await chat({
88
+ adapter: openaiText('gpt-4o'),
89
+ stream: false,
90
+ messages: [
91
+ {
92
+ role: 'user',
93
+ content: `Generate a complete recipe for: ${recipeName}.
94
+
95
+ Format the recipe in beautiful markdown with:
96
+ - A title with the recipe name
97
+ - A brief description
98
+ - Prep time, cook time, and servings
99
+ - Ingredients list with amounts
100
+ - Numbered step-by-step instructions
101
+ - Optional tips section
102
+ - Nutritional info if applicable
103
+
104
+ Make it detailed and easy to follow.`,
105
+ },
106
+ ],
107
+ } as any)
108
+
109
+ return new Response(
110
+ JSON.stringify({
111
+ mode: 'oneshot',
112
+ markdown,
113
+ provider: 'openai',
114
+ model: 'gpt-4o',
115
+ }),
116
+ {
117
+ status: 200,
118
+ headers: { 'Content-Type': 'application/json' },
119
+ },
120
+ )
121
+ }
122
+ } catch (error: any) {
123
+ return new Response(
124
+ JSON.stringify({
125
+ error: error.message || 'An error occurred',
126
+ }),
127
+ {
128
+ status: 500,
129
+ headers: { 'Content-Type': 'application/json' },
130
+ },
131
+ )
132
+ }
133
+ },
134
+ },
135
+ },
136
+ })
@@ -0,0 +1,89 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { generateTranscription } from '@tanstack/ai'
3
+ import { openaiTranscription } from '@tanstack/ai-openai'
4
+
5
+ export const Route = createFileRoute('/demo/api/ai/transcription')({
6
+ server: {
7
+ handlers: {
8
+ POST: async ({ request }) => {
9
+ const formData = await request.formData()
10
+ const audioFile = formData.get('audio') as File | null
11
+ const audioBase64 = formData.get('audioBase64') as string | null
12
+ const model = (formData.get('model') as string) || 'whisper-1'
13
+ const language = formData.get('language') as string | null
14
+ const responseFormat = formData.get('responseFormat') as string | null
15
+
16
+ if (!audioFile && !audioBase64) {
17
+ return new Response(
18
+ JSON.stringify({
19
+ error: 'Audio file or base64 data is required',
20
+ }),
21
+ {
22
+ status: 400,
23
+ headers: { 'Content-Type': 'application/json' },
24
+ },
25
+ )
26
+ }
27
+
28
+ if (!process.env.OPENAI_API_KEY) {
29
+ return new Response(
30
+ JSON.stringify({
31
+ error: 'OPENAI_API_KEY is not configured',
32
+ }),
33
+ {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ },
37
+ )
38
+ }
39
+
40
+ try {
41
+ const adapter = openaiTranscription(model as any)
42
+
43
+ // Prepare audio data
44
+ let audioData: string | File
45
+ if (audioFile) {
46
+ audioData = audioFile
47
+ } else if (audioBase64) {
48
+ audioData = audioBase64
49
+ } else {
50
+ throw new Error('No audio data provided')
51
+ }
52
+
53
+ const result = await generateTranscription({
54
+ adapter,
55
+ audio: audioData,
56
+ language: language || undefined,
57
+ responseFormat: (responseFormat as any) || 'verbose_json',
58
+ })
59
+
60
+ return new Response(
61
+ JSON.stringify({
62
+ id: result.id,
63
+ model: result.model,
64
+ text: result.text,
65
+ language: result.language,
66
+ duration: result.duration,
67
+ segments: result.segments,
68
+ words: result.words,
69
+ }),
70
+ {
71
+ status: 200,
72
+ headers: { 'Content-Type': 'application/json' },
73
+ },
74
+ )
75
+ } catch (error: any) {
76
+ return new Response(
77
+ JSON.stringify({
78
+ error: error.message || 'An error occurred',
79
+ }),
80
+ {
81
+ status: 500,
82
+ headers: { 'Content-Type': 'application/json' },
83
+ },
84
+ )
85
+ }
86
+ },
87
+ },
88
+ },
89
+ })
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
2
2
  import { generateSpeech } from '@tanstack/ai'
3
3
  import { openaiSpeech } from '@tanstack/ai-openai'
4
4
 
5
- export const Route = createFileRoute('/demo/api/tts')({
5
+ export const Route = createFileRoute('/demo/api/ai/tts')({
6
6
  server: {
7
7
  handlers: {
8
8
  POST: async ({ request }) => {
@@ -1,7 +1,8 @@
1
1
  import { Link, createFileRoute } from '@tanstack/react-router'
2
- import guitars from '../../data/example-guitars'
3
2
 
4
- export const Route = createFileRoute('/example/guitars/$guitarId')({
3
+ import guitars from '@/data/demo-guitars'
4
+
5
+ export const Route = createFileRoute('/demo/guitars/$guitarId')({
5
6
  component: RouteComponent,
6
7
  loader: async ({ params }) => {
7
8
  const guitar = guitars.find((guitar) => guitar.id === +params.guitarId)
@@ -1,7 +1,8 @@
1
1
  import { Link, createFileRoute } from '@tanstack/react-router'
2
- import guitars from '../../data/example-guitars'
3
2
 
4
- export const Route = createFileRoute('/example/guitars/')({
3
+ import guitars from '@/data/demo-guitars'
4
+
5
+ export const Route = createFileRoute('/demo/guitars/')({
5
6
  component: GuitarsIndex,
6
7
  })
7
8