create-start-app 0.6.12 → 0.7.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/dist/create-app.js +1 -1
- package/package.json +2 -11
- package/src/create-app.ts +1 -1
- package/templates/react/base/README.md.ejs +2 -2
- package/templates/react/example/tanchat/assets/public/example-guitar-dune.jpg +0 -0
- package/templates/react/example/tanchat/assets/public/example-guitar-motherboard.jpg +0 -0
- package/templates/react/example/tanchat/assets/public/example-guitar-racing.jpg +0 -0
- package/templates/react/example/tanchat/assets/public/example-guitar-steamer-trunk.jpg +0 -0
- package/templates/react/example/tanchat/assets/public/example-guitar-steampunk.jpg +0 -0
- package/templates/react/example/tanchat/assets/public/example-guitar-underwater.jpg +0 -0
- package/templates/react/example/tanchat/assets/src/components/example-AIAssistant.tsx +173 -0
- package/templates/react/example/tanchat/assets/src/components/example-GuitarRecommendation.tsx +47 -0
- package/templates/react/example/tanchat/assets/src/data/example-guitars.ts +73 -0
- package/templates/react/example/tanchat/assets/src/integrations/tanchat/header-user.tsx +5 -0
- package/templates/react/example/tanchat/assets/src/routes/example.chat.tsx +119 -397
- package/templates/react/example/tanchat/assets/src/routes/example.guitars/$guitarId.tsx +50 -0
- package/templates/react/example/tanchat/assets/src/routes/example.guitars/index.tsx +54 -0
- package/templates/react/example/tanchat/assets/src/store/example-assistant.ts +3 -0
- package/templates/react/example/tanchat/assets/src/utils/demo.ai.ts +14 -70
- package/templates/react/example/tanchat/assets/src/utils/demo.tools.ts +43 -0
- package/templates/react/example/tanchat/info.json +4 -0
- package/templates/react/example/tanchat/package.json +4 -1
- package/tests/snapshots/cra/cr-js-npm.json +1 -1
- package/tests/snapshots/cra/cr-ts-npm.json +1 -1
- package/tests/snapshots/cra/fr-ts-npm.json +1 -1
- package/tests/snapshots/cra/fr-ts-tw-npm.json +1 -1
- package/.github/workflows/auto.yml +0 -46
- package/templates/react/example/tanchat/assets/src/components/demo.SettingsDialog.tsx +0 -148
- package/templates/react/example/tanchat/assets/src/store/demo.hooks.ts +0 -21
- package/templates/react/example/tanchat/assets/src/store/demo.store.ts +0 -133
|
@@ -1,437 +1,159 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
-
import { useEffect,
|
|
3
|
-
import {
|
|
4
|
-
PlusCircle,
|
|
5
|
-
MessageCircle,
|
|
6
|
-
Trash2,
|
|
7
|
-
Send,
|
|
8
|
-
Settings,
|
|
9
|
-
Edit2,
|
|
10
|
-
} from 'lucide-react'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
import { Send } from 'lucide-react'
|
|
11
4
|
import ReactMarkdown from 'react-markdown'
|
|
12
5
|
import rehypeRaw from 'rehype-raw'
|
|
13
6
|
import rehypeSanitize from 'rehype-sanitize'
|
|
14
7
|
import rehypeHighlight from 'rehype-highlight'
|
|
8
|
+
import remarkGfm from 'remark-gfm'
|
|
9
|
+
import { useChat } from '@ai-sdk/react'
|
|
15
10
|
|
|
16
|
-
import { SettingsDialog } from '../components/demo.SettingsDialog'
|
|
17
|
-
import { useAppState } from '../store/demo.hooks'
|
|
18
|
-
import { store } from '../store/demo.store'
|
|
19
11
|
import { genAIResponse } from '../utils/demo.ai'
|
|
20
12
|
|
|
21
|
-
import type {
|
|
13
|
+
import type { UIMessage } from 'ai'
|
|
22
14
|
|
|
23
15
|
import '../demo.index.css'
|
|
24
16
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
17
|
+
function InitalLayout({ children }: { children: React.ReactNode }) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex-1 flex items-center justify-center px-4">
|
|
20
|
+
<div className="text-center max-w-3xl mx-auto w-full">
|
|
21
|
+
<h1 className="text-6xl font-bold mb-4 bg-gradient-to-r from-orange-500 to-red-600 text-transparent bg-clip-text uppercase">
|
|
22
|
+
<span className="text-white">TanStack</span> Chat
|
|
23
|
+
</h1>
|
|
24
|
+
<p className="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
|
|
25
|
+
You can ask me about anything, I might or might not have a good
|
|
26
|
+
answer, but you can still ask.
|
|
27
|
+
</p>
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
function ChattingLayout({ children }: { children: React.ReactNode }) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="absolute bottom-0 right-0 left-64 bg-gray-900/80 backdrop-blur-sm border-t border-orange-500/10">
|
|
37
|
+
<div className="max-w-3xl mx-auto w-full px-4 py-3">{children}</div>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
const [input, setInput] = useState('')
|
|
45
|
-
const [editingChatId, setEditingChatId] = useState<string | null>(null)
|
|
46
|
-
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
|
42
|
+
function Messages({ messages }: { messages: Array<UIMessage> }) {
|
|
47
43
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
48
|
-
const [pendingMessage, setPendingMessage] = useState<Message | null>(null)
|
|
49
44
|
|
|
50
|
-
|
|
45
|
+
useEffect(() => {
|
|
51
46
|
if (messagesContainerRef.current) {
|
|
52
47
|
messagesContainerRef.current.scrollTop =
|
|
53
48
|
messagesContainerRef.current.scrollHeight
|
|
54
49
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Scroll to bottom when messages change or loading state changes
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
scrollToBottom()
|
|
60
|
-
}, [messages, isLoading])
|
|
61
|
-
|
|
62
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
63
|
-
e.preventDefault()
|
|
64
|
-
if (!input.trim() || isLoading) return
|
|
65
|
-
|
|
66
|
-
const currentInput = input
|
|
67
|
-
setInput('') // Clear input early for better UX
|
|
68
|
-
setLoading(true)
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
let conversationId = currentConversationId
|
|
72
|
-
|
|
73
|
-
// If no current conversation, create one
|
|
74
|
-
if (!conversationId) {
|
|
75
|
-
conversationId = Date.now().toString()
|
|
76
|
-
const newConversation = {
|
|
77
|
-
id: conversationId,
|
|
78
|
-
title: currentInput.trim().slice(0, 30),
|
|
79
|
-
messages: [],
|
|
80
|
-
}
|
|
81
|
-
addConversation(newConversation)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const userMessage: Message = {
|
|
85
|
-
id: Date.now().toString(),
|
|
86
|
-
role: 'user' as const,
|
|
87
|
-
content: currentInput.trim(),
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Add user message
|
|
91
|
-
addMessage(conversationId, userMessage)
|
|
92
|
-
|
|
93
|
-
// Get active prompt
|
|
94
|
-
const activePrompt = getActivePrompt(store.state)
|
|
95
|
-
let systemPrompt
|
|
96
|
-
if (activePrompt) {
|
|
97
|
-
systemPrompt = {
|
|
98
|
-
value: activePrompt.content,
|
|
99
|
-
enabled: true,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Get AI response
|
|
104
|
-
const response = await genAIResponse({
|
|
105
|
-
data: {
|
|
106
|
-
messages: [...messages, userMessage],
|
|
107
|
-
systemPrompt,
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const reader = response.body?.getReader()
|
|
112
|
-
if (!reader) {
|
|
113
|
-
throw new Error('No reader found in response')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const decoder = new TextDecoder()
|
|
117
|
-
|
|
118
|
-
let done = false
|
|
119
|
-
let newMessage = {
|
|
120
|
-
id: (Date.now() + 1).toString(),
|
|
121
|
-
role: 'assistant' as const,
|
|
122
|
-
content: '',
|
|
123
|
-
}
|
|
124
|
-
while (!done) {
|
|
125
|
-
const out = await reader.read()
|
|
126
|
-
done = out.done
|
|
127
|
-
if (!done) {
|
|
128
|
-
try {
|
|
129
|
-
const json = JSON.parse(decoder.decode(out.value))
|
|
130
|
-
if (json.type === 'content_block_delta') {
|
|
131
|
-
newMessage = {
|
|
132
|
-
...newMessage,
|
|
133
|
-
content: newMessage.content + json.delta.text,
|
|
134
|
-
}
|
|
135
|
-
setPendingMessage(newMessage)
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setPendingMessage(null)
|
|
142
|
-
if (newMessage.content.trim()) {
|
|
143
|
-
addMessage(conversationId, newMessage)
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error('Error:', error)
|
|
147
|
-
const errorMessage: Message = {
|
|
148
|
-
id: (Date.now() + 1).toString(),
|
|
149
|
-
role: 'assistant' as const,
|
|
150
|
-
content: 'Sorry, I encountered an error processing your request.',
|
|
151
|
-
}
|
|
152
|
-
if (currentConversationId) {
|
|
153
|
-
addMessage(currentConversationId, errorMessage)
|
|
154
|
-
}
|
|
155
|
-
} finally {
|
|
156
|
-
setLoading(false)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const handleNewChat = () => {
|
|
161
|
-
const newConversation = {
|
|
162
|
-
id: Date.now().toString(),
|
|
163
|
-
title: 'New Chat',
|
|
164
|
-
messages: [],
|
|
165
|
-
}
|
|
166
|
-
addConversation(newConversation)
|
|
167
|
-
}
|
|
50
|
+
}, [messages])
|
|
168
51
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const handleUpdateChatTitle = (id: string, title: string) => {
|
|
174
|
-
updateConversationTitle(id, title)
|
|
175
|
-
setEditingChatId(null)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Handle input change
|
|
179
|
-
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
180
|
-
setInput(e.target.value)
|
|
52
|
+
if (!messages.length) {
|
|
53
|
+
return null
|
|
181
54
|
}
|
|
182
55
|
|
|
183
56
|
return (
|
|
184
|
-
<div className="
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{/* Sidebar */}
|
|
196
|
-
<div className="flex flex-col w-64 bg-gray-800 border-r border-gray-700">
|
|
197
|
-
<div className="p-4 border-b border-gray-700">
|
|
198
|
-
<button
|
|
199
|
-
onClick={handleNewChat}
|
|
200
|
-
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-gradient-to-r from-orange-500 to-red-600 rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-orange-500 w-full justify-center"
|
|
57
|
+
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-24">
|
|
58
|
+
<div className="max-w-3xl mx-auto w-full px-4">
|
|
59
|
+
{messages.map(({ id, role, content }) => (
|
|
60
|
+
<div
|
|
61
|
+
key={id}
|
|
62
|
+
className={`py-6 ${
|
|
63
|
+
role === 'assistant'
|
|
64
|
+
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
|
|
65
|
+
: 'bg-transparent'
|
|
66
|
+
}`}
|
|
201
67
|
>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{/* Chat List */}
|
|
208
|
-
<div className="flex-1 overflow-y-auto">
|
|
209
|
-
{conversations.map((chat) => (
|
|
210
|
-
<div
|
|
211
|
-
key={chat.id}
|
|
212
|
-
className={`group flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-gray-700/50 ${
|
|
213
|
-
chat.id === currentConversationId ? 'bg-gray-700/50' : ''
|
|
214
|
-
}`}
|
|
215
|
-
onClick={() => setCurrentConversationId(chat.id)}
|
|
216
|
-
>
|
|
217
|
-
<MessageCircle className="w-4 h-4 text-gray-400" />
|
|
218
|
-
{editingChatId === chat.id ? (
|
|
219
|
-
<input
|
|
220
|
-
type="text"
|
|
221
|
-
value={chat.title}
|
|
222
|
-
onChange={(e) =>
|
|
223
|
-
handleUpdateChatTitle(chat.id, e.target.value)
|
|
224
|
-
}
|
|
225
|
-
onBlur={() => setEditingChatId(null)}
|
|
226
|
-
onKeyDown={(e) => {
|
|
227
|
-
if (e.key === 'Enter') {
|
|
228
|
-
handleUpdateChatTitle(chat.id, chat.title)
|
|
229
|
-
}
|
|
230
|
-
}}
|
|
231
|
-
className="flex-1 bg-transparent text-sm text-white focus:outline-none"
|
|
232
|
-
autoFocus
|
|
233
|
-
/>
|
|
68
|
+
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
69
|
+
{role === 'assistant' ? (
|
|
70
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
|
|
71
|
+
AI
|
|
72
|
+
</div>
|
|
234
73
|
) : (
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
</
|
|
74
|
+
<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">
|
|
75
|
+
Y
|
|
76
|
+
</div>
|
|
238
77
|
)}
|
|
239
|
-
<div className="
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
</button>
|
|
249
|
-
<button
|
|
250
|
-
onClick={(e) => {
|
|
251
|
-
e.stopPropagation()
|
|
252
|
-
handleDeleteChat(chat.id)
|
|
253
|
-
}}
|
|
254
|
-
className="p-1 text-gray-400 hover:text-red-500"
|
|
78
|
+
<div className="flex-1 min-w-0">
|
|
79
|
+
<ReactMarkdown
|
|
80
|
+
className="prose dark:prose-invert max-w-none"
|
|
81
|
+
rehypePlugins={[
|
|
82
|
+
rehypeRaw,
|
|
83
|
+
rehypeSanitize,
|
|
84
|
+
rehypeHighlight,
|
|
85
|
+
remarkGfm,
|
|
86
|
+
]}
|
|
255
87
|
>
|
|
256
|
-
|
|
257
|
-
</
|
|
88
|
+
{content}
|
|
89
|
+
</ReactMarkdown>
|
|
258
90
|
</div>
|
|
259
91
|
</div>
|
|
260
|
-
|
|
261
|
-
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
262
94
|
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
263
98
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
.map((message) => (
|
|
277
|
-
<div
|
|
278
|
-
key={message!.id}
|
|
279
|
-
className={`py-6 ${
|
|
280
|
-
message!.role === 'assistant'
|
|
281
|
-
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
|
|
282
|
-
: 'bg-transparent'
|
|
283
|
-
}`}
|
|
284
|
-
>
|
|
285
|
-
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
286
|
-
{message!.role === 'assistant' ? (
|
|
287
|
-
<div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
|
|
288
|
-
AI
|
|
289
|
-
</div>
|
|
290
|
-
) : (
|
|
291
|
-
<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">
|
|
292
|
-
Y
|
|
293
|
-
</div>
|
|
294
|
-
)}
|
|
295
|
-
<div className="flex-1 min-w-0">
|
|
296
|
-
<ReactMarkdown
|
|
297
|
-
className="prose dark:prose-invert max-w-none"
|
|
298
|
-
rehypePlugins={[
|
|
299
|
-
rehypeRaw,
|
|
300
|
-
rehypeSanitize,
|
|
301
|
-
rehypeHighlight,
|
|
302
|
-
]}
|
|
303
|
-
>
|
|
304
|
-
{message!.content}
|
|
305
|
-
</ReactMarkdown>
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
))}
|
|
310
|
-
{isLoading && (
|
|
311
|
-
<div className="py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5">
|
|
312
|
-
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
313
|
-
<div className="relative w-8 h-8 flex-shrink-0">
|
|
314
|
-
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 via-red-500 to-orange-500 animate-[spin_2s_linear_infinite]"></div>
|
|
315
|
-
<div className="absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center">
|
|
316
|
-
<div className="relative w-full h-full rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center">
|
|
317
|
-
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 animate-pulse"></div>
|
|
318
|
-
<span className="relative z-10 text-sm font-medium text-white">
|
|
319
|
-
AI
|
|
320
|
-
</span>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
<div className="flex items-center gap-3">
|
|
325
|
-
<div className="text-gray-400 font-medium text-lg">
|
|
326
|
-
Thinking
|
|
327
|
-
</div>
|
|
328
|
-
<div className="flex gap-2">
|
|
329
|
-
<div
|
|
330
|
-
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
331
|
-
style={{ animationDelay: '0ms' }}
|
|
332
|
-
></div>
|
|
333
|
-
<div
|
|
334
|
-
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
335
|
-
style={{ animationDelay: '200ms' }}
|
|
336
|
-
></div>
|
|
337
|
-
<div
|
|
338
|
-
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
339
|
-
style={{ animationDelay: '400ms' }}
|
|
340
|
-
></div>
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
)}
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
99
|
+
function ChatPage() {
|
|
100
|
+
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
|
101
|
+
initialMessages: [],
|
|
102
|
+
fetch: (_url, options) => {
|
|
103
|
+
const { messages } = JSON.parse(options!.body! as string)
|
|
104
|
+
return genAIResponse({
|
|
105
|
+
data: {
|
|
106
|
+
messages,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
})
|
|
348
111
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
) : (
|
|
387
|
-
<div className="flex-1 flex items-center justify-center px-4">
|
|
388
|
-
<div className="text-center max-w-3xl mx-auto w-full">
|
|
389
|
-
<h1 className="text-6xl font-bold mb-4 bg-gradient-to-r from-orange-500 to-red-600 text-transparent bg-clip-text uppercase">
|
|
390
|
-
<span className="text-white">TanStack</span> Chat
|
|
391
|
-
</h1>
|
|
392
|
-
<p className="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
|
|
393
|
-
You can ask me about anything, I might or might not have a good
|
|
394
|
-
answer, but you can still ask.
|
|
395
|
-
</p>
|
|
396
|
-
<form onSubmit={handleSubmit}>
|
|
397
|
-
<div className="relative max-w-xl mx-auto">
|
|
398
|
-
<textarea
|
|
399
|
-
value={input}
|
|
400
|
-
onChange={handleInputChange}
|
|
401
|
-
onKeyDown={(e) => {
|
|
402
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
403
|
-
e.preventDefault()
|
|
404
|
-
handleSubmit(e)
|
|
405
|
-
}
|
|
406
|
-
}}
|
|
407
|
-
placeholder="Type something clever (or don't, we won't judge)..."
|
|
408
|
-
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"
|
|
409
|
-
rows={1}
|
|
410
|
-
style={{ minHeight: '88px' }}
|
|
411
|
-
/>
|
|
412
|
-
<button
|
|
413
|
-
type="submit"
|
|
414
|
-
disabled={!input.trim() || isLoading}
|
|
415
|
-
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"
|
|
416
|
-
>
|
|
417
|
-
<Send className="w-4 h-4" />
|
|
418
|
-
</button>
|
|
419
|
-
</div>
|
|
420
|
-
</form>
|
|
112
|
+
const Layout = messages.length ? ChattingLayout : InitalLayout
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="relative flex h-[calc(100vh-32px)] bg-gray-900">
|
|
116
|
+
<div className="flex-1 flex flex-col">
|
|
117
|
+
<Messages messages={messages} />
|
|
118
|
+
|
|
119
|
+
<Layout>
|
|
120
|
+
<form onSubmit={handleSubmit}>
|
|
121
|
+
<div className="relative max-w-xl mx-auto">
|
|
122
|
+
<textarea
|
|
123
|
+
value={input}
|
|
124
|
+
onChange={handleInputChange}
|
|
125
|
+
placeholder="Type something clever (or don't, we won't judge)..."
|
|
126
|
+
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"
|
|
127
|
+
rows={1}
|
|
128
|
+
style={{ minHeight: '44px', maxHeight: '200px' }}
|
|
129
|
+
onInput={(e) => {
|
|
130
|
+
const target = e.target as HTMLTextAreaElement
|
|
131
|
+
target.style.height = 'auto'
|
|
132
|
+
target.style.height =
|
|
133
|
+
Math.min(target.scrollHeight, 200) + 'px'
|
|
134
|
+
}}
|
|
135
|
+
onKeyDown={(e) => {
|
|
136
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
handleSubmit(e)
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
<button
|
|
143
|
+
type="submit"
|
|
144
|
+
disabled={!input.trim()}
|
|
145
|
+
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"
|
|
146
|
+
>
|
|
147
|
+
<Send className="w-4 h-4" />
|
|
148
|
+
</button>
|
|
421
149
|
</div>
|
|
422
|
-
</
|
|
423
|
-
|
|
150
|
+
</form>
|
|
151
|
+
</Layout>
|
|
424
152
|
</div>
|
|
425
|
-
|
|
426
|
-
{/* Settings Dialog */}
|
|
427
|
-
<SettingsDialog
|
|
428
|
-
isOpen={isSettingsOpen}
|
|
429
|
-
onClose={() => setIsSettingsOpen(false)}
|
|
430
|
-
/>
|
|
431
153
|
</div>
|
|
432
154
|
)
|
|
433
155
|
}
|
|
434
156
|
|
|
435
157
|
export const Route = createFileRoute('/example/chat')({
|
|
436
|
-
component:
|
|
158
|
+
component: ChatPage,
|
|
437
159
|
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Link, createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import guitars from '../../data/example-guitars'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/example/guitars/$guitarId')({
|
|
5
|
+
component: RouteComponent,
|
|
6
|
+
loader: async ({ params }) => {
|
|
7
|
+
const guitar = guitars.find((guitar) => guitar.id === +params.guitarId)
|
|
8
|
+
if (!guitar) {
|
|
9
|
+
throw new Error('Guitar not found')
|
|
10
|
+
}
|
|
11
|
+
return guitar
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function RouteComponent() {
|
|
16
|
+
const guitar = Route.useLoaderData()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="relative min-h-[100vh] flex items-center bg-black text-white p-5">
|
|
20
|
+
<div className="relative z-10 w-[60%] bg-gray-900/60 backdrop-blur-md rounded-2xl p-8 border border-gray-800/50 shadow-xl">
|
|
21
|
+
<Link
|
|
22
|
+
to="/example/guitars"
|
|
23
|
+
className="inline-block mb-4 text-emerald-400 hover:text-emerald-300"
|
|
24
|
+
>
|
|
25
|
+
← Back to all guitars
|
|
26
|
+
</Link>
|
|
27
|
+
<h1 className="text-3xl font-bold mb-4">{guitar.name}</h1>
|
|
28
|
+
<p className="text-gray-300 mb-6">{guitar.description}</p>
|
|
29
|
+
<div className="flex items-center justify-between">
|
|
30
|
+
<div className="text-2xl font-bold text-emerald-400">
|
|
31
|
+
${guitar.price}
|
|
32
|
+
</div>
|
|
33
|
+
<button className="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-lg transition-colors">
|
|
34
|
+
Add to Cart
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="absolute top-0 right-0 w-[55%] h-full z-0">
|
|
40
|
+
<div className="w-full h-full overflow-hidden rounded-2xl border-4 border-gray-800 shadow-2xl">
|
|
41
|
+
<img
|
|
42
|
+
src={guitar.image}
|
|
43
|
+
alt={guitar.name}
|
|
44
|
+
className="w-full h-full object-cover guitar-image"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Link, createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import guitars from '../../data/example-guitars'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/example/guitars/')({
|
|
5
|
+
component: App,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export default function App() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="bg-black text-white p-5">
|
|
11
|
+
<h1 className="text-3xl font-bold mb-8 text-center">Featured Guitars</h1>
|
|
12
|
+
<div className="flex flex-wrap gap-12 justify-center">
|
|
13
|
+
{guitars.map((guitar) => (
|
|
14
|
+
<div
|
|
15
|
+
key={guitar.id}
|
|
16
|
+
className="w-full md:w-[calc(50%-1.5rem)] xl:w-[calc(33.333%-2rem)] relative mb-24"
|
|
17
|
+
>
|
|
18
|
+
<Link
|
|
19
|
+
to="/example/guitars/$guitarId"
|
|
20
|
+
params={{
|
|
21
|
+
guitarId: guitar.id.toString(),
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<div className="relative z-0 w-full aspect-square mb-8">
|
|
25
|
+
<div className="w-full h-full overflow-hidden rounded-2xl border-4 border-gray-800 shadow-2xl">
|
|
26
|
+
<img
|
|
27
|
+
src={guitar.image}
|
|
28
|
+
alt={guitar.name}
|
|
29
|
+
className="w-full h-full object-cover guitar-image group-hover:scale-105 transition-transform duration-500"
|
|
30
|
+
/>
|
|
31
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-emerald-500/80 text-white px-4 py-2 rounded-full text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 backdrop-blur-sm">
|
|
35
|
+
View Details
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="absolute bottom-0 right-0 z-10 w-[80%] bg-gray-900/60 backdrop-blur-md rounded-2xl p-5 border border-gray-800/50 shadow-xl transform translate-y-[40%]">
|
|
40
|
+
<h2 className="text-xl font-bold mb-2">{guitar.name}</h2>
|
|
41
|
+
<p className="text-gray-300 mb-3 line-clamp-2">
|
|
42
|
+
{guitar.shortDescription}
|
|
43
|
+
</p>
|
|
44
|
+
<div className="text-xl font-bold text-emerald-400">
|
|
45
|
+
${guitar.price}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</Link>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|