1
- import { Fragment } from 'react' ;
1
+ import { Fragment , useLayoutEffect , useState } from 'react' ;
2
+ import styled from '@emotion/styled' ;
2
3
import * as Sentry from '@sentry/react' ;
3
4
5
+ import { Button } from 'sentry/components/core/button' ;
4
6
import { t } from 'sentry/locale' ;
7
+ import { space } from 'sentry/styles/space' ;
5
8
import type { EventTransaction } from 'sentry/types/event' ;
6
9
import { defined } from 'sentry/utils' ;
7
10
import useOrganization from 'sentry/utils/useOrganization' ;
11
+ import usePrevious from 'sentry/utils/usePrevious' ;
8
12
import type { TraceItemResponseAttribute } from 'sentry/views/explore/hooks/useTraceItemDetails' ;
9
13
import { hasAgentInsightsFeature } from 'sentry/views/insights/agentMonitoring/utils/features' ;
10
14
import {
@@ -17,6 +21,13 @@ import {TraceDrawerComponents} from 'sentry/views/performance/newTraceDetails/tr
17
21
import type { TraceTree } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree' ;
18
22
import type { TraceTreeNode } from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode' ;
19
23
24
+ type AIMessageRole = 'system' | 'user' | 'assistant' | 'tool' ;
25
+
26
+ interface AIMessage {
27
+ content : React . ReactNode ;
28
+ role : AIMessageRole ;
29
+ }
30
+
20
31
function renderTextMessages ( content : any ) {
21
32
if ( ! Array . isArray ( content ) ) {
22
33
return content ;
@@ -31,7 +42,7 @@ function renderToolMessage(content: any) {
31
42
return content ;
32
43
}
33
44
34
- function parseAIMessages ( messages : string ) {
45
+ function parseAIMessages ( messages : string ) : AIMessage [ ] | string {
35
46
try {
36
47
const array : any [ ] = Array . isArray ( messages ) ? messages : JSON . parse ( messages ) ;
37
48
return array
@@ -134,7 +145,7 @@ function transformPrompt(prompt: string) {
134
145
}
135
146
}
136
147
137
- const roleHeadings = {
148
+ const roleHeadings : Record < AIMessageRole , string > = {
138
149
system : t ( 'System' ) ,
139
150
user : t ( 'User' ) ,
140
151
assistant : t ( 'Assistant' ) ,
@@ -196,30 +207,77 @@ export function AIInputSection({
196
207
< TraceDrawerComponents . MultilineText >
197
208
{ messages }
198
209
</ TraceDrawerComponents . MultilineText >
199
- ) : messages ? (
200
- < Fragment >
201
- { messages . map ( ( message , index ) => (
202
- < Fragment key = { index } >
203
- < TraceDrawerComponents . MultilineTextLabel >
204
- { roleHeadings [ message . role ] }
205
- </ TraceDrawerComponents . MultilineTextLabel >
206
- { typeof message . content === 'string' ? (
207
- < TraceDrawerComponents . MultilineText >
208
- { message . content }
209
- </ TraceDrawerComponents . MultilineText >
210
- ) : (
211
- < TraceDrawerComponents . MultilineJSON
212
- value = { message . content }
213
- maxDefaultDepth = { 2 }
214
- />
215
- ) }
216
- </ Fragment >
217
- ) ) }
218
- </ Fragment >
219
210
) : null }
211
+ { Array . isArray ( messages ) ? < MessagesArrayRenderer messages = { messages } /> : null }
220
212
{ toolArgs ? (
221
213
< TraceDrawerComponents . MultilineJSON value = { toolArgs } maxDefaultDepth = { 1 } />
222
214
) : null }
223
215
</ FoldSection >
224
216
) ;
225
217
}
218
+
219
+ const MAX_MESSAGES_AT_START = 2 ;
220
+ const MAX_MESSAGES_AT_END = 1 ;
221
+ const MAX_MESSAGES_TO_SHOW = MAX_MESSAGES_AT_START + MAX_MESSAGES_AT_END ;
222
+
223
+ /**
224
+ * As the whole message history takes up too much space we only show the first two (as those often contain the system and initial user prompt)
225
+ * and the last messages with the option to expand
226
+ */
227
+ function MessagesArrayRenderer ( { messages} : { messages : AIMessage [ ] } ) {
228
+ const [ isExpanded , setIsExpanded ] = useState ( messages . length <= MAX_MESSAGES_TO_SHOW ) ;
229
+
230
+ // Reset the expanded state when the messages length changes
231
+ const previousMessagesLength = usePrevious ( messages . length ) ;
232
+ useLayoutEffect ( ( ) => {
233
+ if ( previousMessagesLength !== messages . length ) {
234
+ setIsExpanded ( messages . length <= MAX_MESSAGES_TO_SHOW ) ;
235
+ }
236
+ } , [ messages . length , previousMessagesLength ] ) ;
237
+
238
+ const renderMessage = ( message : AIMessage , index : number ) => {
239
+ return (
240
+ < Fragment key = { index } >
241
+ < TraceDrawerComponents . MultilineTextLabel >
242
+ { roleHeadings [ message . role ] }
243
+ </ TraceDrawerComponents . MultilineTextLabel >
244
+ { typeof message . content === 'string' ? (
245
+ < TraceDrawerComponents . MultilineText >
246
+ { message . content }
247
+ </ TraceDrawerComponents . MultilineText >
248
+ ) : (
249
+ < TraceDrawerComponents . MultilineJSON
250
+ value = { message . content }
251
+ maxDefaultDepth = { 2 }
252
+ />
253
+ ) }
254
+ </ Fragment >
255
+ ) ;
256
+ } ;
257
+
258
+ if ( isExpanded ) {
259
+ return messages . map ( renderMessage ) ;
260
+ }
261
+
262
+ return (
263
+ < Fragment >
264
+ { messages . slice ( 0 , MAX_MESSAGES_AT_START ) . map ( renderMessage ) }
265
+ < ButtonDivider >
266
+ < Button onClick = { ( ) => setIsExpanded ( true ) } size = "xs" >
267
+ { t ( '+%s more messages' , messages . length - MAX_MESSAGES_TO_SHOW ) }
268
+ </ Button >
269
+ </ ButtonDivider >
270
+ { messages . slice ( - MAX_MESSAGES_AT_END ) . map ( renderMessage ) }
271
+ </ Fragment >
272
+ ) ;
273
+ }
274
+
275
+ const ButtonDivider = styled ( 'div' ) `
276
+ height: 1px;
277
+ width: 100%;
278
+ border-bottom: 1px dashed ${ p => p . theme . border } ;
279
+ display: flex;
280
+ justify-content: center;
281
+ align-items: center;
282
+ margin: ${ space ( 4 ) } 0;
283
+ ` ;
0 commit comments