@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.
- package/{examples/tanchat/assets/src/components/example-AIAssistant.tsx → add-ons/ai/assets/src/components/demo-AIAssistant.tsx} +6 -8
- package/{examples/tanchat/assets/src/components/example-GuitarRecommendation.tsx → add-ons/ai/assets/src/components/demo-GuitarRecommendation.tsx} +2 -2
- package/{examples/tanchat/assets/src/lib/example.ai-hook.ts → add-ons/ai/assets/src/lib/demo-ai-hook.ts} +9 -9
- package/{examples/tanchat/assets/src/lib/example.guitar-tools.ts → add-ons/ai/assets/src/lib/demo-guitar-tools.ts} +1 -1
- package/{examples/tanchat/assets/src/routes/demo/tanchat.tsx → add-ons/ai/assets/src/routes/demo/ai-chat.tsx} +28 -148
- package/{examples/tanchat/assets/src/routes/demo/image.tsx → add-ons/ai/assets/src/routes/demo/ai-image.tsx} +2 -50
- package/add-ons/ai/assets/src/routes/demo/ai-structured.tsx +310 -0
- package/{examples/tanchat/assets/src/routes/demo/api.tanchat.ts → add-ons/ai/assets/src/routes/demo/api.ai.chat.ts} +16 -6
- package/{examples/tanchat/assets/src/routes/demo/api.image.ts → add-ons/ai/assets/src/routes/demo/api.ai.image.ts} +3 -5
- package/add-ons/ai/assets/src/routes/demo/api.ai.structured.ts +136 -0
- package/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts +89 -0
- package/{examples/tanchat/assets/src/routes/demo/api.tts.ts → add-ons/ai/assets/src/routes/demo/api.ai.tts.ts} +1 -1
- package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/$guitarId.tsx +3 -2
- package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/index.tsx +3 -2
- package/add-ons/ai/info.json +46 -0
- package/{examples/tanchat → add-ons/ai}/package.json +1 -1
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/RemyButton.tsx +18 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +121 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +194 -0
- package/examples/events/info.json +63 -0
- package/examples/events/package.json +23 -0
- package/package.json +2 -2
- package/examples/tanchat/assets/src/lib/model-selection.ts +0 -78
- package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +0 -55
- package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +0 -35
- package/examples/tanchat/assets/src/routes/demo/api.structured.ts +0 -168
- package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +0 -89
- package/examples/tanchat/assets/src/routes/demo/structured.tsx +0 -460
- package/examples/tanchat/info.json +0 -46
- /package/{examples/tanchat → add-ons/ai}/README.md +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/_dot_env.local.append +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-flowers.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-motherboard.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-racing.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-steamer-trunk.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-superhero.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-traveling.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-video-games.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-ukelele-tanstack.jpg +0 -0
- /package/{examples/tanchat/assets/src/data/example-guitars.ts → add-ons/ai/assets/src/data/demo-guitars.ts} +0 -0
- /package/{examples/tanchat/assets/src/hooks/useAudioRecorder.ts → add-ons/ai/assets/src/hooks/demo-useAudioRecorder.ts} +0 -0
- /package/{examples/tanchat/assets/src/hooks/useTTS.ts → add-ons/ai/assets/src/hooks/demo-useTTS.ts} +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/src/lib/ai-devtools.tsx +0 -0
- /package/{examples/tanchat/assets/src/routes/demo/tanchat.css → add-ons/ai/assets/src/routes/demo/ai-chat.css} +0 -0
- /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/
|
|
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/
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
import guitars from '@/data/demo-guitars'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/demo/guitars/')({
|
|
5
6
|
component: GuitarsIndex,
|
|
6
7
|
})
|
|
7
8
|
|