@tanstack/cta-framework-react-cra 0.43.1 → 0.44.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/add-ons/apollo-client/README.md +150 -0
- package/add-ons/apollo-client/assets/src/routes/demo.apollo-client.tsx +75 -0
- package/add-ons/apollo-client/info.json +19 -0
- package/add-ons/apollo-client/package.json +8 -0
- package/add-ons/apollo-client/small-logo.svg +11 -0
- package/add-ons/convex/package.json +2 -2
- package/add-ons/db/assets/src/hooks/demo.useChat.ts +1 -1
- package/add-ons/db/assets/src/routes/demo/db-chat-api.ts +4 -1
- package/add-ons/db/package.json +1 -1
- package/add-ons/mcp/package.json +1 -1
- package/add-ons/neon/package.json +1 -1
- package/add-ons/sentry/assets/instrument.server.mjs +16 -9
- package/add-ons/sentry/assets/src/routes/demo/sentry.testing.tsx +42 -2
- package/add-ons/shadcn/package.json +1 -1
- package/add-ons/start/assets/src/router.tsx.ejs +34 -10
- package/add-ons/start/package.json +2 -2
- package/add-ons/store/package.json +3 -3
- package/add-ons/storybook/package.json +2 -2
- package/examples/tanchat/assets/src/hooks/useAudioRecorder.ts +85 -0
- package/examples/tanchat/assets/src/hooks/useTTS.ts +78 -0
- package/examples/tanchat/assets/src/lib/model-selection.ts +78 -0
- package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +55 -0
- package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +35 -0
- package/examples/tanchat/assets/src/routes/demo/api.image.ts +74 -0
- package/examples/tanchat/assets/src/routes/demo/api.structured.ts +168 -0
- package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts +89 -0
- package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +89 -0
- package/examples/tanchat/assets/src/routes/demo/api.tts.ts +81 -0
- package/examples/tanchat/assets/src/routes/demo/image.tsx +257 -0
- package/examples/tanchat/assets/src/routes/demo/structured.tsx +460 -0
- package/examples/tanchat/assets/src/routes/demo/tanchat.css +14 -7
- package/examples/tanchat/assets/src/routes/demo/tanchat.tsx +301 -81
- package/examples/tanchat/info.json +10 -7
- package/examples/tanchat/package.json +8 -5
- package/package.json +2 -2
- package/project/base/src/routes/__root.tsx.ejs +14 -6
- package/tests/react-cra.test.ts +14 -0
- package/tests/snapshots/react-cra/cr-ts-start-apollo-client-npm.json +31 -0
- package/tests/snapshots/react-cra/cr-ts-start-npm.json +2 -2
- package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +2 -2
- package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts.ejs +0 -72
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ChefHat, Loader2, Clock, Users, Gauge } from 'lucide-react'
|
|
4
|
+
import { Streamdown } from 'streamdown'
|
|
5
|
+
|
|
6
|
+
import type { Recipe } from './api.structured'
|
|
7
|
+
import type { Provider } from '@/lib/model-selection'
|
|
8
|
+
|
|
9
|
+
import { MODEL_OPTIONS } from '@/lib/model-selection'
|
|
10
|
+
|
|
11
|
+
type Mode = 'structured' | 'oneshot'
|
|
12
|
+
|
|
13
|
+
interface StructuredProvider {
|
|
14
|
+
id: Provider
|
|
15
|
+
name: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ALL_PROVIDERS: Array<StructuredProvider> = [
|
|
19
|
+
{ id: 'openai', name: 'OpenAI (GPT-4o)' },
|
|
20
|
+
{ id: 'anthropic', name: 'Anthropic (Claude Sonnet)' },
|
|
21
|
+
{ id: 'gemini', name: 'Gemini (2.0 Flash)' },
|
|
22
|
+
{ id: 'ollama', name: 'Ollama (Mistral 7B)' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const SAMPLE_RECIPES = [
|
|
26
|
+
'Homemade Margherita Pizza',
|
|
27
|
+
'Thai Green Curry',
|
|
28
|
+
'Classic Beef Bourguignon',
|
|
29
|
+
'Chocolate Lava Cake',
|
|
30
|
+
'Crispy Korean Fried Chicken',
|
|
31
|
+
'Fresh Spring Rolls with Peanut Sauce',
|
|
32
|
+
'Creamy Mushroom Risotto',
|
|
33
|
+
'Authentic Pad Thai',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
function RecipeCard({ recipe }: { recipe: Recipe }) {
|
|
37
|
+
const difficultyColors = {
|
|
38
|
+
easy: 'bg-green-500/20 text-green-400',
|
|
39
|
+
medium: 'bg-yellow-500/20 text-yellow-400',
|
|
40
|
+
hard: 'bg-red-500/20 text-red-400',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="space-y-6">
|
|
45
|
+
{/* Header */}
|
|
46
|
+
<div>
|
|
47
|
+
<h3 className="text-2xl font-bold text-white mb-2">{recipe.name}</h3>
|
|
48
|
+
<p className="text-gray-400">{recipe.description}</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Meta info */}
|
|
52
|
+
<div className="flex flex-wrap gap-4">
|
|
53
|
+
<div className="flex items-center gap-2 text-gray-300">
|
|
54
|
+
<Clock className="w-4 h-4 text-orange-400" />
|
|
55
|
+
<span className="text-sm">Prep: {recipe.prepTime}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex items-center gap-2 text-gray-300">
|
|
58
|
+
<Clock className="w-4 h-4 text-orange-400" />
|
|
59
|
+
<span className="text-sm">Cook: {recipe.cookTime}</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex items-center gap-2 text-gray-300">
|
|
62
|
+
<Users className="w-4 h-4 text-orange-400" />
|
|
63
|
+
<span className="text-sm">{recipe.servings} servings</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div
|
|
66
|
+
className={`flex items-center gap-2 px-2 py-1 rounded-full ${difficultyColors[recipe.difficulty]}`}
|
|
67
|
+
>
|
|
68
|
+
<Gauge className="w-4 h-4" />
|
|
69
|
+
<span className="text-sm capitalize">{recipe.difficulty}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Ingredients */}
|
|
74
|
+
<div>
|
|
75
|
+
<h4 className="text-lg font-semibold text-white mb-3">Ingredients</h4>
|
|
76
|
+
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
77
|
+
{recipe.ingredients.map((ing, idx) => (
|
|
78
|
+
<li key={idx} className="flex items-start gap-2 text-gray-300">
|
|
79
|
+
<span className="text-orange-400">•</span>
|
|
80
|
+
<span>
|
|
81
|
+
<span className="font-medium">{ing.amount}</span> {ing.item}
|
|
82
|
+
{ing.notes && (
|
|
83
|
+
<span className="text-gray-500 text-sm"> ({ing.notes})</span>
|
|
84
|
+
)}
|
|
85
|
+
</span>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Instructions */}
|
|
92
|
+
<div>
|
|
93
|
+
<h4 className="text-lg font-semibold text-white mb-3">Instructions</h4>
|
|
94
|
+
<ol className="space-y-3">
|
|
95
|
+
{recipe.instructions.map((step, idx) => (
|
|
96
|
+
<li key={idx} className="flex gap-3 text-gray-300">
|
|
97
|
+
<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">
|
|
98
|
+
{idx + 1}
|
|
99
|
+
</span>
|
|
100
|
+
<span>{step}</span>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ol>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Tips */}
|
|
107
|
+
{recipe.tips && recipe.tips.length > 0 && (
|
|
108
|
+
<div>
|
|
109
|
+
<h4 className="text-lg font-semibold text-white mb-3">Tips</h4>
|
|
110
|
+
<ul className="space-y-2">
|
|
111
|
+
{recipe.tips.map((tip, idx) => (
|
|
112
|
+
<li key={idx} className="flex items-start gap-2 text-gray-300">
|
|
113
|
+
<span className="text-yellow-400">*</span>
|
|
114
|
+
<span>{tip}</span>
|
|
115
|
+
</li>
|
|
116
|
+
))}
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Nutrition */}
|
|
122
|
+
{recipe.nutritionPerServing && (
|
|
123
|
+
<div>
|
|
124
|
+
<h4 className="text-lg font-semibold text-white mb-3">
|
|
125
|
+
Nutrition (per serving)
|
|
126
|
+
</h4>
|
|
127
|
+
<div className="flex flex-wrap gap-4 text-sm">
|
|
128
|
+
{recipe.nutritionPerServing.calories && (
|
|
129
|
+
<span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
|
|
130
|
+
{recipe.nutritionPerServing.calories} cal
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
{recipe.nutritionPerServing.protein && (
|
|
134
|
+
<span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
|
|
135
|
+
Protein: {recipe.nutritionPerServing.protein}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
{recipe.nutritionPerServing.carbs && (
|
|
139
|
+
<span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
|
|
140
|
+
Carbs: {recipe.nutritionPerServing.carbs}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
{recipe.nutritionPerServing.fat && (
|
|
144
|
+
<span className="px-3 py-1 bg-gray-700 rounded-full text-gray-300">
|
|
145
|
+
Fat: {recipe.nutritionPerServing.fat}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function StructuredPage() {
|
|
156
|
+
const [recipeName, setRecipeName] = useState('')
|
|
157
|
+
const [provider, setProvider] = useState<Provider>('openai')
|
|
158
|
+
const [mode, setMode] = useState<Mode>('structured')
|
|
159
|
+
const [result, setResult] = useState<{
|
|
160
|
+
mode: Mode
|
|
161
|
+
recipe?: Recipe
|
|
162
|
+
markdown?: string
|
|
163
|
+
provider: string
|
|
164
|
+
model: string
|
|
165
|
+
} | null>(null)
|
|
166
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
167
|
+
const [error, setError] = useState<string | null>(null)
|
|
168
|
+
const [availableProviders, setAvailableProviders] = useState<Provider[]>([])
|
|
169
|
+
const [isCheckingProviders, setIsCheckingProviders] = useState(true)
|
|
170
|
+
|
|
171
|
+
// Fetch available providers
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
fetch('/demo/api/available-providers')
|
|
174
|
+
.then((res) => res.json())
|
|
175
|
+
.then((data) => {
|
|
176
|
+
setAvailableProviders(data.providers)
|
|
177
|
+
// Set default provider to first available
|
|
178
|
+
if (data.providers.length > 0 && !data.providers.includes(provider)) {
|
|
179
|
+
setProvider(data.providers[0])
|
|
180
|
+
}
|
|
181
|
+
setIsCheckingProviders(false)
|
|
182
|
+
})
|
|
183
|
+
.catch(() => {
|
|
184
|
+
setAvailableProviders(['ollama'])
|
|
185
|
+
setProvider('ollama')
|
|
186
|
+
setIsCheckingProviders(false)
|
|
187
|
+
})
|
|
188
|
+
}, [])
|
|
189
|
+
|
|
190
|
+
const filteredProviders = ALL_PROVIDERS.filter((p) =>
|
|
191
|
+
availableProviders.includes(p.id),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const handleGenerate = async () => {
|
|
195
|
+
if (!recipeName.trim()) return
|
|
196
|
+
|
|
197
|
+
setIsLoading(true)
|
|
198
|
+
setError(null)
|
|
199
|
+
setResult(null)
|
|
200
|
+
|
|
201
|
+
// Get the model for the selected provider
|
|
202
|
+
const modelOption = MODEL_OPTIONS.find((m) => m.provider === provider)
|
|
203
|
+
const model = modelOption?.model
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch('/demo/api/structured', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'Content-Type': 'application/json' },
|
|
209
|
+
body: JSON.stringify({ recipeName, provider, model, mode }),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const data = await response.json()
|
|
213
|
+
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw new Error(data.error || 'Failed to generate recipe')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setResult(data)
|
|
219
|
+
} catch (err: any) {
|
|
220
|
+
setError(err.message)
|
|
221
|
+
} finally {
|
|
222
|
+
setIsLoading(false)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (isCheckingProviders) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6 flex items-center justify-center">
|
|
229
|
+
<Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (filteredProviders.length === 0) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6">
|
|
237
|
+
<div className="max-w-2xl mx-auto text-center py-16">
|
|
238
|
+
<ChefHat className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
|
239
|
+
<h1 className="text-2xl font-bold text-white mb-4">
|
|
240
|
+
No Providers Available
|
|
241
|
+
</h1>
|
|
242
|
+
<p className="text-gray-400 mb-4">
|
|
243
|
+
Please configure at least one AI provider in your{' '}
|
|
244
|
+
<code className="text-orange-400">.env.local</code> file, or ensure
|
|
245
|
+
Ollama is running locally.
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6">
|
|
254
|
+
<div className="max-w-6xl mx-auto">
|
|
255
|
+
<div className="flex items-center gap-3 mb-6">
|
|
256
|
+
<ChefHat className="w-8 h-8 text-orange-500" />
|
|
257
|
+
<h1 className="text-2xl font-bold text-white">
|
|
258
|
+
One-Shot & Structured Output
|
|
259
|
+
</h1>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<p className="text-gray-400 mb-6">
|
|
263
|
+
Compare two output modes:{' '}
|
|
264
|
+
<strong className="text-orange-400">One-Shot</strong> returns freeform
|
|
265
|
+
markdown, while{' '}
|
|
266
|
+
<strong className="text-orange-400">Structured</strong> returns
|
|
267
|
+
validated JSON conforming to a Zod schema.
|
|
268
|
+
</p>
|
|
269
|
+
|
|
270
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
271
|
+
{/* Input Panel */}
|
|
272
|
+
<div className="space-y-4">
|
|
273
|
+
<div>
|
|
274
|
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
275
|
+
Provider
|
|
276
|
+
</label>
|
|
277
|
+
<select
|
|
278
|
+
value={provider}
|
|
279
|
+
onChange={(e) => setProvider(e.target.value as Provider)}
|
|
280
|
+
disabled={isLoading}
|
|
281
|
+
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"
|
|
282
|
+
>
|
|
283
|
+
{filteredProviders.map((p) => (
|
|
284
|
+
<option key={p.id} value={p.id}>
|
|
285
|
+
{p.name}
|
|
286
|
+
</option>
|
|
287
|
+
))}
|
|
288
|
+
</select>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div>
|
|
292
|
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
293
|
+
Output Mode
|
|
294
|
+
</label>
|
|
295
|
+
<div className="grid grid-cols-2 gap-2">
|
|
296
|
+
<button
|
|
297
|
+
onClick={() => setMode('oneshot')}
|
|
298
|
+
disabled={isLoading}
|
|
299
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
300
|
+
mode === 'oneshot'
|
|
301
|
+
? 'bg-orange-500 text-white'
|
|
302
|
+
: 'bg-gray-800 text-gray-400 hover:text-white border border-orange-500/20'
|
|
303
|
+
}`}
|
|
304
|
+
>
|
|
305
|
+
One-Shot (Markdown)
|
|
306
|
+
</button>
|
|
307
|
+
<button
|
|
308
|
+
onClick={() => setMode('structured')}
|
|
309
|
+
disabled={isLoading}
|
|
310
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
311
|
+
mode === 'structured'
|
|
312
|
+
? 'bg-orange-500 text-white'
|
|
313
|
+
: 'bg-gray-800 text-gray-400 hover:text-white border border-orange-500/20'
|
|
314
|
+
}`}
|
|
315
|
+
>
|
|
316
|
+
Structured (JSON)
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div>
|
|
322
|
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
323
|
+
Recipe Name
|
|
324
|
+
</label>
|
|
325
|
+
<input
|
|
326
|
+
type="text"
|
|
327
|
+
value={recipeName}
|
|
328
|
+
onChange={(e) => setRecipeName(e.target.value)}
|
|
329
|
+
disabled={isLoading}
|
|
330
|
+
placeholder="e.g., Chocolate Chip Cookies"
|
|
331
|
+
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"
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div>
|
|
336
|
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
337
|
+
Quick Picks
|
|
338
|
+
</label>
|
|
339
|
+
<div className="flex flex-wrap gap-2">
|
|
340
|
+
{SAMPLE_RECIPES.map((name) => (
|
|
341
|
+
<button
|
|
342
|
+
key={name}
|
|
343
|
+
onClick={() => setRecipeName(name)}
|
|
344
|
+
disabled={isLoading}
|
|
345
|
+
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"
|
|
346
|
+
>
|
|
347
|
+
{name}
|
|
348
|
+
</button>
|
|
349
|
+
))}
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<button
|
|
354
|
+
onClick={handleGenerate}
|
|
355
|
+
disabled={isLoading || !recipeName.trim()}
|
|
356
|
+
className="w-full px-4 py-3 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-600 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
|
357
|
+
>
|
|
358
|
+
{isLoading ? (
|
|
359
|
+
<>
|
|
360
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
361
|
+
Generating...
|
|
362
|
+
</>
|
|
363
|
+
) : (
|
|
364
|
+
'Generate Recipe'
|
|
365
|
+
)}
|
|
366
|
+
</button>
|
|
367
|
+
|
|
368
|
+
{/* Schema Preview */}
|
|
369
|
+
<div className="mt-6 p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
|
370
|
+
<h4 className="text-sm font-medium text-gray-400 mb-2">
|
|
371
|
+
Structured Output Schema
|
|
372
|
+
</h4>
|
|
373
|
+
<pre className="text-xs text-gray-500 overflow-x-auto">
|
|
374
|
+
{`z.object({
|
|
375
|
+
name: z.string(),
|
|
376
|
+
description: z.string(),
|
|
377
|
+
prepTime: z.string(),
|
|
378
|
+
cookTime: z.string(),
|
|
379
|
+
servings: z.number(),
|
|
380
|
+
difficulty: z.enum(['easy', 'medium', 'hard']),
|
|
381
|
+
ingredients: z.array(z.object({
|
|
382
|
+
item: z.string(),
|
|
383
|
+
amount: z.string(),
|
|
384
|
+
notes: z.string().optional(),
|
|
385
|
+
})),
|
|
386
|
+
instructions: z.array(z.string()),
|
|
387
|
+
tips: z.array(z.string()).optional(),
|
|
388
|
+
nutritionPerServing: z.object({...}).optional(),
|
|
389
|
+
})`}
|
|
390
|
+
</pre>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
{/* Output Panel */}
|
|
395
|
+
<div className="lg:col-span-2 bg-gray-800 rounded-lg p-6 border border-orange-500/20">
|
|
396
|
+
<div className="flex items-center justify-between mb-4">
|
|
397
|
+
<h2 className="text-lg font-semibold text-white">
|
|
398
|
+
Generated Recipe
|
|
399
|
+
</h2>
|
|
400
|
+
{result && (
|
|
401
|
+
<span
|
|
402
|
+
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
403
|
+
result.mode === 'structured'
|
|
404
|
+
? 'bg-purple-500/20 text-purple-400'
|
|
405
|
+
: 'bg-blue-500/20 text-blue-400'
|
|
406
|
+
}`}
|
|
407
|
+
>
|
|
408
|
+
{result.mode === 'structured'
|
|
409
|
+
? 'Structured JSON'
|
|
410
|
+
: 'Markdown'}
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{error && (
|
|
416
|
+
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 mb-4">
|
|
417
|
+
{error}
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
{result ? (
|
|
422
|
+
<div className="space-y-4">
|
|
423
|
+
{result.mode === 'structured' && result.recipe ? (
|
|
424
|
+
<RecipeCard recipe={result.recipe} />
|
|
425
|
+
) : result.markdown ? (
|
|
426
|
+
<div className="prose prose-invert max-w-none">
|
|
427
|
+
<Streamdown>{result.markdown}</Streamdown>
|
|
428
|
+
</div>
|
|
429
|
+
) : null}
|
|
430
|
+
|
|
431
|
+
<div className="pt-4 border-t border-gray-700 text-sm text-gray-400">
|
|
432
|
+
<p>
|
|
433
|
+
Provider:{' '}
|
|
434
|
+
<span className="text-orange-400">{result.provider}</span>
|
|
435
|
+
</p>
|
|
436
|
+
<p>
|
|
437
|
+
Model:{' '}
|
|
438
|
+
<span className="text-orange-400">{result.model}</span>
|
|
439
|
+
</p>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
) : !error && !isLoading ? (
|
|
443
|
+
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
|
444
|
+
<ChefHat className="w-16 h-16 mb-4 opacity-50" />
|
|
445
|
+
<p>
|
|
446
|
+
Enter a recipe name and click "Generate Recipe" to get
|
|
447
|
+
started.
|
|
448
|
+
</p>
|
|
449
|
+
</div>
|
|
450
|
+
) : null}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export const Route = createFileRoute('/demo/structured')({
|
|
459
|
+
component: StructuredPage,
|
|
460
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
@import
|
|
2
|
-
@import
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import 'highlight.js/styles/github-dark.css';
|
|
3
3
|
@source "../../../../node_modules/streamdown/dist/*.js";
|
|
4
4
|
|
|
5
5
|
/* Custom scrollbar styles */
|
|
@@ -59,13 +59,17 @@ html {
|
|
|
59
59
|
color: inherit;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
.prose h1,
|
|
62
|
+
.prose h1,
|
|
63
|
+
.prose h2,
|
|
64
|
+
.prose h3,
|
|
65
|
+
.prose h4 {
|
|
63
66
|
color: #f9fafb; /* text-gray-50 */
|
|
64
67
|
/* margin-top: 2em; */
|
|
65
68
|
/* margin-bottom: 1em; */
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
.prose ul,
|
|
71
|
+
.prose ul,
|
|
72
|
+
.prose ol {
|
|
69
73
|
margin-top: 1.25em;
|
|
70
74
|
margin-bottom: 1.25em;
|
|
71
75
|
padding-left: 1.625em;
|
|
@@ -106,7 +110,8 @@ html {
|
|
|
106
110
|
margin: 1.25em 0;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
.prose th,
|
|
113
|
+
.prose th,
|
|
114
|
+
.prose td {
|
|
110
115
|
padding: 0.75em;
|
|
111
116
|
border: 1px solid rgba(249, 115, 22, 0.2);
|
|
112
117
|
}
|
|
@@ -125,7 +130,9 @@ html {
|
|
|
125
130
|
.message-enter-active {
|
|
126
131
|
opacity: 1;
|
|
127
132
|
transform: translateY(0);
|
|
128
|
-
transition:
|
|
133
|
+
transition:
|
|
134
|
+
opacity 300ms,
|
|
135
|
+
transform 300ms;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
.message-exit {
|
|
@@ -218,4 +225,4 @@ html {
|
|
|
218
225
|
background-color: transparent;
|
|
219
226
|
padding: 0;
|
|
220
227
|
border-radius: 0;
|
|
221
|
-
}
|
|
228
|
+
}
|