11'use client' ;
22
3- import { useChat } from 'ai/react' ;
3+ import { useChat } from '@ai-sdk/react' ;
4+ import { DefaultChatTransport , type UIMessage } from 'ai' ;
45import { useState , useEffect } from 'react' ;
56import { Alert , AlertTitle , AlertDescription } from '@/components/ui/alert' ;
67import Markdown from 'react-markdown' ;
78import { MarkdownWrapper } from '@/components/ui/markdown' ;
89import remarkGfm from 'remark-gfm' ;
910import BlurFade from "@/components/ui/blur-fade" ;
10- // import Spinner from "@/components/spinner";
1111import VercelLogo from "@/components/vercel" ;
1212import BrowserbaseLogo from "@/components/browserbase"
1313import FlickeringGrid from '@/components/ui/flickering-grid' ;
1414import FlickeringLoad from '@/components/ui/flickering-load' ;
1515import { Prompts } from '@/components/prompts' ;
1616
17+ function messageText ( m : UIMessage ) : string {
18+ return m . parts
19+ . filter ( ( p ) : p is { type : 'text' ; text : string } => p . type === 'text' )
20+ . map ( ( p ) => p . text )
21+ . join ( '' ) ;
22+ }
23+
24+ function isToolUIPart ( p : UIMessage [ 'parts' ] [ number ] ) : boolean {
25+ return typeof p . type === 'string' && p . type . startsWith ( 'tool-' ) ;
26+ }
27+
28+ type ToolPartLoose = {
29+ type : string ;
30+ state ?: string ;
31+ input ?: Record < string , unknown > ;
32+ output ?: Record < string , unknown > ;
33+ } ;
34+
1735export default function Chat ( ) {
18- const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat ( {
19- maxSteps : 5 ,
36+ const [ input , setInput ] = useState ( '' ) ;
37+ const { messages, sendMessage, status } = useChat ( {
38+ transport : new DefaultChatTransport ( { api : '/api/chat' } ) ,
2039 } ) ;
2140
41+ const isLoading = status === 'streaming' || status === 'submitted' ;
42+
2243 const [ showAlert , setShowAlert ] = useState ( false ) ;
2344 const [ statusMessage , setStatusMessage ] = useState ( '' ) ;
2445 const [ sessionId , setSessionId ] = useState < string | null > ( null ) ;
2546 const [ hasInteracted , setHasInteracted ] = useState ( false ) ;
2647
48+ const lastMessage = messages [ messages . length - 1 ] ;
49+ const lastText = lastMessage ? messageText ( lastMessage ) : '' ;
50+
2751 const isGenerating =
2852 isLoading &&
2953 ( ! messages . length ||
30- messages [ messages . length - 1 ] . role !== 'assistant' ||
31- ! messages [ messages . length - 1 ] . content ) ;
54+ lastMessage ? .role !== 'assistant' ||
55+ ! lastText ) ;
3256
3357 useEffect ( ( ) => {
34- const lastMessage = messages [ messages . length - 1 ] ;
35-
3658 if ( isGenerating ) {
3759 setShowAlert ( true ) ;
3860
39- // Check if any tool invocation has dataCollected = true
40- const dataCollected = lastMessage ?. toolInvocations ?. some (
41- invocation => 'result' in invocation &&
42- typeof invocation . result === 'object' &&
43- invocation . result !== null &&
44- 'dataCollected' in invocation . result &&
45- invocation . result . dataCollected === true
46- ) ;
47-
48- if ( dataCollected && ! lastMessage . content ) {
49- // The AI has collected data and is generating a response
61+ const dataCollected = lastMessage ?. parts . some ( ( part ) => {
62+ if ( ! isToolUIPart ( part ) ) return false ;
63+ const tp = part as ToolPartLoose ;
64+ const out = tp . state === 'output-available' ? tp . output : undefined ;
65+ return (
66+ out &&
67+ typeof out === 'object' &&
68+ 'dataCollected' in out &&
69+ ( out as { dataCollected ?: boolean } ) . dataCollected === true
70+ ) ;
71+ } ) ;
72+
73+ if ( dataCollected && ! lastText ) {
5074 setStatusMessage ( 'The AI has collected data and is generating a response. Please wait.' ) ;
5175 } else {
52- // The AI is currently processing the request
5376 setStatusMessage ( 'The AI is currently processing your request. Please wait.' ) ;
5477 }
5578
5679 setSessionId ( null ) ;
5780 } else {
5881 setShowAlert ( false ) ;
5982 }
60- } , [ isGenerating , messages ] ) ;
83+ } , [ isGenerating , messages , lastMessage , lastText ] ) ;
6184
6285 useEffect ( ( ) => {
63- const lastMessage = messages [ messages . length - 1 ] ;
64- if ( lastMessage ?. toolInvocations ) {
65- for ( const invocation of lastMessage . toolInvocations ) {
66- if ( 'result' in invocation && invocation . result ?. sessionId ) {
67- setSessionId ( invocation . result . sessionId ) ;
68- break ;
69- }
86+ if ( ! lastMessage ?. parts ) return ;
87+ for ( const part of lastMessage . parts ) {
88+ if ( ! isToolUIPart ( part ) ) continue ;
89+ const tp = part as ToolPartLoose ;
90+ if ( tp . state !== 'output-available' ) continue ;
91+ const out = tp . output as { sessionId ?: string } | undefined ;
92+ if ( out ?. sessionId ) {
93+ setSessionId ( out . sessionId ) ;
94+ break ;
7095 }
7196 }
72- } , [ messages ] ) ;
97+ } , [ messages , lastMessage ] ) ;
7398
7499 const handleSubmitWrapper = ( e : React . FormEvent < HTMLFormElement > ) => {
75100 e . preventDefault ( ) ;
76- setHasInteracted ( true ) ;
77- handleSubmit ( e , { data : { message : input } } ) ;
101+ if ( ! input . trim ( ) ) return ;
102+ setHasInteracted ( true ) ;
103+ void sendMessage ( { text : input } ) ;
104+ setInput ( '' ) ;
78105 } ;
79106
80107 const handlePromptClick = ( text : string ) => {
81108 setHasInteracted ( true ) ;
82- // Set the input value
83- handleInputChange ( { target : { value : text } } as React . ChangeEvent < HTMLInputElement > ) ;
84- // Submit the form after a short delay
85- setTimeout ( ( ) => {
86- const submitButton = document . querySelector ( 'button[type="submit"]' ) as HTMLButtonElement ;
87- if ( submitButton ) {
88- submitButton . click ( ) ;
89- }
90- } , 100 ) ; // 100ms delay, adjust as needed
109+ void sendMessage ( { text } ) ;
91110 } ;
92111
93112 return (
@@ -125,31 +144,42 @@ export default function Chat() {
125144 < Prompts onPromptClick = { handlePromptClick } />
126145 </ div >
127146 ) : (
128- messages . map ( ( m , index ) => (
147+ messages . map ( ( m , index ) => {
148+ const text = messageText ( m ) ;
149+ const toolParts = m . parts . filter ( isToolUIPart ) ;
150+
151+ return (
129152 < div key = { m . id } className = "whitespace-pre-wrap" >
130153 { m . role === 'user' ? (
131154 < >
132155 < strong className = "block mb-0 text-xl pb-2" > User:</ strong >
133- < p className = "mt-0 pb-4 font-mono" > { m . content } </ p >
156+ < p className = "mt-0 pb-4 font-mono" > { text } </ p >
134157 </ >
135- ) : m . toolInvocations ? (
158+ ) : toolParts . length > 0 ? (
136159 < BlurFade >
137160 < Alert className = "my-4 border-[#E5E7EB]" >
138161 < AlertDescription >
139- { m . toolInvocations ?. map ( ( invocation , index ) => {
162+ { toolParts . map ( ( part , partIndex ) => {
163+ const tp = part as ToolPartLoose ;
164+ const input = tp . input ;
165+ const out =
166+ tp . state === 'output-available'
167+ ? tp . output
168+ : undefined ;
169+
140170 let content = '' ;
141- if ( 'result' in invocation ) {
142- if ( invocation . result ?. sessionId ) {
143- content = `Session ID: ${ invocation . result . sessionId } ` ;
144- } else if ( invocation . result ?. content ) {
145- content = `Content: ${ invocation . result . content } ` ;
146- }
171+ if ( out ?. sessionId ) {
172+ content = `Session ID: ${ String ( out . sessionId ) } ` ;
173+ } else if ( out ?. content ) {
174+ content = `Content: ${ String ( out . content ) } ` ;
147175 }
148- if ( invocation . args ?. debuggerFullscreenUrl ) {
176+
177+ const debuggerUrl = input ?. debuggerFullscreenUrl ;
178+ if ( typeof debuggerUrl === 'string' ) {
149179 return (
150- < div key = { index } >
180+ < div key = { partIndex } >
151181 < iframe
152- src = { `${ invocation . args . debuggerFullscreenUrl } &navBar=false` }
182+ src = { `${ debuggerUrl } &navBar=false` }
153183 className = "w-full sm:h-72 h-52"
154184 title = "Debugger"
155185 sandbox = "allow-same-origin allow-scripts"
@@ -159,7 +189,7 @@ export default function Chat() {
159189 ) ;
160190 }
161191 return content ? (
162- < div key = { index } className = "overflow-x-auto" >
192+ < div key = { partIndex } className = "overflow-x-auto" >
163193 < pre className = "whitespace-pre-wrap break-all" > { content } </ pre >
164194 </ div >
165195 ) : null ;
@@ -182,36 +212,40 @@ export default function Chat() {
182212 } `}
183213 >
184214 < MarkdownWrapper >
185- < Markdown remarkPlugins = { [ remarkGfm ] } > { m . content } </ Markdown >
215+ < Markdown remarkPlugins = { [ remarkGfm ] } > { text } </ Markdown >
186216 </ MarkdownWrapper >
187217 </ div >
188218 </ >
189219 ) }
190220 </ div >
191- ) )
221+ ) ;
222+ } )
192223 ) }
193224
194- { showAlert && ! sessionId && (
225+ { showAlert && ! sessionId && lastMessage ?. role === 'assistant' && (
195226 < BlurFade >
196227 < Alert className = "my-4 border-[#E5E7EB] mb-20" >
197228 < div className = "flex justify-between items-center" >
198229 < div >
199230 < AlertTitle >
200- { messages [ messages . length - 1 ] . toolInvocations
201- ?. map ( ( invocation ) => {
202- if ( 'result' in invocation ) {
203- return invocation . result ?. toolName ;
204- }
205- return invocation . args ?. toolName ;
231+ { lastMessage . parts
232+ . filter ( isToolUIPart )
233+ . map ( ( part ) => {
234+ const tp = part as ToolPartLoose ;
235+ const out =
236+ tp . state === 'output-available'
237+ ? ( tp . output as { toolName ?: string } | undefined )
238+ : undefined ;
239+ if ( out ?. toolName ) return out . toolName ;
240+ const input = tp . input as { toolName ?: string } | undefined ;
241+ return input ?. toolName ;
206242 } )
207243 . filter ( Boolean )
208244 . join ( ', ' ) }
209245 </ AlertTitle >
210246 < AlertDescription > { statusMessage } </ AlertDescription >
211247 </ div >
212- { /* <div role="status" className="w-[70px] h-[40px]"> */ }
213248 < FlickeringLoad height = { 50 } width = { 60 } className = 'p-1' />
214- { /* </div> */ }
215249 </ div >
216250 </ Alert >
217251 </ BlurFade >
@@ -228,7 +262,7 @@ export default function Chat() {
228262 className = "w-full p-2 pr-10 border border-[#E5E7EB] transition-all duration-200 ease-in-out shadow-md shadow-gray-300/50 focus:border-red-300 focus:shadow-lg focus:shadow-red-300/40 outline-none"
229263 value = { input }
230264 placeholder = "Ask anything..."
231- onChange = { handleInputChange }
265+ onChange = { ( e ) => setInput ( e . target . value ) }
232266 />
233267 < button
234268 type = "submit"
@@ -244,4 +278,4 @@ export default function Chat() {
244278 </ div >
245279 </ div >
246280 ) ;
247- }
281+ }
0 commit comments