1
1
import { createFileRoute } from '@tanstack/react-router'
2
2
import { useEffect , useState , useRef } from 'react'
3
- import { PlusCircle, MessageCircle, ChevronLeft, ChevronRight, Trash2, X, Menu, Send, Settings, User, LogOut, Edit2 } from 'lucide-react'
3
+ import {
4
+ PlusCircle ,
5
+ MessageCircle ,
6
+ Trash2 ,
7
+ Send ,
8
+ Settings ,
9
+ Edit2 ,
10
+ } from 'lucide-react'
4
11
import ReactMarkdown from 'react-markdown'
5
12
import rehypeRaw from 'rehype-raw'
6
13
import rehypeSanitize from 'rehype-sanitize'
7
14
import rehypeHighlight from 'rehype-highlight'
15
+
8
16
import { SettingsDialog } from '../components/demo.SettingsDialog'
9
17
import { useAppState } from '../store/demo.hooks'
10
18
import { store } from '../store/demo.store'
11
- import { genAIResponse, type Message } from '../utils/demo.ai'
12
- import "../demo.index.css"
19
+ import { genAIResponse } from '../utils/demo.ai'
20
+
21
+ import type { Message } from '../utils/demo.ai'
22
+
23
+ import '../demo.index.css'
13
24
14
25
function Home ( ) {
15
26
const {
@@ -23,22 +34,23 @@ function Home() {
23
34
addMessage,
24
35
setLoading,
25
36
getCurrentConversation,
26
- getActivePrompt
37
+ getActivePrompt,
27
38
} = useAppState ( )
28
39
29
40
const currentConversation = getCurrentConversation ( store . state )
30
41
const messages = currentConversation ?. messages || [ ]
31
-
42
+
32
43
// Local state
33
44
const [ input , setInput ] = useState ( '' )
34
45
const [ editingChatId , setEditingChatId ] = useState < string | null > ( null )
35
46
const [ isSettingsOpen , setIsSettingsOpen ] = useState ( false )
36
- const [isDropdownOpen, setIsDropdownOpen] = useState(false)
37
47
const messagesContainerRef = useRef < HTMLDivElement > ( null )
48
+ const [ pendingMessage , setPendingMessage ] = useState < Message | null > ( null )
38
49
39
50
const scrollToBottom = ( ) => {
40
51
if ( messagesContainerRef . current ) {
41
- messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
52
+ messagesContainerRef . current . scrollTop =
53
+ messagesContainerRef . current . scrollHeight
42
54
}
43
55
}
44
56
@@ -50,7 +62,7 @@ function Home() {
50
62
const handleSubmit = async ( e : React . FormEvent ) => {
51
63
e . preventDefault ( )
52
64
if ( ! input . trim ( ) || isLoading ) return
53
-
65
+
54
66
const currentInput = input
55
67
setInput ( '' ) // Clear input early for better UX
56
68
setLoading ( true )
@@ -64,7 +76,7 @@ function Home() {
64
76
const newConversation = {
65
77
id : conversationId ,
66
78
title : currentInput . trim ( ) . slice ( 0 , 30 ) ,
67
- messages: []
79
+ messages : [ ] ,
68
80
}
69
81
addConversation ( newConversation )
70
82
}
@@ -84,29 +96,52 @@ function Home() {
84
96
if ( activePrompt ) {
85
97
systemPrompt = {
86
98
value : activePrompt . content ,
87
- enabled: true
99
+ enabled : true ,
88
100
}
89
101
}
90
102
91
103
// Get AI response
92
104
const response = await genAIResponse ( {
93
105
data : {
94
106
messages : [ ...messages , userMessage ] ,
95
- systemPrompt
96
- }
107
+ systemPrompt,
108
+ } ,
97
109
} )
98
110
99
- if (!response.text?.trim()) {
100
- throw new Error('Received empty response from AI')
111
+ const reader = response . body ?. getReader ( )
112
+ if ( ! reader ) {
113
+ throw new Error ( 'No reader found in response' )
101
114
}
102
115
103
- const assistantMessage: Message = {
116
+ const decoder = new TextDecoder ( )
117
+
118
+ let done = false
119
+ let newMessage = {
104
120
id : ( Date . now ( ) + 1 ) . toString ( ) ,
105
121
role : 'assistant' as const ,
106
- content: response.text
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
+ }
107
139
}
108
140
109
- addMessage(conversationId, assistantMessage)
141
+ setPendingMessage ( null )
142
+ if ( newMessage . content . trim ( ) ) {
143
+ addMessage ( conversationId , newMessage )
144
+ }
110
145
} catch ( error ) {
111
146
console . error ( 'Error:' , error )
112
147
const errorMessage : Message = {
@@ -126,7 +161,7 @@ function Home() {
126
161
const newConversation = {
127
162
id : Date . now ( ) . toString ( ) ,
128
163
title : 'New Chat' ,
129
- messages: []
164
+ messages : [ ] ,
130
165
}
131
166
addConversation ( newConversation )
132
167
}
@@ -184,7 +219,9 @@ function Home() {
184
219
< input
185
220
type = "text"
186
221
value = { chat . title }
187
- onChange ={(e) => handleUpdateChatTitle(chat.id, e.target.value)}
222
+ onChange = { ( e ) =>
223
+ handleUpdateChatTitle ( chat . id , e . target . value )
224
+ }
188
225
onBlur = { ( ) => setEditingChatId ( null ) }
189
226
onKeyDown = { ( e ) => {
190
227
if ( e . key === 'Enter' ) {
@@ -229,37 +266,47 @@ function Home() {
229
266
{ currentConversationId ? (
230
267
< >
231
268
{ /* Messages */ }
232
- <div ref ={messagesContainerRef} className =" flex-1 overflow-y-auto pb-24" >
269
+ < div
270
+ ref = { messagesContainerRef }
271
+ className = "flex-1 overflow-y-auto pb-24"
272
+ >
233
273
< div className = "max-w-3xl mx-auto w-full px-4" >
234
- {messages.map((message) => (
235
- <div
236
- key ={message.id}
237
- className ={ `py-6 ${message.role === ' assistant'
238
- ? ' bg-gradient-to-r from-orange-500/5 to-red-600/5'
239
- : ' bg-transparent'
240
- }`}
241
- >
242
- <div className =" flex items-start gap-4 max-w-3xl mx-auto w-full" >
243
- {message.role === 'assistant' ? (
244
- <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" >
245
- AI
246
- </div >
247
- ) : (
248
- <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" >
249
- Y
274
+ { [ ...messages , pendingMessage ]
275
+ . filter ( ( v ) => v )
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 >
250
306
</ div >
251
- )}
252
- <div className =" flex-1 min-w-0" >
253
- <ReactMarkdown
254
- className =" prose dark:prose-invert max-w-none"
255
- rehypePlugins ={[rehypeRaw, rehypeSanitize, rehypeHighlight]}
256
- >
257
- {message.content}
258
- </ReactMarkdown >
259
307
</ div >
260
308
</ div >
261
- </div >
262
- ))}
309
+ ) ) }
263
310
{ isLoading && (
264
311
< div className = "py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5" >
265
312
< div className = "flex items-start gap-4 max-w-3xl mx-auto w-full" >
@@ -268,16 +315,29 @@ function Home() {
268
315
< div className = "absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center" >
269
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" >
270
317
< div className = "absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 animate-pulse" > </ div >
271
- <span className =" relative z-10 text-sm font-medium text-white" >AI</span >
318
+ < span className = "relative z-10 text-sm font-medium text-white" >
319
+ AI
320
+ </ span >
272
321
</ div >
273
322
</ div >
274
323
</ div >
275
324
< div className = "flex items-center gap-3" >
276
- <div className =" text-gray-400 font-medium text-lg" >Thinking</div >
325
+ < div className = "text-gray-400 font-medium text-lg" >
326
+ Thinking
327
+ </ div >
277
328
< div className = "flex gap-2" >
278
- <div className =" w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style ={{ animationDelay: ' 0ms' }} ></div >
279
- <div className =" w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style ={{ animationDelay: ' 200ms' }} ></div >
280
- <div className =" w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style ={{ animationDelay: ' 400ms' }} ></div >
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 >
281
341
</ div >
282
342
</ div >
283
343
</ div >
@@ -305,9 +365,10 @@ function Home() {
305
365
rows = { 1 }
306
366
style = { { minHeight : '44px' , maxHeight : '200px' } }
307
367
onInput = { ( e ) => {
308
- const target = e.target as HTMLTextAreaElement;
309
- target.style.height = 'auto';
310
- target.style.height = Math.min(target.scrollHeight, 200) + 'px';
368
+ const target = e . target as HTMLTextAreaElement
369
+ target . style . height = 'auto'
370
+ target . style . height =
371
+ Math . min ( target . scrollHeight , 200 ) + 'px'
311
372
} }
312
373
/>
313
374
< button
@@ -329,7 +390,8 @@ function Home() {
329
390
< span className = "text-white" > TanStack</ span > Chat
330
391
</ h1 >
331
392
< p className = "text-gray-400 mb-6 w-2/3 mx-auto text-lg" >
332
- You can ask me about anything, I might or might not have a good answer, but you can still ask.
393
+ You can ask me about anything, I might or might not have a good
394
+ answer, but you can still ask.
333
395
</ p >
334
396
< form onSubmit = { handleSubmit } >
335
397
< div className = "relative max-w-xl mx-auto" >
@@ -370,6 +432,6 @@ function Home() {
370
432
)
371
433
}
372
434
373
- export const Route = createFileRoute('/')({
374
- component: Home
435
+ export const Route = createFileRoute ( '/example/chat ' ) ( {
436
+ component : Home ,
375
437
} )
0 commit comments