1- import { useEffect , useMemo , useRef , useState } from "react" ;
1+ import { useEffect , useMemo , useState } from "react" ;
22import { useParams } from "react-router" ;
33import { Fragment } from "react/jsx-runtime" ;
44import { useStableSearchParams } from "../../../../hooks/useStableSearchParams" ;
@@ -7,12 +7,11 @@ import {
77 useGetIncidentAgentsQuery
88} from "../../../../redux/services/digma" ;
99import type { GetIncidentAgentEventsResponse } from "../../../../redux/services/types" ;
10- import { isBoolean } from "../../../../typeGuards/isBoolean" ;
1110import { isNumber } from "../../../../typeGuards/isNumber" ;
12- import { isUndefined } from "../../../../typeGuards/isUndefined" ;
1311import { ThreeCirclesSpinner } from "../../../common/ThreeCirclesSpinner" ;
1412import { Spinner } from "../../../common/v3/Spinner" ;
1513import { TypingMarkdown } from "../TypingMarkdown" ;
14+ import { useAutoScroll } from "../useAutoScroll" ;
1615import { convertToMarkdown } from "../utils/convertToMarkdown" ;
1716import { Accordion } from "./Accordion" ;
1817import * as s from "./styles" ;
@@ -25,12 +24,9 @@ export const AgentEvents = () => {
2524 const incidentId = params . id ;
2625 const [ searchParams ] = useStableSearchParams ( ) ;
2726 const agentId = searchParams . get ( "agent" ) ;
28- const [ initialAgentRunning , setInitialAgentRunning ] = useState < boolean > ( ) ;
27+ const [ initialEventsCount , setInitialEventsCount ] = useState < number > ( ) ;
2928 const [ eventsVisibleCount , setEventsVisibleCount ] = useState < number > ( ) ;
30- const [ data , setData ] = useState < GetIncidentAgentEventsResponse > ( ) ;
31- const containerRef = useRef < HTMLDivElement | null > ( null ) ;
32- const [ shouldAutoScroll , setShouldAutoScroll ] = useState ( true ) ;
33- const scrollHeightRef = useRef < number > ( 0 ) ;
29+ const { elementRef, handleElementScroll } = useAutoScroll < HTMLDivElement > ( ) ;
3430
3531 const { data : agentsData } = useGetIncidentAgentsQuery (
3632 { id : incidentId ?? "" } ,
@@ -44,12 +40,12 @@ export const AgentEvents = () => {
4440 { incidentId : incidentId ?? "" , agentId : agentId ?? "" } ,
4541 {
4642 pollingInterval : REFRESH_INTERVAL ,
47- skip : ! incidentId || ! agentId || ! isBoolean ( initialAgentRunning )
43+ skip : ! incidentId || ! agentId
4844 }
4945 ) ;
5046
5147 const handleMarkdownTypingComplete = ( i : number ) => ( ) => {
52- const events = data ?? [ ] ;
48+ const events = agentEventsData ?? [ ] ;
5349 const tokenEventsIndexes = events . reduce ( ( acc , event , index ) => {
5450 if ( event . type === "token" ) {
5551 acc . push ( index ) ;
@@ -66,129 +62,73 @@ export const AgentEvents = () => {
6662 }
6763 } ;
6864
69- const handleContainerScroll = ( ) => {
70- const isAtBottom = ( ) => {
71- if ( ! containerRef . current ) {
72- return false ;
73- }
74- const { scrollTop, scrollHeight, clientHeight } = containerRef . current ;
75- return scrollHeight - scrollTop <= clientHeight + 1 ; // Allow a small buffer for precision issues
76- } ;
77-
78- if ( ! containerRef . current ) {
79- return ;
80- }
81-
82- setShouldAutoScroll ( isAtBottom ( ) ) ;
83- } ;
84-
85- const scrollToBottom = ( ) => {
86- if ( containerRef . current ) {
87- containerRef . current . scrollTop = containerRef . current . scrollHeight ;
88- }
89- } ;
90-
91- // Handle scroll height changes and auto-scroll
9265 useEffect ( ( ) => {
93- const element = containerRef . current ;
94- if ( ! element ) {
95- return ;
66+ if ( agentEventsData ) {
67+ setInitialEventsCount ( ( prev ) =>
68+ ! isNumber ( prev ) ? agentEventsData . length : prev
69+ ) ;
70+ setEventsVisibleCount ( agentEventsData . length ) ;
9671 }
97-
98- const checkScrollHeight = ( ) => {
99- const currentScrollHeight = element . scrollHeight ;
100-
101- // Only auto-scroll if height has grown and auto-scroll is enabled
102- if ( currentScrollHeight > scrollHeightRef . current && shouldAutoScroll ) {
103- scrollToBottom ( ) ;
104- }
105-
106- scrollHeightRef . current = currentScrollHeight ;
107- } ;
108-
109- const mutationObserver = new MutationObserver ( ( ) => {
110- // Use RAF to ensure DOM is updated before measuring
111- requestAnimationFrame ( checkScrollHeight ) ;
112- } ) ;
113-
114- mutationObserver . observe ( element , {
115- childList : true ,
116- subtree : true ,
117- attributes : true ,
118- characterData : true
119- } ) ;
120-
121- // Initial setup
122- scrollHeightRef . current = element . scrollHeight ;
123- scrollToBottom ( ) ;
124-
125- return ( ) => {
126- mutationObserver . disconnect ( ) ;
127- } ;
128- } , [ shouldAutoScroll ] ) ;
129-
130- useEffect ( ( ) => {
131- setData ( agentEventsData ) ;
13272 } , [ agentEventsData ] ) ;
13373
134- // Set agent initial running state
135- useEffect ( ( ) => {
136- if ( ! isBoolean ( initialAgentRunning ) ) {
137- const agent = agentsData ?. agents . find ( ( x ) => x . name === agentId ) ;
138- setInitialAgentRunning ( agent ?. running ) ;
139- }
140- } , [ agentsData , agentId , initialAgentRunning ] ) ;
141-
142- // Set initial visible count based on agent initial running state
143- useEffect ( ( ) => {
144- if (
145- isBoolean ( initialAgentRunning ) &&
146- isUndefined ( eventsVisibleCount ) &&
147- data
148- ) {
149- const initialCount = initialAgentRunning ? 1 : data . length ;
150- setEventsVisibleCount ( initialCount ) ;
151- }
152- } , [ initialAgentRunning , eventsVisibleCount , data ] ) ;
153-
15474 const visibleEvents = useMemo (
15575 ( ) =>
156- data && isNumber ( eventsVisibleCount )
157- ? data . slice ( 0 , eventsVisibleCount )
76+ agentEventsData && isNumber ( eventsVisibleCount )
77+ ? agentEventsData . slice ( 0 , eventsVisibleCount )
15878 : [ ] ,
159- [ data , eventsVisibleCount ]
79+ [ agentEventsData , eventsVisibleCount ]
80+ ) ;
81+
82+ const isAgentRunning = useMemo (
83+ ( ) => Boolean ( agentsData ?. agents . find ( ( x ) => x . name === agentId ) ?. running ) ,
84+ [ agentsData , agentId ]
16085 ) ;
16186
87+ const shouldShowTypingForEvent = ( index : number ) =>
88+ isNumber ( initialEventsCount ) && index >= initialEventsCount ;
89+
90+ const renderEvent = (
91+ event : GetIncidentAgentEventsResponse [ number ] ,
92+ i : number
93+ ) => {
94+ switch ( event . type ) {
95+ case "token" :
96+ return (
97+ < TypingMarkdown
98+ text = { event . message }
99+ onComplete = {
100+ shouldShowTypingForEvent ( i )
101+ ? handleMarkdownTypingComplete ( i )
102+ : undefined
103+ }
104+ speed = { shouldShowTypingForEvent ( i ) ? TYPING_SPEED : undefined }
105+ />
106+ ) ;
107+ case "tool" :
108+ return (
109+ < Accordion
110+ summary = { `${ event . tool_name } (${ [ event . mcp_name , "MCP tool" ]
111+ . filter ( Boolean )
112+ . join ( " " ) } )`}
113+ content = { < TypingMarkdown text = { convertToMarkdown ( event . message ) } /> }
114+ />
115+ ) ;
116+ default :
117+ return null ;
118+ }
119+ } ;
120+
162121 return (
163- < s . Container ref = { containerRef } onScroll = { handleContainerScroll } >
164- { ! data && isLoading && (
122+ < s . Container ref = { elementRef } onScroll = { handleElementScroll } >
123+ { ! agentEventsData && isLoading && (
165124 < s . LoadingContainer >
166125 < Spinner size = { 32 } />
167126 </ s . LoadingContainer >
168127 ) }
169128 { visibleEvents . map ( ( x , i ) => (
170- < Fragment key = { i } >
171- { x . type === "tool" ? (
172- < Accordion
173- summary = { `${ x . tool_name } (${ [ x . mcp_name , "MCP tool" ]
174- . filter ( Boolean )
175- . join ( " " ) } )`}
176- content = { < TypingMarkdown text = { convertToMarkdown ( x . message ) } /> }
177- />
178- ) : (
179- < TypingMarkdown
180- text = { x . message }
181- onComplete = {
182- initialAgentRunning
183- ? handleMarkdownTypingComplete ( i )
184- : undefined
185- }
186- speed = { initialAgentRunning ? TYPING_SPEED : undefined }
187- />
188- ) }
189- </ Fragment >
129+ < Fragment key = { i } > { renderEvent ( x , i ) } </ Fragment >
190130 ) ) }
191- { initialAgentRunning && < ThreeCirclesSpinner /> }
131+ { isAgentRunning && < ThreeCirclesSpinner /> }
192132 </ s . Container >
193133 ) ;
194134} ;
0 commit comments