1- import React , { useState } from 'react' ;
1+ import React , { useState , useEffect , useRef } from 'react' ;
22import {
33 Button ,
44} from '@fluentui/react-components' ;
@@ -7,15 +7,36 @@ import ReactMarkdown from "react-markdown";
77import remarkGfm from "remark-gfm" ;
88import rehypePrism from "rehype-prism" ;
99
10- const renderBufferMessage = ( streamingMessageBuffer : string ) => {
10+ interface StreamingBufferMessageProps {
11+ streamingMessageBuffer : string ;
12+ isStreaming ?: boolean ;
13+ }
14+
15+ const renderBufferMessage = ( streamingMessageBuffer : string , isStreaming : boolean = false ) => {
1116 const [ isExpanded , setIsExpanded ] = useState < boolean > ( false ) ;
17+ const [ shouldFade , setShouldFade ] = useState < boolean > ( false ) ;
18+ const contentRef = useRef < HTMLDivElement > ( null ) ;
19+ const prevBufferLength = useRef < number > ( 0 ) ;
1220
13- if ( ! streamingMessageBuffer || streamingMessageBuffer . trim ( ) === "" ) return null ;
21+ // Trigger fade effect when new content is being streamed
22+ useEffect ( ( ) => {
23+ if ( isStreaming && streamingMessageBuffer . length > prevBufferLength . current ) {
24+ setShouldFade ( true ) ;
25+ const timer = setTimeout ( ( ) => setShouldFade ( false ) , 300 ) ;
26+ prevBufferLength . current = streamingMessageBuffer . length ;
27+ return ( ) => clearTimeout ( timer ) ;
28+ }
29+ prevBufferLength . current = streamingMessageBuffer . length ;
30+ } , [ streamingMessageBuffer , isStreaming ] ) ;
1431
15- const start = Math . max ( 0 , streamingMessageBuffer . length - 500 ) ;
16- const previewText = start === 0
17- ? streamingMessageBuffer
18- : "..." + streamingMessageBuffer . substring ( start ) ;
32+ // Auto-scroll to bottom when streaming
33+ useEffect ( ( ) => {
34+ if ( isStreaming && ! isExpanded && contentRef . current ) {
35+ contentRef . current . scrollTop = contentRef . current . scrollHeight ;
36+ }
37+ } , [ streamingMessageBuffer , isStreaming , isExpanded ] ) ;
38+
39+ if ( ! streamingMessageBuffer || streamingMessageBuffer . trim ( ) === "" ) return null ;
1940
2041 return (
2142 < div style = { {
@@ -29,14 +50,20 @@ const renderBufferMessage = (streamingMessageBuffer: string) => {
2950 borderRadius : '8px' ,
3051 padding : '16px' ,
3152 fontSize : '14px' ,
32- lineHeight : '1.5'
53+ lineHeight : '1.5' ,
54+ height : isExpanded ? 'auto' : '256px' , // Auto height when expanded
55+ display : 'flex' ,
56+ flexDirection : 'column' ,
57+ position : 'relative' ,
58+ overflow : isExpanded ? 'visible' : 'hidden' // Allow overflow when expanded
3359 } } >
3460 { /* Header */ }
3561 < div style = { {
3662 display : 'flex' ,
3763 justifyContent : 'space-between' ,
3864 alignItems : 'center' ,
39- marginBottom : isExpanded ? '16px' : '8px'
65+ marginBottom : isExpanded ? '16px' : '16px' ,
66+ flexShrink : 0
4067 } } >
4168 < div style = { {
4269 display : 'flex' ,
@@ -72,57 +99,93 @@ const renderBufferMessage = (streamingMessageBuffer: string) => {
7299 fontSize : '14px'
73100 } }
74101 >
75- Details
102+ { isExpanded ? 'Hide' : ' Details' }
76103 </ Button >
77104 </ div >
78105
79- { /* Preview content when collapsed */ }
106+ { /* Content area - collapsed state */ }
80107 { ! isExpanded && (
81- < div style = { {
82- display : 'flex' ,
83- alignItems : 'flex-start' ,
84- gap : '8px' ,
85- marginLeft : '32px'
86- } } >
87- < ArrowTurnDownRightRegular style = { {
88- color : 'var(--colorNeutralForeground3)' ,
89- fontSize : '14px' ,
90- marginTop : '2px' ,
91- flexShrink : 0
108+ < div
109+ ref = { contentRef }
110+ style = { {
111+ flex : 1 ,
112+ position : 'relative' ,
113+ overflowY : 'hidden' ,
114+ overflowX : 'hidden' ,
115+ paddingLeft : '32px' ,
116+ transition : 'opacity 0.3s ease-in-out' ,
117+ opacity : shouldFade ? 0.6 : 1
118+ } }
119+ >
120+ { /* Top fade overlay for collapsed state */ }
121+ < div style = { {
122+ position : 'absolute' ,
123+ top : 0 ,
124+ left : 0 ,
125+ right : 0 ,
126+ height : '40px' ,
127+ background : 'linear-gradient(to bottom, var(--colorNeutralBackground2), transparent)' ,
128+ pointerEvents : 'none' ,
129+ zIndex : 1
92130 } } />
131+
93132 < div style = { {
94- color : 'var(--colorNeutralForeground2)' ,
95- fontSize : '14px' ,
96- lineHeight : '1.4'
133+ display : 'flex' ,
134+ alignItems : 'flex-end' ,
135+ gap : '8px' ,
136+ height : '100%'
97137 } } >
98- < ReactMarkdown
99- remarkPlugins = { [ remarkGfm ] }
100- rehypePlugins = { [ rehypePrism ] }
101- components = { {
102- a : ( { node, ...props } ) => (
103- < a
104- { ...props }
105- style = { {
106- color : 'var(--colorNeutralBrandForeground1)' ,
107- textDecoration : 'none'
108- } }
109- onMouseEnter = { ( e ) => {
110- e . currentTarget . style . textDecoration = 'underline' ;
111- } }
112- onMouseLeave = { ( e ) => {
113- e . currentTarget . style . textDecoration = 'none' ;
114- } }
115- />
116- )
117- } }
118- >
119- { previewText }
120- </ ReactMarkdown >
138+ < ArrowTurnDownRightRegular style = { {
139+ color : 'var(--colorNeutralForeground3)' ,
140+ fontSize : '14px' ,
141+ marginBottom : '2px' ,
142+ flexShrink : 0
143+ } } />
144+ < div style = { {
145+ color : 'var(--colorNeutralForeground2)' ,
146+ fontSize : '14px' ,
147+ lineHeight : '1.4' ,
148+ height : '100%' ,
149+ overflow : 'hidden' ,
150+ wordWrap : 'break-word' ,
151+ display : 'flex' ,
152+ flexDirection : 'column' ,
153+ justifyContent : 'flex-end' ,
154+ maskImage : 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)' ,
155+ WebkitMaskImage : 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)'
156+ } } >
157+ < ReactMarkdown
158+ remarkPlugins = { [ remarkGfm ] }
159+ rehypePlugins = { [ rehypePrism ] }
160+ components = { {
161+ a : ( { node, ...props } ) => (
162+ < a
163+ { ...props }
164+ style = { {
165+ color : 'var(--colorNeutralBrandForeground1)' ,
166+ textDecoration : 'none'
167+ } }
168+ onMouseEnter = { ( e ) => {
169+ e . currentTarget . style . textDecoration = 'underline' ;
170+ } }
171+ onMouseLeave = { ( e ) => {
172+ e . currentTarget . style . textDecoration = 'none' ;
173+ } }
174+ />
175+ ) ,
176+ p : ( { node, ...props } ) => (
177+ < p { ...props } style = { { margin : '0 0 8px 0' } } />
178+ )
179+ } }
180+ >
181+ { streamingMessageBuffer }
182+ </ ReactMarkdown >
183+ </ div >
121184 </ div >
122185 </ div >
123186 ) }
124187
125- { /* Full content when expanded */ }
188+ { /* Content area - expanded state (original behavior) */ }
126189 { isExpanded && (
127190 < div style = { {
128191 padding : '12px' ,
@@ -131,6 +194,23 @@ const renderBufferMessage = (streamingMessageBuffer: string) => {
131194 < ReactMarkdown
132195 remarkPlugins = { [ remarkGfm ] }
133196 rehypePlugins = { [ rehypePrism ] }
197+ components = { {
198+ a : ( { node, ...props } ) => (
199+ < a
200+ { ...props }
201+ style = { {
202+ color : 'var(--colorNeutralBrandForeground1)' ,
203+ textDecoration : 'none'
204+ } }
205+ onMouseEnter = { ( e ) => {
206+ e . currentTarget . style . textDecoration = 'underline' ;
207+ } }
208+ onMouseLeave = { ( e ) => {
209+ e . currentTarget . style . textDecoration = 'none' ;
210+ } }
211+ />
212+ )
213+ } }
134214 >
135215 { streamingMessageBuffer }
136216 </ ReactMarkdown >
0 commit comments