11import { createFileRoute } from '@tanstack/react-router'
22import { 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'
411import ReactMarkdown from 'react-markdown'
512import rehypeRaw from 'rehype-raw'
613import rehypeSanitize from 'rehype-sanitize'
714import rehypeHighlight from 'rehype-highlight'
15+
816import { SettingsDialog } from '../components/demo.SettingsDialog'
917import { useAppState } from '../store/demo.hooks'
1018import { 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'
1324
1425function Home ( ) {
1526 const {
@@ -23,22 +34,23 @@ function Home() {
2334 addMessage,
2435 setLoading,
2536 getCurrentConversation,
26- getActivePrompt
37+ getActivePrompt,
2738 } = useAppState ( )
2839
2940 const currentConversation = getCurrentConversation ( store . state )
3041 const messages = currentConversation ?. messages || [ ]
31-
42+
3243 // Local state
3344 const [ input , setInput ] = useState ( '' )
3445 const [ editingChatId , setEditingChatId ] = useState < string | null > ( null )
3546 const [ isSettingsOpen , setIsSettingsOpen ] = useState ( false )
36- const [isDropdownOpen, setIsDropdownOpen] = useState(false)
3747 const messagesContainerRef = useRef < HTMLDivElement > ( null )
48+ const [ pendingMessage , setPendingMessage ] = useState < Message | null > ( null )
3849
3950 const scrollToBottom = ( ) => {
4051 if ( messagesContainerRef . current ) {
41- messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
52+ messagesContainerRef . current . scrollTop =
53+ messagesContainerRef . current . scrollHeight
4254 }
4355 }
4456
@@ -50,7 +62,7 @@ function Home() {
5062 const handleSubmit = async ( e : React . FormEvent ) => {
5163 e . preventDefault ( )
5264 if ( ! input . trim ( ) || isLoading ) return
53-
65+
5466 const currentInput = input
5567 setInput ( '' ) // Clear input early for better UX
5668 setLoading ( true )
@@ -64,7 +76,7 @@ function Home() {
6476 const newConversation = {
6577 id : conversationId ,
6678 title : currentInput . trim ( ) . slice ( 0 , 30 ) ,
67- messages: []
79+ messages : [ ] ,
6880 }
6981 addConversation ( newConversation )
7082 }
@@ -84,29 +96,52 @@ function Home() {
8496 if ( activePrompt ) {
8597 systemPrompt = {
8698 value : activePrompt . content ,
87- enabled: true
99+ enabled : true ,
88100 }
89101 }
90102
91103 // Get AI response
92104 const response = await genAIResponse ( {
93105 data : {
94106 messages : [ ...messages , userMessage ] ,
95- systemPrompt
96- }
107+ systemPrompt,
108+ } ,
97109 } )
98110
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' )
101114 }
102115
103- const assistantMessage: Message = {
116+ const decoder = new TextDecoder ( )
117+
118+ let done = false
119+ let newMessage = {
104120 id : ( Date . now ( ) + 1 ) . toString ( ) ,
105121 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+ }
107139 }
108140
109- addMessage(conversationId, assistantMessage)
141+ setPendingMessage ( null )
142+ if ( newMessage . content . trim ( ) ) {
143+ addMessage ( conversationId , newMessage )
144+ }
110145 } catch ( error ) {
111146 console . error ( 'Error:' , error )
112147 const errorMessage : Message = {
@@ -126,7 +161,7 @@ function Home() {
126161 const newConversation = {
127162 id : Date . now ( ) . toString ( ) ,
128163 title : 'New Chat' ,
129- messages: []
164+ messages : [ ] ,
130165 }
131166 addConversation ( newConversation )
132167 }
@@ -184,7 +219,9 @@ function Home() {
184219 < input
185220 type = "text"
186221 value = { chat . title }
187- onChange ={(e) => handleUpdateChatTitle(chat.id, e.target.value)}
222+ onChange = { ( e ) =>
223+ handleUpdateChatTitle ( chat . id , e . target . value )
224+ }
188225 onBlur = { ( ) => setEditingChatId ( null ) }
189226 onKeyDown = { ( e ) => {
190227 if ( e . key === 'Enter' ) {
@@ -229,37 +266,47 @@ function Home() {
229266 { currentConversationId ? (
230267 < >
231268 { /* 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+ >
233273 < 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 >
250306 </ 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 >
259307 </ div >
260308 </ div >
261- </div >
262- ))}
309+ ) ) }
263310 { isLoading && (
264311 < div className = "py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5" >
265312 < div className = "flex items-start gap-4 max-w-3xl mx-auto w-full" >
@@ -268,16 +315,29 @@ function Home() {
268315 < div className = "absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center" >
269316 < div className = "relative w-full h-full rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center" >
270317 < 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 >
272321 </ div >
273322 </ div >
274323 </ div >
275324 < 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 >
277328 < 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 >
281341 </ div >
282342 </ div >
283343 </ div >
@@ -305,9 +365,10 @@ function Home() {
305365 rows = { 1 }
306366 style = { { minHeight : '44px' , maxHeight : '200px' } }
307367 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'
311372 } }
312373 />
313374 < button
@@ -329,7 +390,8 @@ function Home() {
329390 < span className = "text-white" > TanStack</ span > Chat
330391 </ h1 >
331392 < 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.
333395 </ p >
334396 < form onSubmit = { handleSubmit } >
335397 < div className = "relative max-w-xl mx-auto" >
@@ -370,6 +432,6 @@ function Home() {
370432 )
371433}
372434
373- export const Route = createFileRoute('/')({
374- component: Home
435+ export const Route = createFileRoute ( '/example/chat ' ) ( {
436+ component : Home ,
375437} )
0 commit comments