@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
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Send,
|
|
5
|
+
Square,
|
|
6
|
+
Mic,
|
|
7
|
+
MicOff,
|
|
8
|
+
Volume2,
|
|
9
|
+
VolumeX,
|
|
10
|
+
Loader2,
|
|
11
|
+
} from 'lucide-react'
|
|
4
12
|
import { Streamdown } from 'streamdown'
|
|
5
13
|
|
|
6
14
|
import { useGuitarRecommendationChat } from '@/lib/example.ai-hook'
|
|
7
15
|
import type { ChatMessages } from '@/lib/example.ai-hook'
|
|
16
|
+
import {
|
|
17
|
+
MODEL_OPTIONS,
|
|
18
|
+
getAvailableModelOptions,
|
|
19
|
+
getStoredModelPreference,
|
|
20
|
+
setStoredModelPreference,
|
|
21
|
+
} from '@/lib/model-selection'
|
|
22
|
+
import type { Provider, ModelOption } from '@/lib/model-selection'
|
|
23
|
+
import { hasCapability } from '@/lib/vendor-capabilities'
|
|
24
|
+
import { useAudioRecorder } from '@/hooks/useAudioRecorder'
|
|
25
|
+
import { useTTS } from '@/hooks/useTTS'
|
|
8
26
|
|
|
9
27
|
import GuitarRecommendation from '@/components/example-GuitarRecommendation'
|
|
10
28
|
|
|
@@ -35,7 +53,19 @@ function ChattingLayout({ children }: { children: React.ReactNode }) {
|
|
|
35
53
|
)
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
function Messages({
|
|
56
|
+
function Messages({
|
|
57
|
+
messages,
|
|
58
|
+
playingId,
|
|
59
|
+
onSpeak,
|
|
60
|
+
onStopSpeak,
|
|
61
|
+
canPlayTTS,
|
|
62
|
+
}: {
|
|
63
|
+
messages: ChatMessages
|
|
64
|
+
playingId: string | null
|
|
65
|
+
onSpeak: (text: string, id: string) => void
|
|
66
|
+
onStopSpeak: () => void
|
|
67
|
+
canPlayTTS: boolean
|
|
68
|
+
}) {
|
|
39
69
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
40
70
|
|
|
41
71
|
useEffect(() => {
|
|
@@ -49,80 +79,245 @@ function Messages({ messages }: { messages: ChatMessages }) {
|
|
|
49
79
|
return null
|
|
50
80
|
}
|
|
51
81
|
|
|
82
|
+
// Extract text content from message parts
|
|
83
|
+
const getTextContent = (
|
|
84
|
+
parts: ChatMessages[number]['parts'],
|
|
85
|
+
): string | null => {
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (part.type === 'text' && part.content) {
|
|
88
|
+
return part.content
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
52
94
|
return (
|
|
53
95
|
<div
|
|
54
96
|
ref={messagesContainerRef}
|
|
55
97
|
className="flex-1 overflow-y-auto pb-4 min-h-0"
|
|
56
98
|
>
|
|
57
99
|
<div className="max-w-3xl mx-auto w-full px-4">
|
|
58
|
-
{messages.map((message) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
{messages.map((message) => {
|
|
101
|
+
const textContent = getTextContent(message.parts)
|
|
102
|
+
const isPlaying = playingId === message.id
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
key={message.id}
|
|
107
|
+
className={`p-4 ${
|
|
108
|
+
message.role === 'assistant'
|
|
109
|
+
? 'bg-linear-to-r from-orange-500/5 to-red-600/5'
|
|
110
|
+
: 'bg-transparent'
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
114
|
+
{message.role === 'assistant' ? (
|
|
115
|
+
<div className="w-8 h-8 rounded-lg bg-linear-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
|
|
116
|
+
AI
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
|
|
120
|
+
Y
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<div className="flex-1 min-w-0">
|
|
124
|
+
{message.parts.map((part, index) => {
|
|
125
|
+
if (part.type === 'text' && part.content) {
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
className="flex-1 min-w-0 prose dark:prose-invert max-w-none prose-sm"
|
|
129
|
+
key={index}
|
|
130
|
+
>
|
|
131
|
+
<Streamdown>{part.content}</Streamdown>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
// Guitar recommendation card
|
|
136
|
+
if (
|
|
137
|
+
part.type === 'tool-call' &&
|
|
138
|
+
part.name === 'recommendGuitar' &&
|
|
139
|
+
part.output
|
|
140
|
+
) {
|
|
141
|
+
return (
|
|
142
|
+
<div key={part.id} className="max-w-[80%] mx-auto">
|
|
143
|
+
<GuitarRecommendation id={String(part.output?.id)} />
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return null
|
|
148
|
+
})}
|
|
75
149
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
) {
|
|
95
|
-
return (
|
|
96
|
-
<div key={part.id} className="max-w-[80%] mx-auto">
|
|
97
|
-
<GuitarRecommendation id={String(part.output?.id)} />
|
|
98
|
-
</div>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
return null
|
|
102
|
-
})}
|
|
150
|
+
{/* TTS button for assistant messages */}
|
|
151
|
+
{message.role === 'assistant' && textContent && canPlayTTS && (
|
|
152
|
+
<button
|
|
153
|
+
onClick={() =>
|
|
154
|
+
isPlaying
|
|
155
|
+
? onStopSpeak()
|
|
156
|
+
: onSpeak(textContent, message.id)
|
|
157
|
+
}
|
|
158
|
+
className="flex-shrink-0 p-2 text-gray-400 hover:text-orange-400 transition-colors"
|
|
159
|
+
title={isPlaying ? 'Stop speaking' : 'Read aloud'}
|
|
160
|
+
>
|
|
161
|
+
{isPlaying ? (
|
|
162
|
+
<VolumeX className="w-4 h-4" />
|
|
163
|
+
) : (
|
|
164
|
+
<Volume2 className="w-4 h-4" />
|
|
165
|
+
)}
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
103
168
|
</div>
|
|
104
169
|
</div>
|
|
105
|
-
|
|
106
|
-
)
|
|
170
|
+
)
|
|
171
|
+
})}
|
|
107
172
|
</div>
|
|
108
173
|
</div>
|
|
109
174
|
)
|
|
110
175
|
}
|
|
111
176
|
|
|
112
177
|
function ChatPage() {
|
|
113
|
-
const { messages, sendMessage, isLoading, stop } =
|
|
114
|
-
useGuitarRecommendationChat()
|
|
115
178
|
const [input, setInput] = useState('')
|
|
179
|
+
const [availableProviders, setAvailableProviders] = useState<Provider[]>([])
|
|
180
|
+
const [selectedModel, setSelectedModel] = useState<ModelOption | null>(null)
|
|
181
|
+
const [isCheckingProviders, setIsCheckingProviders] = useState(true)
|
|
182
|
+
|
|
183
|
+
// Audio hooks
|
|
184
|
+
const { isRecording, isTranscribing, startRecording, stopRecording } =
|
|
185
|
+
useAudioRecorder()
|
|
186
|
+
const { playingId, speak, stop: stopTTS } = useTTS()
|
|
187
|
+
|
|
188
|
+
// Fetch available providers on mount
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
fetch('/demo/api/available-providers')
|
|
191
|
+
.then((res) => res.json())
|
|
192
|
+
.then((data) => {
|
|
193
|
+
setAvailableProviders(data.providers)
|
|
194
|
+
|
|
195
|
+
// Set default model based on stored preference or first available
|
|
196
|
+
const storedPref = getStoredModelPreference()
|
|
197
|
+
const availableOptions = getAvailableModelOptions(data.providers)
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
storedPref &&
|
|
201
|
+
availableOptions.some((o) => o.model === storedPref.model)
|
|
202
|
+
) {
|
|
203
|
+
setSelectedModel(storedPref)
|
|
204
|
+
} else if (availableOptions.length > 0) {
|
|
205
|
+
setSelectedModel(availableOptions[0])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setIsCheckingProviders(false)
|
|
209
|
+
})
|
|
210
|
+
.catch(() => {
|
|
211
|
+
// Fallback to Ollama if can't reach API
|
|
212
|
+
setAvailableProviders(['ollama'])
|
|
213
|
+
const ollamaOption = MODEL_OPTIONS.find((m) => m.provider === 'ollama')
|
|
214
|
+
if (ollamaOption) setSelectedModel(ollamaOption)
|
|
215
|
+
setIsCheckingProviders(false)
|
|
216
|
+
})
|
|
217
|
+
}, [])
|
|
218
|
+
|
|
219
|
+
const availableModelOptions = getAvailableModelOptions(availableProviders)
|
|
220
|
+
|
|
221
|
+
// Check if current provider supports TTS (only OpenAI)
|
|
222
|
+
const canPlayTTS =
|
|
223
|
+
selectedModel && hasCapability(selectedModel.provider, 'tts')
|
|
224
|
+
const canRecordAudio =
|
|
225
|
+
selectedModel && hasCapability(selectedModel.provider, 'transcription')
|
|
226
|
+
|
|
227
|
+
const { messages, sendMessage, isLoading, stop } =
|
|
228
|
+
useGuitarRecommendationChat(
|
|
229
|
+
selectedModel?.provider || 'anthropic',
|
|
230
|
+
selectedModel?.model || 'claude-haiku-4-5',
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const handleModelChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
234
|
+
const selected = availableModelOptions.find(
|
|
235
|
+
(m) => `${m.provider}:${m.model}` === e.target.value,
|
|
236
|
+
)
|
|
237
|
+
if (selected) {
|
|
238
|
+
setSelectedModel(selected)
|
|
239
|
+
setStoredModelPreference(selected)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const handleMicClick = async () => {
|
|
244
|
+
if (isRecording) {
|
|
245
|
+
const transcribedText = await stopRecording()
|
|
246
|
+
if (transcribedText) {
|
|
247
|
+
setInput((prev) =>
|
|
248
|
+
prev ? `${prev} ${transcribedText}` : transcribedText,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
await startRecording()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
116
255
|
|
|
117
256
|
const Layout = messages.length ? ChattingLayout : InitalLayout
|
|
118
257
|
|
|
258
|
+
if (isCheckingProviders) {
|
|
259
|
+
return (
|
|
260
|
+
<div className="relative flex h-[calc(100vh-80px)] bg-gray-900 items-center justify-center">
|
|
261
|
+
<Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (availableModelOptions.length === 0) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="relative flex h-[calc(100vh-80px)] bg-gray-900">
|
|
269
|
+
<div className="flex-1 flex items-center justify-center px-4">
|
|
270
|
+
<div className="text-center max-w-xl">
|
|
271
|
+
<h1 className="text-2xl font-bold text-white mb-4">
|
|
272
|
+
No AI Providers Available
|
|
273
|
+
</h1>
|
|
274
|
+
<p className="text-gray-400 mb-4">
|
|
275
|
+
Please configure at least one AI provider in your{' '}
|
|
276
|
+
<code className="text-orange-400">.env.local</code> file, or
|
|
277
|
+
ensure Ollama is running locally.
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
119
285
|
return (
|
|
120
286
|
<div className="relative flex h-[calc(100vh-80px)] bg-gray-900">
|
|
121
287
|
<div className="flex-1 flex flex-col min-h-0">
|
|
122
|
-
<Messages
|
|
288
|
+
<Messages
|
|
289
|
+
messages={messages}
|
|
290
|
+
playingId={playingId}
|
|
291
|
+
onSpeak={speak}
|
|
292
|
+
onStopSpeak={stopTTS}
|
|
293
|
+
canPlayTTS={!!canPlayTTS}
|
|
294
|
+
/>
|
|
123
295
|
|
|
124
296
|
<Layout>
|
|
125
297
|
<div className="space-y-3">
|
|
298
|
+
{/* Model Selector */}
|
|
299
|
+
<div className="flex justify-center">
|
|
300
|
+
<select
|
|
301
|
+
value={
|
|
302
|
+
selectedModel
|
|
303
|
+
? `${selectedModel.provider}:${selectedModel.model}`
|
|
304
|
+
: ''
|
|
305
|
+
}
|
|
306
|
+
onChange={handleModelChange}
|
|
307
|
+
disabled={isLoading}
|
|
308
|
+
className="rounded-lg border border-orange-500/20 bg-gray-800/50 px-3 py-1.5 text-xs text-gray-300 focus:outline-none focus:ring-2 focus:ring-orange-500/50"
|
|
309
|
+
>
|
|
310
|
+
{availableModelOptions.map((option) => (
|
|
311
|
+
<option
|
|
312
|
+
key={`${option.provider}:${option.model}`}
|
|
313
|
+
value={`${option.provider}:${option.model}`}
|
|
314
|
+
>
|
|
315
|
+
{option.label}
|
|
316
|
+
</option>
|
|
317
|
+
))}
|
|
318
|
+
</select>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
126
321
|
{isLoading && (
|
|
127
322
|
<div className="flex items-center justify-center">
|
|
128
323
|
<button
|
|
@@ -143,36 +338,61 @@ function ChatPage() {
|
|
|
143
338
|
}
|
|
144
339
|
}}
|
|
145
340
|
>
|
|
146
|
-
<div className="relative max-w-xl mx-auto">
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
341
|
+
<div className="relative max-w-xl mx-auto flex items-center gap-2">
|
|
342
|
+
{/* Mic button - only show if OpenAI is available */}
|
|
343
|
+
{canRecordAudio && (
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={handleMicClick}
|
|
347
|
+
disabled={isLoading || isTranscribing}
|
|
348
|
+
className={`p-3 rounded-lg transition-colors ${
|
|
349
|
+
isRecording
|
|
350
|
+
? 'bg-red-600 hover:bg-red-700 text-white'
|
|
351
|
+
: 'bg-gray-800/50 text-gray-400 hover:text-orange-400 border border-orange-500/20'
|
|
352
|
+
} disabled:opacity-50`}
|
|
353
|
+
title={isRecording ? 'Stop recording' : 'Start recording'}
|
|
354
|
+
>
|
|
355
|
+
{isTranscribing ? (
|
|
356
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
357
|
+
) : isRecording ? (
|
|
358
|
+
<MicOff className="w-4 h-4" />
|
|
359
|
+
) : (
|
|
360
|
+
<Mic className="w-4 h-4" />
|
|
361
|
+
)}
|
|
362
|
+
</button>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
<div className="relative flex-1">
|
|
366
|
+
<textarea
|
|
367
|
+
value={input}
|
|
368
|
+
onChange={(e) => setInput(e.target.value)}
|
|
369
|
+
placeholder="Type something clever..."
|
|
370
|
+
className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
|
|
371
|
+
rows={1}
|
|
372
|
+
style={{ minHeight: '44px', maxHeight: '200px' }}
|
|
373
|
+
disabled={isLoading}
|
|
374
|
+
onInput={(e) => {
|
|
375
|
+
const target = e.target as HTMLTextAreaElement
|
|
376
|
+
target.style.height = 'auto'
|
|
377
|
+
target.style.height =
|
|
378
|
+
Math.min(target.scrollHeight, 200) + 'px'
|
|
379
|
+
}}
|
|
380
|
+
onKeyDown={(e) => {
|
|
381
|
+
if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
|
|
382
|
+
e.preventDefault()
|
|
383
|
+
sendMessage(input)
|
|
384
|
+
setInput('')
|
|
385
|
+
}
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
<button
|
|
389
|
+
type="submit"
|
|
390
|
+
disabled={!input.trim() || isLoading}
|
|
391
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
|
|
392
|
+
>
|
|
393
|
+
<Send className="w-4 h-4" />
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
176
396
|
</div>
|
|
177
397
|
</form>
|
|
178
398
|
</div>
|
|
@@ -15,15 +15,18 @@
|
|
|
15
15
|
"jsName": "ChatDemo"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
"icon": "
|
|
19
|
-
"url": "/
|
|
20
|
-
"name": "
|
|
21
|
-
"path": "src/routes/
|
|
22
|
-
"jsName": "
|
|
18
|
+
"icon": "ImageIcon",
|
|
19
|
+
"url": "/demo/image",
|
|
20
|
+
"name": "Generate Image",
|
|
21
|
+
"path": "src/routes/demo/image.tsx",
|
|
22
|
+
"jsName": "ImageDemo"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"icon": "ChefHat",
|
|
26
|
+
"url": "/demo/structured",
|
|
27
|
+
"name": "Structured Output",
|
|
28
|
+
"path": "src/routes/demo/structured.tsx",
|
|
29
|
+
"jsName": "StructuredDemo"
|
|
27
30
|
}
|
|
28
31
|
],
|
|
29
32
|
"integrations": [
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@tanstack/ai": "
|
|
4
|
-
"@tanstack/ai-anthropic": "
|
|
5
|
-
"@tanstack/ai-client": "
|
|
6
|
-
"@tanstack/ai-
|
|
7
|
-
"@tanstack/
|
|
3
|
+
"@tanstack/ai": "latest",
|
|
4
|
+
"@tanstack/ai-anthropic": "latest",
|
|
5
|
+
"@tanstack/ai-client": "latest",
|
|
6
|
+
"@tanstack/ai-gemini": "latest",
|
|
7
|
+
"@tanstack/ai-ollama": "latest",
|
|
8
|
+
"@tanstack/ai-openai": "latest",
|
|
9
|
+
"@tanstack/ai-react": "latest",
|
|
10
|
+
"@tanstack/react-ai-devtools": "latest",
|
|
8
11
|
"highlight.js": "^11.11.1",
|
|
9
12
|
"streamdown": "^1.6.5",
|
|
10
13
|
"lucide-react": "^0.544.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/cta-framework-react-cra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "CTA Framework for React (Create React App)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "Jack Herrington <jherr@pobox.com>",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@tanstack/cta-engine": "0.
|
|
26
|
+
"@tanstack/cta-engine": "0.44.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.6.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
<% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %>
|
|
1
2
|
<% if (!fileRouter) { ignoreFile() } %>import { <% if (addOnEnabled.start) { %>
|
|
2
|
-
HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (
|
|
3
|
+
HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (hasContext) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router'
|
|
3
4
|
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
|
|
4
5
|
import { TanStackDevtools } from '@tanstack/react-devtools'
|
|
5
6
|
<% if (addOns.length) { %>
|
|
@@ -9,21 +10,28 @@ import <%= integration.jsName %> from '<%= relativePath(integration.path, true)
|
|
|
9
10
|
<% } %>
|
|
10
11
|
<% if (addOnEnabled.start) { %>
|
|
11
12
|
import appCss from '../styles.css?url'
|
|
12
|
-
<% }
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (addOnEnabled["apollo-client"]) { %>
|
|
15
|
+
import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (addOnEnabled["tanstack-query"]) { %>
|
|
13
18
|
import type { QueryClient } from '@tanstack/react-query'
|
|
14
19
|
<% if (addOnEnabled.tRPC) { %>
|
|
15
20
|
import type { TRPCRouter } from '@/integrations/trpc/router'
|
|
16
21
|
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query'
|
|
17
22
|
<% } %>
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
<% } %>
|
|
24
|
+
<% if (hasContext) { %>
|
|
25
|
+
interface MyRouterContext <% if (addOnEnabled["apollo-client"]) {%> extends ApolloClientIntegration.RouterContext <%} %>{
|
|
26
|
+
<% if (addOnEnabled["tanstack-query"]) { %>
|
|
20
27
|
queryClient: QueryClient
|
|
21
28
|
<% if (addOnEnabled.tRPC) { %>
|
|
22
29
|
trpc: TRPCOptionsProxy<TRPCRouter>
|
|
23
30
|
<% } %>
|
|
31
|
+
<% } %>
|
|
24
32
|
}<% } %>
|
|
25
|
-
|
|
26
|
-
export const Route = <% if (
|
|
33
|
+
|
|
34
|
+
export const Route = <% if (hasContext) { %>createRootRouteWithContext<MyRouterContext>()<% } else { %>createRootRoute<% } %>({
|
|
27
35
|
<% if (addOnEnabled.start) { %>
|
|
28
36
|
head: () => ({
|
|
29
37
|
meta: [
|
package/tests/react-cra.test.ts
CHANGED
|
@@ -148,3 +148,17 @@ test('file router with add-on start on npm', async () => {
|
|
|
148
148
|
'./snapshots/react-cra/cr-ts-start-tanstack-query-npm.json',
|
|
149
149
|
)
|
|
150
150
|
})
|
|
151
|
+
|
|
152
|
+
test('file router with add-on start and apollo-client on npm', async () => {
|
|
153
|
+
const { environment, output } = createMemoryEnvironment()
|
|
154
|
+
const options = {
|
|
155
|
+
...(await createReactOptions(['start', 'apollo-client'])),
|
|
156
|
+
tailwind: true,
|
|
157
|
+
typescript: true,
|
|
158
|
+
} as Options
|
|
159
|
+
await createApp(environment, options)
|
|
160
|
+
const cleanedOutput = cleanupOutput(options, output)
|
|
161
|
+
await expect(JSON.stringify(cleanedOutput, null, 2)).toMatchFileSnapshot(
|
|
162
|
+
'./snapshots/react-cra/cr-ts-start-apollo-client-npm.json',
|
|
163
|
+
)
|
|
164
|
+
})
|