@carto/ps-react-ui 4.4.0-chat-ui.1 → 4.4.0-chat-ui.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.js +1071 -391
- package/dist/chat.js.map +1 -1
- package/dist/components.js +141 -159
- package/dist/components.js.map +1 -1
- package/dist/copy-button-DGL1tyli.js +26 -0
- package/dist/copy-button-DGL1tyli.js.map +1 -0
- package/dist/index-BnyeR7Qx.js +6601 -0
- package/dist/index-BnyeR7Qx.js.map +1 -0
- package/dist/types/chat/const.d.ts +1 -0
- package/dist/types/chat/feedback/chat-tool-code-area.d.ts +4 -0
- package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
- package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
- package/dist/types/chat/feedback/index.d.ts +4 -1
- package/dist/types/chat/feedback/styles.d.ts +149 -3
- package/dist/types/chat/index.d.ts +6 -3
- package/dist/types/chat/types.d.ts +58 -5
- package/dist/widgets/toolbar-actions.js +101 -6693
- package/dist/widgets/toolbar-actions.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/const.ts +1 -0
- package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
- package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
- package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
- package/src/chat/feedback/chat-tool-group.tsx +156 -0
- package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
- package/src/chat/feedback/chat-tool-trace.tsx +187 -0
- package/src/chat/feedback/get-tool-label.test.tsx +91 -0
- package/src/chat/feedback/get-tool-label.ts +13 -0
- package/src/chat/feedback/index.ts +4 -1
- package/src/chat/feedback/styles.ts +153 -4
- package/src/chat/index.ts +14 -3
- package/src/chat/types.ts +64 -5
- package/dist/types/chat/feedback/chat-tools.d.ts +0 -2
- package/src/chat/feedback/chat-tools.test.tsx +0 -23
- package/src/chat/feedback/chat-tools.tsx +0 -54
package/package.json
CHANGED
package/src/chat/const.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { ChatToolCodeArea } from './chat-tool-code-area'
|
|
4
|
+
|
|
5
|
+
describe('ChatToolCodeArea', () => {
|
|
6
|
+
test('renders code content', () => {
|
|
7
|
+
render(<ChatToolCodeArea content='{"status": 200}' />)
|
|
8
|
+
expect(screen.getByText('{"status": 200}')).toBeTruthy()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('applies error styling via isError prop', () => {
|
|
12
|
+
const { container } = render(
|
|
13
|
+
<ChatToolCodeArea content='Error output' isError />,
|
|
14
|
+
)
|
|
15
|
+
const pre = container.querySelector('pre')
|
|
16
|
+
expect(pre).toBeTruthy()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('does not show Full view button when content fits', () => {
|
|
20
|
+
render(<ChatToolCodeArea content='short' />)
|
|
21
|
+
expect(screen.queryByText('Full view')).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { Box, IconButton, Tooltip } from '@mui/material'
|
|
3
|
+
import type { ChatToolCodeAreaProps } from '../types'
|
|
4
|
+
import { CHAT_TOOL_CODE_AREA_MAX_HEIGHT } from '../const'
|
|
5
|
+
import { ChatToolFullViewDialog } from './chat-tool-full-view-dialog'
|
|
6
|
+
import { styles } from './styles'
|
|
7
|
+
import { OpenDiagonallyRight } from '@carto/meridian-ds/custom-icons'
|
|
8
|
+
|
|
9
|
+
export function ChatToolCodeArea({
|
|
10
|
+
content,
|
|
11
|
+
title = '',
|
|
12
|
+
isError = false,
|
|
13
|
+
labels = {},
|
|
14
|
+
sx,
|
|
15
|
+
}: ChatToolCodeAreaProps & { isError?: boolean }) {
|
|
16
|
+
const preRef = useRef<HTMLPreElement>(null)
|
|
17
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
18
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
19
|
+
|
|
20
|
+
const checkOverflow = useCallback(() => {
|
|
21
|
+
const el = preRef.current
|
|
22
|
+
if (el) {
|
|
23
|
+
setIsOverflowing(el.scrollHeight > el.clientHeight)
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
checkOverflow()
|
|
29
|
+
const el = preRef.current
|
|
30
|
+
if (!el) return
|
|
31
|
+
|
|
32
|
+
const observer = new ResizeObserver(checkOverflow)
|
|
33
|
+
observer.observe(el)
|
|
34
|
+
return () => observer.disconnect()
|
|
35
|
+
}, [checkOverflow, content])
|
|
36
|
+
|
|
37
|
+
const fullViewLabel = labels.fullView ?? 'Full view'
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Box sx={{ ...styles.codeArea, ...sx }}>
|
|
41
|
+
<Box
|
|
42
|
+
component='pre'
|
|
43
|
+
ref={preRef}
|
|
44
|
+
sx={{
|
|
45
|
+
...styles.codeAreaPre,
|
|
46
|
+
maxHeight: CHAT_TOOL_CODE_AREA_MAX_HEIGHT,
|
|
47
|
+
...(isError ? styles.codeAreaPreError : {}),
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{content}
|
|
51
|
+
</Box>
|
|
52
|
+
{isOverflowing && (
|
|
53
|
+
<Tooltip title={fullViewLabel}>
|
|
54
|
+
<IconButton
|
|
55
|
+
size='small'
|
|
56
|
+
onClick={() => setDialogOpen(true)}
|
|
57
|
+
sx={styles.codeAreaFullViewButton}
|
|
58
|
+
>
|
|
59
|
+
<OpenDiagonallyRight />
|
|
60
|
+
</IconButton>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
)}
|
|
63
|
+
<ChatToolFullViewDialog
|
|
64
|
+
open={dialogOpen}
|
|
65
|
+
onClose={() => setDialogOpen(false)}
|
|
66
|
+
title={title}
|
|
67
|
+
content={content}
|
|
68
|
+
/>
|
|
69
|
+
</Box>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { ChatToolFullViewDialog } from './chat-tool-full-view-dialog'
|
|
4
|
+
|
|
5
|
+
describe('ChatToolFullViewDialog', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
open: true,
|
|
8
|
+
onClose: vi.fn(),
|
|
9
|
+
title: 'Tool executed: Output',
|
|
10
|
+
content: '{"status": 200,\n"data": "test"}',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('renders dialog with title when open', () => {
|
|
14
|
+
render(<ChatToolFullViewDialog {...defaultProps} />)
|
|
15
|
+
expect(screen.getByText('Tool executed: Output')).toBeTruthy()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('renders content with line numbers', () => {
|
|
19
|
+
render(<ChatToolFullViewDialog {...defaultProps} />)
|
|
20
|
+
// NOTE: need to query outside of component to reach dialog portal
|
|
21
|
+
const pre = document.querySelector('pre')
|
|
22
|
+
expect(pre).toBeTruthy()
|
|
23
|
+
// NOTE: json text is tokenized and split into various tokens so we cannot reach it fully with `queryByText`
|
|
24
|
+
expect(pre!.textContent).toContain('{"status": 200,')
|
|
25
|
+
expect(pre!.textContent).toContain('"data": "test"}')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('does not render content when closed', () => {
|
|
29
|
+
render(<ChatToolFullViewDialog {...defaultProps} open={false} />)
|
|
30
|
+
expect(screen.queryByText('Tool executed: Output')).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('calls onClose when close button is clicked', () => {
|
|
34
|
+
const onClose = vi.fn()
|
|
35
|
+
render(<ChatToolFullViewDialog {...defaultProps} onClose={onClose} />)
|
|
36
|
+
fireEvent.click(screen.getByLabelText('close'))
|
|
37
|
+
expect(onClose).toHaveBeenCalledOnce()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogTitle,
|
|
5
|
+
DialogContent,
|
|
6
|
+
IconButton,
|
|
7
|
+
Box,
|
|
8
|
+
} from '@mui/material'
|
|
9
|
+
import { Close } from '@mui/icons-material'
|
|
10
|
+
import { CopyButton } from '../../components/copy-button/copy-button'
|
|
11
|
+
import type { ChatToolFullViewDialogProps } from '../types'
|
|
12
|
+
import { styles } from './styles'
|
|
13
|
+
|
|
14
|
+
interface Token {
|
|
15
|
+
type: 'key' | 'string' | 'number' | 'boolean' | 'null' | 'punctuation'
|
|
16
|
+
value: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NUMBER_START_REGEX = /^-?\d/
|
|
20
|
+
|
|
21
|
+
function tokenizeLine(line: string): (Token | string)[] {
|
|
22
|
+
const tokens: (Token | string)[] = []
|
|
23
|
+
let lastIndex = 0
|
|
24
|
+
let match: RegExpExecArray | null
|
|
25
|
+
|
|
26
|
+
// First, find all key positions by looking for "...": patterns
|
|
27
|
+
const keyPositions = new Set<number>()
|
|
28
|
+
const keyRegex = /("(?:[^"\\]|\\.)*")\s*:/g
|
|
29
|
+
let keyMatch: RegExpExecArray | null
|
|
30
|
+
while ((keyMatch = keyRegex.exec(line)) !== null) {
|
|
31
|
+
keyPositions.add(keyMatch.index)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tokenRegex = // eslint-disable-next-line no-useless-escape
|
|
35
|
+
/"(?:[^"\\]|\\.)*"|\b(?:true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\]:,]/g
|
|
36
|
+
|
|
37
|
+
while ((match = tokenRegex.exec(line)) !== null) {
|
|
38
|
+
if (match.index > lastIndex) {
|
|
39
|
+
tokens.push(line.slice(lastIndex, match.index))
|
|
40
|
+
}
|
|
41
|
+
const value = match[0]
|
|
42
|
+
if (value.startsWith('"')) {
|
|
43
|
+
if (keyPositions.has(match.index)) {
|
|
44
|
+
tokens.push({ type: 'key', value })
|
|
45
|
+
} else {
|
|
46
|
+
tokens.push({ type: 'string', value })
|
|
47
|
+
}
|
|
48
|
+
} else if (NUMBER_START_REGEX.test(value)) {
|
|
49
|
+
tokens.push({ type: 'number', value })
|
|
50
|
+
} else if (value === 'true' || value === 'false') {
|
|
51
|
+
tokens.push({ type: 'boolean', value })
|
|
52
|
+
} else if (value === 'null') {
|
|
53
|
+
tokens.push({ type: 'null', value })
|
|
54
|
+
} else {
|
|
55
|
+
tokens.push({ type: 'punctuation', value })
|
|
56
|
+
}
|
|
57
|
+
lastIndex = match.index + value.length
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (lastIndex < line.length) {
|
|
61
|
+
tokens.push(line.slice(lastIndex))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return tokens
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ChatToolFullViewDialog({
|
|
68
|
+
open,
|
|
69
|
+
onClose,
|
|
70
|
+
title,
|
|
71
|
+
content,
|
|
72
|
+
}: ChatToolFullViewDialogProps) {
|
|
73
|
+
const tokenizedLines = useMemo(
|
|
74
|
+
() => content.split('\n').map((line) => tokenizeLine(line)),
|
|
75
|
+
[content],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Dialog
|
|
80
|
+
open={open}
|
|
81
|
+
onClose={onClose}
|
|
82
|
+
sx={styles.fullViewDialog}
|
|
83
|
+
PaperProps={{ sx: styles.fullViewPaper }}
|
|
84
|
+
fullScreen
|
|
85
|
+
>
|
|
86
|
+
<DialogTitle sx={styles.fullViewTitle}>
|
|
87
|
+
{title}
|
|
88
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
89
|
+
<CopyButton copyText={content} aria-label='Copy content' />
|
|
90
|
+
<IconButton onClick={onClose} aria-label='close'>
|
|
91
|
+
<Close />
|
|
92
|
+
</IconButton>
|
|
93
|
+
</Box>
|
|
94
|
+
</DialogTitle>
|
|
95
|
+
<DialogContent sx={styles.fullViewDialogContent}>
|
|
96
|
+
<Box component='pre' sx={styles.fullViewPre}>
|
|
97
|
+
{tokenizedLines.map((tokens, i) => (
|
|
98
|
+
<Box component='span' key={i} sx={styles.fullViewLine}>
|
|
99
|
+
{tokens.map((token, j) =>
|
|
100
|
+
typeof token === 'string' ? (
|
|
101
|
+
token
|
|
102
|
+
) : (
|
|
103
|
+
<Box
|
|
104
|
+
component='span'
|
|
105
|
+
key={j}
|
|
106
|
+
sx={
|
|
107
|
+
styles[`syntaxToken_${token.type}` as keyof typeof styles]
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
{token.value}
|
|
111
|
+
</Box>
|
|
112
|
+
),
|
|
113
|
+
)}
|
|
114
|
+
{'\n'}
|
|
115
|
+
</Box>
|
|
116
|
+
))}
|
|
117
|
+
</Box>
|
|
118
|
+
</DialogContent>
|
|
119
|
+
</Dialog>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { ChatToolGroup } from './chat-tool-group'
|
|
4
|
+
import type { ChatToolItem } from '../types'
|
|
5
|
+
|
|
6
|
+
describe('ChatToolGroup', () => {
|
|
7
|
+
const tools: ChatToolItem[] = [
|
|
8
|
+
{
|
|
9
|
+
id: '1',
|
|
10
|
+
name: 'Tool #1',
|
|
11
|
+
status: 'complete',
|
|
12
|
+
reference: 'tool_1',
|
|
13
|
+
duration: 1.2,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: '2',
|
|
17
|
+
name: 'Tool #2',
|
|
18
|
+
status: 'complete',
|
|
19
|
+
reference: 'tool_2',
|
|
20
|
+
duration: 0.8,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: '3',
|
|
24
|
+
name: 'Tool #3',
|
|
25
|
+
status: 'error',
|
|
26
|
+
reference: 'tool_3',
|
|
27
|
+
duration: 2.1,
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
test('renders "N tools used" label when collapsed', () => {
|
|
32
|
+
render(<ChatToolGroup tools={tools} />)
|
|
33
|
+
expect(screen.getByText('3 tools used')).toBeTruthy()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('renders individual tool names when expanded', () => {
|
|
37
|
+
render(<ChatToolGroup tools={tools} expanded />)
|
|
38
|
+
expect(screen.getByText(/Tool #1/)).toBeTruthy()
|
|
39
|
+
expect(screen.getByText(/Tool #2/)).toBeTruthy()
|
|
40
|
+
expect(screen.getByText(/Tool #3/)).toBeTruthy()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('shows "Error" label on failed tools in the expanded list', () => {
|
|
44
|
+
render(<ChatToolGroup tools={tools} expanded />)
|
|
45
|
+
expect(screen.getByText('Error')).toBeTruthy()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('shows error count badge at group level when any tool has error', () => {
|
|
49
|
+
render(<ChatToolGroup tools={tools} />)
|
|
50
|
+
// New UI shows error count ("1") + error icon instead of "Error" text
|
|
51
|
+
expect(screen.getByTestId('ErrorOutlineIcon')).toBeTruthy()
|
|
52
|
+
expect(screen.getByText('1')).toBeTruthy()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('calls onExpandedChange when group header is clicked', () => {
|
|
56
|
+
const onChange = vi.fn()
|
|
57
|
+
render(<ChatToolGroup tools={tools} onExpandedChange={onChange} />)
|
|
58
|
+
const summary = screen.getByText('3 tools used').closest('button')
|
|
59
|
+
if (summary) fireEvent.click(summary)
|
|
60
|
+
expect(onChange).toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('preserves individual tool expansion states via expandedTools prop', () => {
|
|
64
|
+
render(
|
|
65
|
+
<ChatToolGroup
|
|
66
|
+
tools={tools}
|
|
67
|
+
expanded
|
|
68
|
+
expandedTools={{ '1': true, '2': false, '3': false }}
|
|
69
|
+
/>,
|
|
70
|
+
)
|
|
71
|
+
// Tool #1 should be expanded (shows its reference chip)
|
|
72
|
+
expect(screen.getByText(/tool_1/)).toBeTruthy()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('uses custom toolsUsed label', () => {
|
|
76
|
+
render(
|
|
77
|
+
<ChatToolGroup
|
|
78
|
+
tools={tools}
|
|
79
|
+
labels={{ toolsUsed: 'herramientas usadas' }}
|
|
80
|
+
/>,
|
|
81
|
+
)
|
|
82
|
+
expect(screen.getByText('3 herramientas usadas')).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Box, Button, Collapse, Typography } from '@mui/material'
|
|
2
|
+
import { ArrowRight, ChevronRight, ErrorOutline } from '@mui/icons-material'
|
|
3
|
+
import type { ChatToolGroupProps } from '../types'
|
|
4
|
+
import { ChatToolTrace, ChatToolTraceDetails } from './chat-tool-trace'
|
|
5
|
+
import { styles } from './styles'
|
|
6
|
+
import { getToolLabel } from './get-tool-label'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
|
|
9
|
+
function useController<T>(value: T, setter?: (value: T) => void) {
|
|
10
|
+
const [_value, _setter] = useState<T>(value)
|
|
11
|
+
if (setter) {
|
|
12
|
+
return [value, setter] as const
|
|
13
|
+
} else {
|
|
14
|
+
return [_value, _setter] as const
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatToolGroup({
|
|
19
|
+
tools,
|
|
20
|
+
expanded: _expanded = false,
|
|
21
|
+
onExpandedChange: _setExpanded,
|
|
22
|
+
expandedTools: _expandedTools = {},
|
|
23
|
+
onToolExpandedChange: _setExpandedTools,
|
|
24
|
+
labels = {},
|
|
25
|
+
sx,
|
|
26
|
+
}: ChatToolGroupProps) {
|
|
27
|
+
const [expanded, setExpanded] = useController(_expanded, _setExpanded)
|
|
28
|
+
const [expandedTools, setExpandedTools] = useController(
|
|
29
|
+
_expandedTools,
|
|
30
|
+
_setExpandedTools,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const numErrors = tools.filter((t) => t.status === 'error').length
|
|
34
|
+
const runningTools = tools.filter((t) => t.status === 'running')
|
|
35
|
+
const nonRunningTools = tools.filter((t) => t.status !== 'running')
|
|
36
|
+
const toolsUsedLabel = labels.toolsUsed ?? 'tools used'
|
|
37
|
+
const errorLabel = labels.error ?? 'Error'
|
|
38
|
+
|
|
39
|
+
if (tools.length === 1) {
|
|
40
|
+
return (
|
|
41
|
+
<ChatToolTrace
|
|
42
|
+
tool={tools[0]!}
|
|
43
|
+
expanded={expanded}
|
|
44
|
+
onExpandedChange={setExpanded}
|
|
45
|
+
labels={labels}
|
|
46
|
+
sx={sx}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box width='100%' sx={sx} className='PsChat--tool-group'>
|
|
53
|
+
<Button
|
|
54
|
+
size='small'
|
|
55
|
+
variant='text'
|
|
56
|
+
onClick={() => setExpanded(!expanded)}
|
|
57
|
+
aria-expanded={expanded}
|
|
58
|
+
sx={{
|
|
59
|
+
...styles.traceHeader,
|
|
60
|
+
pr: numErrors > 0 ? 0.5 : 0,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Typography variant='caption' fontWeight={600} color='text.secondary'>
|
|
64
|
+
{tools.length} {toolsUsedLabel}
|
|
65
|
+
</Typography>
|
|
66
|
+
<ArrowRight
|
|
67
|
+
sx={{
|
|
68
|
+
...styles.traceChevron,
|
|
69
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
{numErrors > 0 && (
|
|
73
|
+
<Typography variant='code3' sx={styles.errorBadge}>
|
|
74
|
+
{numErrors}
|
|
75
|
+
<ErrorOutline sx={{ fontSize: 12 }} color='error' />
|
|
76
|
+
</Typography>
|
|
77
|
+
)}
|
|
78
|
+
</Button>
|
|
79
|
+
<Collapse in={expanded} unmountOnExit>
|
|
80
|
+
<Box
|
|
81
|
+
component='ul'
|
|
82
|
+
sx={{
|
|
83
|
+
...styles.traceDetailsWrapper,
|
|
84
|
+
p: 0,
|
|
85
|
+
listStyle: 'none',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{nonRunningTools.map((tool) => {
|
|
89
|
+
const isExpanded = expandedTools[tool.id] ?? false
|
|
90
|
+
return (
|
|
91
|
+
<Box
|
|
92
|
+
key={tool.id}
|
|
93
|
+
component='li'
|
|
94
|
+
sx={styles.groupListItem}
|
|
95
|
+
aria-expanded={isExpanded}
|
|
96
|
+
>
|
|
97
|
+
<Button
|
|
98
|
+
variant='text'
|
|
99
|
+
color='inherit'
|
|
100
|
+
fullWidth
|
|
101
|
+
onClick={() =>
|
|
102
|
+
setExpandedTools({
|
|
103
|
+
...expandedTools,
|
|
104
|
+
[tool.id]: !isExpanded,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
aria-expanded={isExpanded}
|
|
108
|
+
sx={styles.groupHeader}
|
|
109
|
+
>
|
|
110
|
+
<Typography
|
|
111
|
+
flexGrow={1}
|
|
112
|
+
flexShrink={0}
|
|
113
|
+
variant='caption'
|
|
114
|
+
color='text.secondary'
|
|
115
|
+
>
|
|
116
|
+
{getToolLabel(tool)}
|
|
117
|
+
</Typography>
|
|
118
|
+
{tool.status === 'error' && (
|
|
119
|
+
<Typography variant='code3' color='error.main'>
|
|
120
|
+
{errorLabel}
|
|
121
|
+
</Typography>
|
|
122
|
+
)}
|
|
123
|
+
<ChevronRight
|
|
124
|
+
sx={{
|
|
125
|
+
...styles.traceChevron,
|
|
126
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</Button>
|
|
130
|
+
<Collapse in={isExpanded} unmountOnExit>
|
|
131
|
+
<Box px={1.5} py={1}>
|
|
132
|
+
<ChatToolTraceDetails tool={tool} labels={labels} />
|
|
133
|
+
</Box>
|
|
134
|
+
</Collapse>
|
|
135
|
+
</Box>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</Box>
|
|
139
|
+
</Collapse>
|
|
140
|
+
{runningTools.map((tool) => (
|
|
141
|
+
<ChatToolTrace
|
|
142
|
+
key={tool.id}
|
|
143
|
+
tool={tool}
|
|
144
|
+
labels={labels}
|
|
145
|
+
expanded={expandedTools[tool.id]}
|
|
146
|
+
onExpandedChange={() =>
|
|
147
|
+
setExpandedTools({
|
|
148
|
+
...expandedTools,
|
|
149
|
+
[tool.id]: !expandedTools[tool.id],
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
</Box>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { ChatToolTrace } from './chat-tool-trace'
|
|
4
|
+
import type { ChatToolItem } from '../types'
|
|
5
|
+
|
|
6
|
+
describe('ChatToolTrace', () => {
|
|
7
|
+
const completeTool: ChatToolItem = {
|
|
8
|
+
id: '1',
|
|
9
|
+
name: 'add_marker',
|
|
10
|
+
status: 'complete',
|
|
11
|
+
reference: 'add_marker',
|
|
12
|
+
duration: 1.8,
|
|
13
|
+
inputArguments:
|
|
14
|
+
'{"latitude": 39.610861,\n"label": "Calle Toledo 1, Ciudad Real",\n"longitude": -4.2393630}',
|
|
15
|
+
output: '{ "success": true, "data": {} }',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const runningTool: ChatToolItem = {
|
|
19
|
+
id: '2',
|
|
20
|
+
name: 'execute_sql',
|
|
21
|
+
status: 'running',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const errorTool: ChatToolItem = {
|
|
25
|
+
id: '3',
|
|
26
|
+
name: 'execute_sql',
|
|
27
|
+
status: 'error',
|
|
28
|
+
reference: 'execute_sql',
|
|
29
|
+
duration: 1.8,
|
|
30
|
+
inputArguments: '{"datasetId": "abc123"}',
|
|
31
|
+
output: 'HTTP 400: Bad Request\n{"status":400,"error":"Syntax error"}',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('renders "Tool executed" label for completed tools', () => {
|
|
35
|
+
render(<ChatToolTrace tool={completeTool} />)
|
|
36
|
+
expect(screen.getByText('Tool executed')).toBeTruthy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('renders reference name when provided', () => {
|
|
40
|
+
render(<ChatToolTrace tool={completeTool} expanded />)
|
|
41
|
+
expect(screen.getByText('add_marker')).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('renders duration when provided', () => {
|
|
45
|
+
render(<ChatToolTrace tool={completeTool} expanded />)
|
|
46
|
+
expect(screen.getByText('1.8s')).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('renders Success status for completed tools', () => {
|
|
50
|
+
render(<ChatToolTrace tool={completeTool} expanded />)
|
|
51
|
+
expect(screen.getByText('Success')).toBeTruthy()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('renders Error status for failed tools', () => {
|
|
55
|
+
render(<ChatToolTrace tool={errorTool} expanded />)
|
|
56
|
+
expect(screen.getByText('Error')).toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('is NOT expandable when status is running', () => {
|
|
60
|
+
const { container } = render(<ChatToolTrace tool={runningTool} />)
|
|
61
|
+
expect(container.querySelector('button')).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('renders input arguments code area', () => {
|
|
65
|
+
render(<ChatToolTrace tool={completeTool} expanded />)
|
|
66
|
+
expect(screen.getByText(/latitude/)).toBeTruthy()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('renders output code area', () => {
|
|
70
|
+
render(<ChatToolTrace tool={completeTool} expanded />)
|
|
71
|
+
expect(screen.getByText(/success/)).toBeTruthy()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('calls onExpandedChange callback', () => {
|
|
75
|
+
const onChange = vi.fn()
|
|
76
|
+
render(<ChatToolTrace tool={completeTool} onExpandedChange={onChange} />)
|
|
77
|
+
const summary = screen.getByText('Tool executed').closest('button')
|
|
78
|
+
if (summary) fireEvent.click(summary)
|
|
79
|
+
expect(onChange).toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
})
|