@@ -420,6 +420,8 @@ function AiGraphChatPanel() {
420420 > ( [ ] )
421421 const [ typingUserMessage , setTypingUserMessage ] = React . useState ( '' )
422422 const primaryServerNode = graphServerNodes [ 0 ]
423+ const chatScrollRef = React . useRef < HTMLDivElement > ( null )
424+ const chatLockedToBottomRef = React . useRef ( true )
423425
424426 React . useEffect ( ( ) => {
425427 const clientIntervalId = window . setInterval ( ( ) => {
@@ -549,6 +551,37 @@ function AiGraphChatPanel() {
549551 }
550552 } , [ ] )
551553
554+ React . useEffect ( ( ) => {
555+ const element = chatScrollRef . current
556+ if ( ! element ) {
557+ return
558+ }
559+
560+ const handleScroll = ( ) => {
561+ const distanceFromBottom =
562+ element . scrollHeight - element . scrollTop - element . clientHeight
563+
564+ chatLockedToBottomRef . current = distanceFromBottom < 72
565+ }
566+
567+ element . addEventListener ( 'scroll' , handleScroll , { passive : true } )
568+ return ( ) => {
569+ element . removeEventListener ( 'scroll' , handleScroll )
570+ }
571+ } , [ ] )
572+
573+ React . useEffect ( ( ) => {
574+ const frameId = window . requestAnimationFrame ( ( ) => {
575+ const element = chatScrollRef . current
576+
577+ if ( element && chatLockedToBottomRef . current ) {
578+ element . scrollTop = element . scrollHeight
579+ }
580+ } )
581+
582+ return ( ) => window . cancelAnimationFrame ( frameId )
583+ } , [ chatMessages ] )
584+
552585 return (
553586 < div className = "grid w-full min-w-0 max-w-full items-start gap-5 lg:grid-cols-[1.05fr_0.95fr]" >
554587 < AiDemoWindow title = "client graph" >
@@ -643,46 +676,51 @@ function AiGraphChatPanel() {
643676 </ AiDemoWindow >
644677
645678 < AiDemoWindow title = "chat runtime" >
646- < div className = "flex min-h-[26rem] min-w-0 flex-col bg-zinc-50 dark:bg-zinc-900" >
647- < div className = "flex flex-1 flex-col justify-end gap-2.5 p-4" >
648- { chatMessages . map ( ( message ) => (
649- < React . Fragment key = { message . id } >
650- < div className = "ml-auto max-w-[86%] rounded-xl bg-pink-500 px-3 py-2 text-xs font-bold leading-5 text-white shadow-sm" >
651- { message . user }
652- </ div >
653- { message . assistant || message . isStreaming ? (
654- < div className = "max-w-[90%] rounded-xl border border-zinc-200 bg-white px-3 py-2 text-xs leading-5 text-zinc-800 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-200" >
655- { message . assistant }
656- { message . isStreaming ? (
657- < span className = "ml-1 inline-block h-3.5 w-1 rounded-sm bg-pink-500 align-[-0.2rem] motion-safe:animate-pulse" />
658- ) : null }
679+ < div className = "flex h-[26rem] min-w-0 flex-col bg-zinc-50 dark:bg-zinc-900" >
680+ < div
681+ ref = { chatScrollRef }
682+ className = "min-h-0 flex-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
683+ >
684+ < div className = "flex min-h-full flex-col justify-end gap-2.5 p-4" >
685+ { chatMessages . map ( ( message ) => (
686+ < React . Fragment key = { message . id } >
687+ < div className = "ml-auto max-w-[86%] rounded-xl bg-pink-500 px-3 py-2 text-xs font-bold leading-5 text-white shadow-sm" >
688+ { message . user }
659689 </ div >
660- ) : null }
661- </ React . Fragment >
662- ) ) }
663- < div className = "grid gap-2 pt-2 text-xs font-bold sm:grid-cols-2" >
664- { [
665- [ 'event' , 'assistant.delta' ] ,
666- [ 'tool' , 'approval required' ] ,
667- [ 'provider' , aiHeroProviders [ activeProvider ] ] ,
668- [ 'server' , 'TanStack AI' ] ,
669- ] . map ( ( [ label , value ] ) => (
670- < div
671- key = { label }
672- className = "rounded-lg bg-white px-3 py-2 dark:bg-zinc-950"
673- >
674- < p className = "text-[0.58rem] uppercase text-zinc-500 dark:text-zinc-500" >
675- { label }
676- </ p >
677- < p className = "mt-1 truncate text-pink-700 dark:text-pink-300" >
678- { value }
679- </ p >
680- </ div >
690+ { message . assistant || message . isStreaming ? (
691+ < div className = "max-w-[90%] rounded-xl border border-zinc-200 bg-white px-3 py-2 text-xs leading-5 text-zinc-800 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-200" >
692+ { message . assistant }
693+ { message . isStreaming ? (
694+ < span className = "ml-1 inline-block h-3.5 w-1 rounded-sm bg-pink-500 align-[-0.2rem] motion-safe:animate-pulse" />
695+ ) : null }
696+ </ div >
697+ ) : null }
698+ </ React . Fragment >
681699 ) ) }
700+ < div className = "grid gap-2 pt-2 text-xs font-bold sm:grid-cols-2" >
701+ { [
702+ [ 'event' , 'assistant.delta' ] ,
703+ [ 'tool' , 'approval required' ] ,
704+ [ 'provider' , aiHeroProviders [ activeProvider ] ] ,
705+ [ 'server' , 'TanStack AI' ] ,
706+ ] . map ( ( [ label , value ] ) => (
707+ < div
708+ key = { label }
709+ className = "rounded-lg bg-white px-3 py-2 dark:bg-zinc-950"
710+ >
711+ < p className = "text-[0.58rem] uppercase text-zinc-500 dark:text-zinc-500" >
712+ { label }
713+ </ p >
714+ < p className = "mt-1 truncate text-pink-700 dark:text-pink-300" >
715+ { value }
716+ </ p >
717+ </ div >
718+ ) ) }
719+ </ div >
682720 </ div >
683721 </ div >
684722
685- < div className = "border-t border-zinc-200 p-4 dark:border-zinc-800" >
723+ < div className = "shrink-0 border-t border-zinc-200 p-4 dark:border-zinc-800" >
686724 < div
687725 className = {
688726 typingUserMessage
0 commit comments