@tanstack/cta-framework-react-cra 0.44.3 → 0.46.1
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} +30 -150
- 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/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ResumeAssistantButton.tsx +20 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +30 -0
- package/examples/resume/package.json +27 -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
|
@@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import { useStore } from '@tanstack/react-store'
|
|
3
3
|
import { Store } from '@tanstack/store'
|
|
4
4
|
|
|
5
|
-
import { Send, X, ChevronRight } from 'lucide-react'
|
|
5
|
+
import { Send, X, ChevronRight, BotIcon } from 'lucide-react'
|
|
6
6
|
import { Streamdown } from 'streamdown'
|
|
7
7
|
|
|
8
|
-
import { useGuitarRecommendationChat } from '@/lib/
|
|
9
|
-
import type { ChatMessages } from '@/lib/
|
|
8
|
+
import { useGuitarRecommendationChat } from '@/lib/demo-ai-hook'
|
|
9
|
+
import type { ChatMessages } from '@/lib/demo-ai-hook'
|
|
10
10
|
|
|
11
|
-
import GuitarRecommendation from './
|
|
11
|
+
import GuitarRecommendation from './demo-GuitarRecommendation'
|
|
12
12
|
|
|
13
13
|
export const showAIAssistant = new Store(false)
|
|
14
14
|
|
|
@@ -87,12 +87,10 @@ export default function AIAssistant() {
|
|
|
87
87
|
<div className="relative z-50">
|
|
88
88
|
<button
|
|
89
89
|
onClick={() => showAIAssistant.setState((state) => !state)}
|
|
90
|
-
className="w-full flex items-center justify-between px-4 py-2.5 rounded-lg bg-linear-to-r from-
|
|
90
|
+
className="w-full flex items-center justify-between px-4 py-2.5 rounded-lg bg-linear-to-r from-green-700 to-green-900 text-white hover:opacity-90 transition-opacity"
|
|
91
91
|
>
|
|
92
92
|
<div className="flex items-center gap-2">
|
|
93
|
-
<
|
|
94
|
-
AI
|
|
95
|
-
</div>
|
|
93
|
+
<BotIcon size={24} />
|
|
96
94
|
<span className="font-medium">AI Assistant</span>
|
|
97
95
|
</div>
|
|
98
96
|
<ChevronRight className="w-4 h-4" />
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useNavigate } from '@tanstack/react-router'
|
|
2
2
|
|
|
3
|
-
import { showAIAssistant } from './
|
|
3
|
+
import { showAIAssistant } from './demo-AIAssistant'
|
|
4
4
|
|
|
5
|
-
import guitars from '
|
|
5
|
+
import guitars from '@/data/demo-guitars'
|
|
6
6
|
|
|
7
7
|
export default function GuitarRecommendation({ id }: { id: string }) {
|
|
8
8
|
const navigate = useNavigate()
|
|
@@ -2,21 +2,21 @@ import {
|
|
|
2
2
|
fetchServerSentEvents,
|
|
3
3
|
useChat,
|
|
4
4
|
createChatClientOptions,
|
|
5
|
-
} from
|
|
6
|
-
import type { InferChatMessages } from
|
|
7
|
-
import { clientTools } from
|
|
5
|
+
} from '@tanstack/ai-react'
|
|
6
|
+
import type { InferChatMessages } from '@tanstack/ai-react'
|
|
7
|
+
import { clientTools } from '@tanstack/ai-client'
|
|
8
8
|
|
|
9
|
-
import { recommendGuitarToolDef } from
|
|
9
|
+
import { recommendGuitarToolDef } from '@/lib/demo-guitar-tools'
|
|
10
10
|
|
|
11
11
|
const recommendGuitarToolClient = recommendGuitarToolDef.client(({ id }) => ({
|
|
12
12
|
id: +id,
|
|
13
|
-
}))
|
|
13
|
+
}))
|
|
14
14
|
|
|
15
15
|
const chatOptions = createChatClientOptions({
|
|
16
|
-
connection: fetchServerSentEvents(
|
|
16
|
+
connection: fetchServerSentEvents('/demo/api/ai/chat'),
|
|
17
17
|
tools: clientTools(recommendGuitarToolClient),
|
|
18
|
-
})
|
|
18
|
+
})
|
|
19
19
|
|
|
20
|
-
export type ChatMessages = InferChatMessages<typeof chatOptions
|
|
20
|
+
export type ChatMessages = InferChatMessages<typeof chatOptions>
|
|
21
21
|
|
|
22
|
-
export const useGuitarRecommendationChat = () => useChat(chatOptions)
|
|
22
|
+
export const useGuitarRecommendationChat = () => useChat(chatOptions)
|
|
@@ -11,24 +11,16 @@ import {
|
|
|
11
11
|
} from 'lucide-react'
|
|
12
12
|
import { Streamdown } from 'streamdown'
|
|
13
13
|
|
|
14
|
-
import { useGuitarRecommendationChat } from '@/lib/
|
|
15
|
-
import type { ChatMessages } from '@/lib/
|
|
16
|
-
import {
|
|
17
|
-
|
|
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'
|
|
14
|
+
import { useGuitarRecommendationChat } from '@/lib/demo-ai-hook'
|
|
15
|
+
import type { ChatMessages } from '@/lib/demo-ai-hook'
|
|
16
|
+
import { useAudioRecorder } from '@/hooks/demo-useAudioRecorder'
|
|
17
|
+
import { useTTS } from '@/hooks/demo-useTTS'
|
|
26
18
|
|
|
27
|
-
import GuitarRecommendation from '@/components/
|
|
19
|
+
import GuitarRecommendation from '@/components/demo-GuitarRecommendation'
|
|
28
20
|
|
|
29
|
-
import './
|
|
21
|
+
import './ai-chat.css'
|
|
30
22
|
|
|
31
|
-
function
|
|
23
|
+
function InitialLayout({ children }: { children: React.ReactNode }) {
|
|
32
24
|
return (
|
|
33
25
|
<div className="flex-1 flex items-center justify-center px-4">
|
|
34
26
|
<div className="text-center max-w-3xl mx-auto w-full">
|
|
@@ -58,13 +50,11 @@ function Messages({
|
|
|
58
50
|
playingId,
|
|
59
51
|
onSpeak,
|
|
60
52
|
onStopSpeak,
|
|
61
|
-
canPlayTTS,
|
|
62
53
|
}: {
|
|
63
54
|
messages: ChatMessages
|
|
64
55
|
playingId: string | null
|
|
65
56
|
onSpeak: (text: string, id: string) => void
|
|
66
57
|
onStopSpeak: () => void
|
|
67
|
-
canPlayTTS: boolean
|
|
68
58
|
}) {
|
|
69
59
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
70
60
|
|
|
@@ -148,7 +138,7 @@ function Messages({
|
|
|
148
138
|
})}
|
|
149
139
|
</div>
|
|
150
140
|
{/* TTS button for assistant messages */}
|
|
151
|
-
{message.role === 'assistant' && textContent &&
|
|
141
|
+
{message.role === 'assistant' && textContent && (
|
|
152
142
|
<button
|
|
153
143
|
onClick={() =>
|
|
154
144
|
isPlaying
|
|
@@ -176,69 +166,13 @@ function Messages({
|
|
|
176
166
|
|
|
177
167
|
function ChatPage() {
|
|
178
168
|
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
169
|
|
|
183
|
-
// Audio hooks
|
|
184
170
|
const { isRecording, isTranscribing, startRecording, stopRecording } =
|
|
185
171
|
useAudioRecorder()
|
|
186
172
|
const { playingId, speak, stop: stopTTS } = useTTS()
|
|
187
173
|
|
|
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
174
|
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
|
-
}
|
|
175
|
+
useGuitarRecommendationChat()
|
|
242
176
|
|
|
243
177
|
const handleMicClick = async () => {
|
|
244
178
|
if (isRecording) {
|
|
@@ -253,34 +187,7 @@ function ChatPage() {
|
|
|
253
187
|
}
|
|
254
188
|
}
|
|
255
189
|
|
|
256
|
-
const Layout = messages.length ? ChattingLayout :
|
|
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
|
-
}
|
|
190
|
+
const Layout = messages.length ? ChattingLayout : InitialLayout
|
|
284
191
|
|
|
285
192
|
return (
|
|
286
193
|
<div className="relative flex h-[calc(100vh-80px)] bg-gray-900">
|
|
@@ -290,34 +197,10 @@ function ChatPage() {
|
|
|
290
197
|
playingId={playingId}
|
|
291
198
|
onSpeak={speak}
|
|
292
199
|
onStopSpeak={stopTTS}
|
|
293
|
-
canPlayTTS={!!canPlayTTS}
|
|
294
200
|
/>
|
|
295
201
|
|
|
296
202
|
<Layout>
|
|
297
203
|
<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
|
-
|
|
321
204
|
{isLoading && (
|
|
322
205
|
<div className="flex items-center justify-center">
|
|
323
206
|
<button
|
|
@@ -339,28 +222,25 @@ function ChatPage() {
|
|
|
339
222
|
}}
|
|
340
223
|
>
|
|
341
224
|
<div className="relative max-w-xl mx-auto flex items-center gap-2">
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)}
|
|
362
|
-
</button>
|
|
363
|
-
)}
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={handleMicClick}
|
|
228
|
+
disabled={isLoading || isTranscribing}
|
|
229
|
+
className={`p-3 rounded-lg transition-colors ${
|
|
230
|
+
isRecording
|
|
231
|
+
? 'bg-red-600 hover:bg-red-700 text-white'
|
|
232
|
+
: 'bg-gray-800/50 text-gray-400 hover:text-orange-400 border border-orange-500/20'
|
|
233
|
+
} disabled:opacity-50`}
|
|
234
|
+
title={isRecording ? 'Stop recording' : 'Start recording'}
|
|
235
|
+
>
|
|
236
|
+
{isTranscribing ? (
|
|
237
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
238
|
+
) : isRecording ? (
|
|
239
|
+
<MicOff className="w-4 h-4" />
|
|
240
|
+
) : (
|
|
241
|
+
<Mic className="w-4 h-4" />
|
|
242
|
+
)}
|
|
243
|
+
</button>
|
|
364
244
|
|
|
365
245
|
<div className="relative flex-1">
|
|
366
246
|
<textarea
|
|
@@ -402,6 +282,6 @@ function ChatPage() {
|
|
|
402
282
|
)
|
|
403
283
|
}
|
|
404
284
|
|
|
405
|
-
export const Route = createFileRoute('/demo/
|
|
285
|
+
export const Route = createFileRoute('/demo/ai/chat')({
|
|
406
286
|
component: ChatPage,
|
|
407
287
|
})
|
|
@@ -19,16 +19,6 @@ function ImagePage() {
|
|
|
19
19
|
const [images, setImages] = useState<Array<GeneratedImage>>([])
|
|
20
20
|
const [isLoading, setIsLoading] = useState(false)
|
|
21
21
|
const [error, setError] = useState<string | null>(null)
|
|
22
|
-
const [usedModel, setUsedModel] = useState<string | null>(null)
|
|
23
|
-
const [hasOpenAI, setHasOpenAI] = useState<boolean | null>(null)
|
|
24
|
-
|
|
25
|
-
// Check if OpenAI is available
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
fetch('/demo/api/available-providers')
|
|
28
|
-
.then((res) => res.json())
|
|
29
|
-
.then((data) => setHasOpenAI(data.hasOpenAI))
|
|
30
|
-
.catch(() => setHasOpenAI(false))
|
|
31
|
-
}, [])
|
|
32
22
|
|
|
33
23
|
const handleGenerate = async () => {
|
|
34
24
|
setIsLoading(true)
|
|
@@ -36,7 +26,7 @@ function ImagePage() {
|
|
|
36
26
|
setImages([])
|
|
37
27
|
|
|
38
28
|
try {
|
|
39
|
-
const response = await fetch('/demo/api/image', {
|
|
29
|
+
const response = await fetch('/demo/api/ai/image', {
|
|
40
30
|
method: 'POST',
|
|
41
31
|
headers: { 'Content-Type': 'application/json' },
|
|
42
32
|
body: JSON.stringify({ prompt, size, numberOfImages }),
|
|
@@ -49,7 +39,6 @@ function ImagePage() {
|
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
setImages(data.images)
|
|
52
|
-
setUsedModel(data.model)
|
|
53
42
|
} catch (err: any) {
|
|
54
43
|
setError(err.message)
|
|
55
44
|
} finally {
|
|
@@ -83,34 +72,6 @@ function ImagePage() {
|
|
|
83
72
|
}
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
// Show loading state while checking for OpenAI
|
|
87
|
-
if (hasOpenAI === null) {
|
|
88
|
-
return (
|
|
89
|
-
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6 flex items-center justify-center">
|
|
90
|
-
<Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Show message if OpenAI is not available
|
|
96
|
-
if (!hasOpenAI) {
|
|
97
|
-
return (
|
|
98
|
-
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6">
|
|
99
|
-
<div className="max-w-2xl mx-auto text-center py-16">
|
|
100
|
-
<ImageIcon className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
|
101
|
-
<h1 className="text-2xl font-bold text-white mb-4">
|
|
102
|
-
Image Generation Unavailable
|
|
103
|
-
</h1>
|
|
104
|
-
<p className="text-gray-400 mb-4">
|
|
105
|
-
Image generation requires an OpenAI API key. Please add your{' '}
|
|
106
|
-
<code className="text-orange-400">OPENAI_API_KEY</code> to your{' '}
|
|
107
|
-
<code className="text-orange-400">.env.local</code> file.
|
|
108
|
-
</p>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
75
|
return (
|
|
115
76
|
<div className="min-h-[calc(100vh-80px)] bg-gray-900 p-6">
|
|
116
77
|
<div className="max-w-6xl mx-auto">
|
|
@@ -227,15 +188,6 @@ function ImagePage() {
|
|
|
227
188
|
</div>
|
|
228
189
|
))}
|
|
229
190
|
</div>
|
|
230
|
-
<div className="pt-4 border-t border-gray-700 text-sm text-gray-400">
|
|
231
|
-
<p>
|
|
232
|
-
Provider:{' '}
|
|
233
|
-
<span className="text-orange-400">OpenAI</span>
|
|
234
|
-
</p>
|
|
235
|
-
<p>
|
|
236
|
-
Model: <span className="text-orange-400">{usedModel}</span>
|
|
237
|
-
</p>
|
|
238
|
-
</div>
|
|
239
191
|
</div>
|
|
240
192
|
) : !error && !isLoading ? (
|
|
241
193
|
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
|
@@ -252,6 +204,6 @@ function ImagePage() {
|
|
|
252
204
|
)
|
|
253
205
|
}
|
|
254
206
|
|
|
255
|
-
export const Route = createFileRoute('/demo/image')({
|
|
207
|
+
export const Route = createFileRoute('/demo/ai/image')({
|
|
256
208
|
component: ImagePage,
|
|
257
209
|
})
|