@tanstack/cta-framework-react-cra 0.43.0 → 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.
Files changed (48) hide show
  1. package/add-ons/apollo-client/README.md +150 -0
  2. package/add-ons/apollo-client/assets/src/routes/demo.apollo-client.tsx +75 -0
  3. package/add-ons/apollo-client/info.json +19 -0
  4. package/add-ons/apollo-client/package.json +8 -0
  5. package/add-ons/apollo-client/small-logo.svg +11 -0
  6. package/add-ons/convex/package.json +2 -2
  7. package/add-ons/db/assets/src/hooks/demo.useChat.ts +1 -1
  8. package/add-ons/db/assets/src/routes/demo/db-chat-api.ts +4 -1
  9. package/add-ons/db/package.json +1 -1
  10. package/add-ons/mcp/package.json +1 -1
  11. package/add-ons/neon/package.json +1 -1
  12. package/add-ons/prisma/package.json.ejs +1 -1
  13. package/add-ons/sentry/assets/instrument.server.mjs +16 -9
  14. package/add-ons/sentry/assets/src/routes/demo/sentry.testing.tsx +42 -2
  15. package/add-ons/shadcn/package.json +1 -1
  16. package/add-ons/start/assets/src/router.tsx.ejs +34 -10
  17. package/add-ons/start/package.json +2 -2
  18. package/add-ons/store/package.json +3 -3
  19. package/add-ons/storybook/package.json +2 -2
  20. package/dist/index.js +0 -3
  21. package/dist/types/index.d.ts +0 -2
  22. package/examples/tanchat/assets/src/hooks/useAudioRecorder.ts +85 -0
  23. package/examples/tanchat/assets/src/hooks/useTTS.ts +78 -0
  24. package/examples/tanchat/assets/src/lib/model-selection.ts +78 -0
  25. package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +55 -0
  26. package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +35 -0
  27. package/examples/tanchat/assets/src/routes/demo/api.image.ts +74 -0
  28. package/examples/tanchat/assets/src/routes/demo/api.structured.ts +168 -0
  29. package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts +89 -0
  30. package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +89 -0
  31. package/examples/tanchat/assets/src/routes/demo/api.tts.ts +81 -0
  32. package/examples/tanchat/assets/src/routes/demo/image.tsx +257 -0
  33. package/examples/tanchat/assets/src/routes/demo/structured.tsx +460 -0
  34. package/examples/tanchat/assets/src/routes/demo/tanchat.css +14 -7
  35. package/examples/tanchat/assets/src/routes/demo/tanchat.tsx +301 -81
  36. package/examples/tanchat/info.json +10 -7
  37. package/examples/tanchat/package.json +8 -5
  38. package/package.json +2 -3
  39. package/project/base/src/routes/__root.tsx.ejs +14 -6
  40. package/src/index.ts +0 -5
  41. package/tests/react-cra.test.ts +14 -0
  42. package/tests/snapshots/react-cra/cr-ts-start-apollo-client-npm.json +31 -0
  43. package/tests/snapshots/react-cra/cr-ts-start-npm.json +2 -2
  44. package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +2 -2
  45. package/dist/checksum.js +0 -3
  46. package/dist/types/checksum.d.ts +0 -1
  47. package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts.ejs +0 -72
  48. package/src/checksum.ts +0 -3
@@ -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 "tailwindcss";
2
- @import "highlight.js/styles/github-dark.css";
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, .prose h2, .prose h3, .prose h4 {
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, .prose ol {
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, .prose td {
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: opacity 300ms, transform 300ms;
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
+ }