|
1 | | -import {PanelTable} from 'sentry/components/panels/panelTable'; |
| 1 | +import {useCallback} from 'react'; |
| 2 | + |
| 3 | +import {Button} from '@sentry/scraps/button'; |
| 4 | +import {Container, Grid} from '@sentry/scraps/layout'; |
| 5 | +import {Text} from '@sentry/scraps/text'; |
| 6 | +import {Tooltip} from '@sentry/scraps/tooltip'; |
| 7 | + |
| 8 | +import { |
| 9 | + COL_WIDTH_UNDEFINED, |
| 10 | + type GridColumnOrder, |
| 11 | +} from 'sentry/components/tables/gridEditable'; |
| 12 | +import TimeSince from 'sentry/components/timeSince'; |
| 13 | +import {getShortEventId} from 'sentry/utils/events'; |
| 14 | +import {useTraceViewDrawer} from 'sentry/views/insights/agents/components/drawer'; |
| 15 | +import {HeadSortCell} from 'sentry/views/insights/agents/components/headSortCell'; |
| 16 | +import {ModelName} from 'sentry/views/insights/agents/components/modelName'; |
| 17 | +import {useCombinedQuery} from 'sentry/views/insights/agents/hooks/useCombinedQuery'; |
| 18 | +import {useTableCursor} from 'sentry/views/insights/agents/hooks/useTableCursor'; |
| 19 | +import {getAIGenerationsFilter} from 'sentry/views/insights/agents/utils/query'; |
| 20 | +import {Referrer} from 'sentry/views/insights/aiGenerations/views/utils/referrer'; |
| 21 | +import {TextAlignRight} from 'sentry/views/insights/common/components/textAlign'; |
| 22 | +import {useSpans} from 'sentry/views/insights/common/queries/useDiscover'; |
| 23 | +import {PlatformInsightsTable} from 'sentry/views/insights/pages/platform/shared/table'; |
| 24 | +import {SpanFields} from 'sentry/views/insights/types'; |
| 25 | + |
| 26 | +const INITIAL_COLUMN_ORDER = [ |
| 27 | + {key: SpanFields.SPAN_ID, name: 'Span ID', width: 100}, |
| 28 | + { |
| 29 | + key: SpanFields.GEN_AI_REQUEST_MESSAGES, |
| 30 | + name: 'Input / Output', |
| 31 | + width: COL_WIDTH_UNDEFINED, |
| 32 | + }, |
| 33 | + { |
| 34 | + key: SpanFields.GEN_AI_REQUEST_MODEL, |
| 35 | + name: 'Model', |
| 36 | + width: 200, |
| 37 | + }, |
| 38 | + {key: SpanFields.TIMESTAMP, name: 'Timestamp'}, |
| 39 | +] as const; |
| 40 | + |
| 41 | +function getLastUserMessage(messages?: string) { |
| 42 | + if (!messages) { |
| 43 | + return null; |
| 44 | + } |
| 45 | + try { |
| 46 | + const messagesArray = JSON.parse(messages); |
| 47 | + const lastUserMessage = messagesArray.findLast( |
| 48 | + (message: any) => message.role === 'user' |
| 49 | + )?.content; |
| 50 | + return typeof lastUserMessage === 'string' |
| 51 | + ? lastUserMessage |
| 52 | + : lastUserMessage[0].text.toString(); |
| 53 | + } catch (error) { |
| 54 | + return messages; |
| 55 | + } |
| 56 | +} |
2 | 57 |
|
3 | 58 | export function GenerationsTable() { |
| 59 | + const {openTraceViewDrawer} = useTraceViewDrawer({}); |
| 60 | + const query = useCombinedQuery( |
| 61 | + // Only show generation spans that result in a response to the user's message |
| 62 | + `${getAIGenerationsFilter()} AND !span.op:gen_ai.embeddings` |
| 63 | + ); |
| 64 | + |
| 65 | + const {cursor} = useTableCursor(); |
| 66 | + |
| 67 | + const {data, isLoading, error, pageLinks, isPlaceholderData} = useSpans( |
| 68 | + { |
| 69 | + search: query, |
| 70 | + fields: [ |
| 71 | + SpanFields.TRACE, |
| 72 | + SpanFields.SPAN_ID, |
| 73 | + SpanFields.SPAN_STATUS, |
| 74 | + SpanFields.SPAN_DESCRIPTION, |
| 75 | + SpanFields.GEN_AI_REQUEST_MESSAGES, |
| 76 | + SpanFields.GEN_AI_RESPONSE_TEXT, |
| 77 | + SpanFields.GEN_AI_RESPONSE_OBJECT, |
| 78 | + SpanFields.GEN_AI_REQUEST_MODEL, |
| 79 | + SpanFields.TIMESTAMP, |
| 80 | + ], |
| 81 | + sorts: [{field: SpanFields.TIMESTAMP, kind: 'desc'}], |
| 82 | + cursor, |
| 83 | + keepPreviousData: true, |
| 84 | + limit: 20, |
| 85 | + }, |
| 86 | + Referrer.GENERATIONS_TABLE |
| 87 | + ); |
| 88 | + |
| 89 | + type TableData = (typeof data)[number]; |
| 90 | + |
| 91 | + const renderBodyCell = useCallback( |
| 92 | + (column: GridColumnOrder<keyof TableData>, dataRow: TableData) => { |
| 93 | + if (column.key === SpanFields.SPAN_ID) { |
| 94 | + return ( |
| 95 | + <div> |
| 96 | + <Button |
| 97 | + priority="link" |
| 98 | + onClick={() => { |
| 99 | + openTraceViewDrawer(dataRow.trace, dataRow.span_id); |
| 100 | + }} |
| 101 | + > |
| 102 | + {getShortEventId(dataRow.span_id)} |
| 103 | + </Button> |
| 104 | + </div> |
| 105 | + ); |
| 106 | + } |
| 107 | + |
| 108 | + if (column.key === SpanFields.GEN_AI_REQUEST_MESSAGES) { |
| 109 | + const noValueFallback = <Text variant="muted">–</Text>; |
| 110 | + const statusValue = dataRow[SpanFields.SPAN_STATUS]; |
| 111 | + const isError = statusValue && statusValue !== 'ok' && statusValue !== 'unknown'; |
| 112 | + |
| 113 | + const userMessage = getLastUserMessage( |
| 114 | + dataRow[SpanFields.GEN_AI_REQUEST_MESSAGES] |
| 115 | + ); |
| 116 | + const outputValue = |
| 117 | + dataRow[SpanFields.GEN_AI_RESPONSE_TEXT] || |
| 118 | + dataRow[SpanFields.GEN_AI_RESPONSE_OBJECT]; |
| 119 | + return ( |
| 120 | + <div> |
| 121 | + <Grid |
| 122 | + areas={` |
| 123 | + "inputLabel inputValue" |
| 124 | + "outputLabel outputValue" |
| 125 | + `} |
| 126 | + columns="min-content 1fr" |
| 127 | + gap="2xs md" |
| 128 | + > |
| 129 | + <Container area="inputLabel"> |
| 130 | + <Text variant="muted">Input</Text> |
| 131 | + </Container> |
| 132 | + <Container area="inputValue" minWidth="0px"> |
| 133 | + <Tooltip |
| 134 | + title={userMessage} |
| 135 | + disabled={!userMessage} |
| 136 | + showOnlyOnOverflow |
| 137 | + maxWidth={800} |
| 138 | + isHoverable |
| 139 | + > |
| 140 | + <Text ellipsis>{userMessage || noValueFallback}</Text> |
| 141 | + </Tooltip> |
| 142 | + </Container> |
| 143 | + <Container area="outputLabel"> |
| 144 | + <Text variant="muted">Output</Text> |
| 145 | + </Container> |
| 146 | + <Container area="outputValue" minWidth="0px"> |
| 147 | + <Tooltip |
| 148 | + title={outputValue} |
| 149 | + disabled={!outputValue} |
| 150 | + showOnlyOnOverflow |
| 151 | + maxWidth={800} |
| 152 | + isHoverable |
| 153 | + > |
| 154 | + {isError ? ( |
| 155 | + <Text variant="danger">{statusValue}</Text> |
| 156 | + ) : ( |
| 157 | + <Text ellipsis>{outputValue || noValueFallback}</Text> |
| 158 | + )} |
| 159 | + </Tooltip> |
| 160 | + </Container> |
| 161 | + </Grid> |
| 162 | + </div> |
| 163 | + ); |
| 164 | + } |
| 165 | + |
| 166 | + if (column.key === SpanFields.GEN_AI_REQUEST_MODEL) { |
| 167 | + return <ModelName modelId={dataRow[column.key] || '(no value)'} />; |
| 168 | + } |
| 169 | + if (column.key === SpanFields.TIMESTAMP) { |
| 170 | + return ( |
| 171 | + <TextAlignRight> |
| 172 | + <TimeSince unitStyle="extraShort" date={new Date(dataRow.timestamp)} /> |
| 173 | + </TextAlignRight> |
| 174 | + ); |
| 175 | + } |
| 176 | + return <div>{dataRow[column.key]}</div>; |
| 177 | + }, |
| 178 | + [openTraceViewDrawer] |
| 179 | + ); |
| 180 | + |
| 181 | + const renderHeadCell = useCallback((column: GridColumnOrder<keyof TableData>) => { |
| 182 | + return ( |
| 183 | + <HeadSortCell |
| 184 | + align={column.key === SpanFields.TIMESTAMP ? 'right' : 'left'} |
| 185 | + sortKey={column.key} |
| 186 | + forceCellGrow={column.key === SpanFields.GEN_AI_REQUEST_MESSAGES} |
| 187 | + > |
| 188 | + {column.name} |
| 189 | + </HeadSortCell> |
| 190 | + ); |
| 191 | + }, []); |
| 192 | + |
4 | 193 | return ( |
5 | | - <PanelTable headers={['id', 'input/output', 'model', 'cost', 'timestamp']}> |
6 | | - <div>1244</div> |
7 | | - <div> |
8 | | - <div>User Input</div> |
9 | | - <div>Some AI response</div> |
10 | | - </div> |
11 | | - <div>gpt-4o</div> |
12 | | - <div>1244$</div> |
13 | | - <div>2025-01-01 12:00:00</div> |
14 | | - <div>1245</div> |
15 | | - <div> |
16 | | - <div>Another user query</div> |
17 | | - <div>Short AI answer</div> |
18 | | - </div> |
19 | | - <div>gpt-4o</div> |
20 | | - <div>8$</div> |
21 | | - <div>2025-01-01 8:00:00</div> |
22 | | - </PanelTable> |
| 194 | + <PlatformInsightsTable |
| 195 | + data={data} |
| 196 | + stickyHeader |
| 197 | + isLoading={isLoading} |
| 198 | + error={error} |
| 199 | + initialColumnOrder={INITIAL_COLUMN_ORDER as any} |
| 200 | + pageLinks={pageLinks} |
| 201 | + grid={{ |
| 202 | + renderBodyCell, |
| 203 | + renderHeadCell, |
| 204 | + }} |
| 205 | + isPlaceholderData={isPlaceholderData} |
| 206 | + /> |
23 | 207 | ); |
24 | 208 | } |
0 commit comments