@carto/ps-react-ui 4.9.1 → 4.11.1

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.
Files changed (207) hide show
  1. package/dist/category-Dnd2_j0x.js +719 -0
  2. package/dist/category-Dnd2_j0x.js.map +1 -0
  3. package/dist/change-column-DjjwoPt1.js +1143 -0
  4. package/dist/change-column-DjjwoPt1.js.map +1 -0
  5. package/dist/chat.js +1507 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/components.js +122 -120
  8. package/dist/components.js.map +1 -1
  9. package/dist/copy-button-DGL1tyli.js +26 -0
  10. package/dist/copy-button-DGL1tyli.js.map +1 -0
  11. package/dist/{data-zoom-layout-0QSptXG_.js → data-zoom-layout-CkVnm6ej.js} +3 -3
  12. package/dist/{data-zoom-layout-0QSptXG_.js.map → data-zoom-layout-CkVnm6ej.js.map} +1 -1
  13. package/dist/{download-config-CzmjOT2T.js → download-config-oJIFZ2WC.js} +9 -8
  14. package/dist/{download-config-CzmjOT2T.js.map → download-config-oJIFZ2WC.js.map} +1 -1
  15. package/dist/{png-item-CS4z1iSH.js → png-item-BE9uEqlD.js} +2 -2
  16. package/dist/png-item-BE9uEqlD.js.map +1 -0
  17. package/dist/{spread-Y9R1f5dm.js → spread-DYNpzgh_.js} +10 -11
  18. package/dist/{spread-Y9R1f5dm.js.map → spread-DYNpzgh_.js.map} +1 -1
  19. package/dist/{table-CQCAnDLb.js → table-C9IMbTr0.js} +50 -53
  20. package/dist/table-C9IMbTr0.js.map +1 -0
  21. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  22. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  23. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  24. package/dist/types/chat/bubbles/index.d.ts +4 -0
  25. package/dist/types/chat/const.d.ts +4 -0
  26. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  27. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  28. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  29. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  30. package/dist/types/chat/containers/index.d.ts +4 -0
  31. package/dist/types/chat/containers/styles.d.ts +93 -0
  32. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  33. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  34. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  35. package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
  36. package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
  37. package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
  38. package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
  39. package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
  40. package/dist/types/chat/feedback/index.d.ts +8 -0
  41. package/dist/types/chat/feedback/styles.d.ts +211 -0
  42. package/dist/types/chat/index.d.ts +20 -0
  43. package/dist/types/chat/types.d.ts +184 -0
  44. package/dist/types/chat/use-typewriter.d.ts +30 -0
  45. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  46. package/dist/types/components/copy-button/types.d.ts +6 -0
  47. package/dist/types/components/index.d.ts +2 -0
  48. package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
  49. package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
  50. package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
  51. package/dist/types/widgets/echart/types.d.ts +1 -1
  52. package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
  53. package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
  54. package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
  55. package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
  56. package/dist/types/widgets-v2/actions/index.d.ts +1 -0
  57. package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
  58. package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
  59. package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
  60. package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
  61. package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
  62. package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
  63. package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
  64. package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
  65. package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
  66. package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
  67. package/dist/types/widgets-v2/category/category.d.ts +9 -2
  68. package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
  69. package/dist/types/widgets-v2/category/style.d.ts +21 -2
  70. package/dist/types/widgets-v2/category/types.d.ts +2 -0
  71. package/dist/types/widgets-v2/index.d.ts +3 -2
  72. package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
  73. package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
  74. package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
  75. package/dist/widgets/actions.js +115 -114
  76. package/dist/widgets/actions.js.map +1 -1
  77. package/dist/widgets/bar.js +1 -1
  78. package/dist/widgets/category.js +9 -8
  79. package/dist/widgets/category.js.map +1 -1
  80. package/dist/widgets/formula.js +11 -10
  81. package/dist/widgets/formula.js.map +1 -1
  82. package/dist/widgets/histogram.js +7 -6
  83. package/dist/widgets/histogram.js.map +1 -1
  84. package/dist/widgets/markdown.js +9 -8
  85. package/dist/widgets/markdown.js.map +1 -1
  86. package/dist/widgets/pie.js +1 -1
  87. package/dist/widgets/scatterplot.js +1 -1
  88. package/dist/widgets/spread.js +9 -8
  89. package/dist/widgets/spread.js.map +1 -1
  90. package/dist/widgets/table.js +17 -16
  91. package/dist/widgets/table.js.map +1 -1
  92. package/dist/widgets/timeseries.js +1 -1
  93. package/dist/widgets/utils.js +1 -1
  94. package/dist/widgets/wrapper.js +3 -2
  95. package/dist/widgets/wrapper.js.map +1 -1
  96. package/dist/widgets-v2/actions.js +41 -37
  97. package/dist/widgets-v2/bar.js +9 -10
  98. package/dist/widgets-v2/bar.js.map +1 -1
  99. package/dist/widgets-v2/category.js +25 -26
  100. package/dist/widgets-v2/category.js.map +1 -1
  101. package/dist/widgets-v2/formula.js +3 -3
  102. package/dist/widgets-v2/histogram.js +11 -13
  103. package/dist/widgets-v2/histogram.js.map +1 -1
  104. package/dist/widgets-v2/markdown.js +26 -27
  105. package/dist/widgets-v2/markdown.js.map +1 -1
  106. package/dist/widgets-v2/pie.js +8 -10
  107. package/dist/widgets-v2/pie.js.map +1 -1
  108. package/dist/widgets-v2/scatterplot.js +10 -12
  109. package/dist/widgets-v2/scatterplot.js.map +1 -1
  110. package/dist/widgets-v2/spread.js +15 -16
  111. package/dist/widgets-v2/spread.js.map +1 -1
  112. package/dist/widgets-v2/table.js +39 -40
  113. package/dist/widgets-v2/table.js.map +1 -1
  114. package/dist/widgets-v2/timeseries.js +9 -11
  115. package/dist/widgets-v2/timeseries.js.map +1 -1
  116. package/dist/widgets-v2/utils.js +1 -1
  117. package/dist/widgets-v2.js +284 -282
  118. package/dist/widgets-v2.js.map +1 -1
  119. package/package.json +5 -1
  120. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  121. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  122. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  123. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  124. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  125. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  126. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  127. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  128. package/src/chat/bubbles/index.ts +4 -0
  129. package/src/chat/bubbles/styles.ts +148 -0
  130. package/src/chat/const.ts +4 -0
  131. package/src/chat/containers/chat-content.test.tsx +269 -0
  132. package/src/chat/containers/chat-content.tsx +142 -0
  133. package/src/chat/containers/chat-footer.test.tsx +34 -0
  134. package/src/chat/containers/chat-footer.tsx +78 -0
  135. package/src/chat/containers/chat-header.test.tsx +28 -0
  136. package/src/chat/containers/chat-header.tsx +29 -0
  137. package/src/chat/containers/chat-starter.test.tsx +32 -0
  138. package/src/chat/containers/chat-starter.tsx +75 -0
  139. package/src/chat/containers/index.ts +4 -0
  140. package/src/chat/containers/styles.ts +96 -0
  141. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  142. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  143. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  144. package/src/chat/feedback/chat-loader.tsx +31 -0
  145. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  146. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  147. package/src/chat/feedback/chat-thinking.tsx +23 -0
  148. package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
  149. package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
  150. package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
  151. package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
  152. package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
  153. package/src/chat/feedback/chat-tool-group.tsx +156 -0
  154. package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
  155. package/src/chat/feedback/chat-tool-trace.tsx +192 -0
  156. package/src/chat/feedback/get-tool-label.test.tsx +91 -0
  157. package/src/chat/feedback/get-tool-label.ts +13 -0
  158. package/src/chat/feedback/index.ts +8 -0
  159. package/src/chat/feedback/styles.ts +229 -0
  160. package/src/chat/index.ts +59 -0
  161. package/src/chat/types.ts +215 -0
  162. package/src/chat/use-typewriter.test.tsx +38 -0
  163. package/src/chat/use-typewriter.ts +82 -0
  164. package/src/components/copy-button/copy-button.test.tsx +41 -0
  165. package/src/components/copy-button/copy-button.tsx +31 -0
  166. package/src/components/copy-button/types.ts +10 -0
  167. package/src/components/index.ts +3 -0
  168. package/src/widgets/echart/types.ts +1 -1
  169. package/src/widgets-v2/actions/brush-toggle/brush-toggle.tsx +1 -1
  170. package/src/widgets-v2/actions/change-column/sortable-column-item.tsx +1 -1
  171. package/src/widgets-v2/actions/download/download.tsx +1 -1
  172. package/src/widgets-v2/actions/download/icons.tsx +1 -1
  173. package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +3 -3
  174. package/src/widgets-v2/actions/index.ts +8 -0
  175. package/src/widgets-v2/actions/lock-selection/lock-selection.tsx +2 -2
  176. package/src/widgets-v2/actions/relative-data/relative-data.tsx +1 -1
  177. package/src/widgets-v2/actions/searcher/searcher-toggle.tsx +1 -1
  178. package/src/widgets-v2/actions/searcher/searcher.tsx +2 -2
  179. package/src/widgets-v2/actions/show-all/index.ts +7 -0
  180. package/src/widgets-v2/actions/show-all/labels.ts +8 -0
  181. package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
  182. package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
  183. package/src/widgets-v2/actions/show-all/style.ts +8 -0
  184. package/src/widgets-v2/actions/stack-toggle/stack-toggle.tsx +1 -1
  185. package/src/widgets-v2/actions/zoom-toggle/zoom-toggle.tsx +1 -1
  186. package/src/widgets-v2/category/category-ui.test.tsx +26 -10
  187. package/src/widgets-v2/category/category-ui.tsx +13 -3
  188. package/src/widgets-v2/category/category.test.tsx +4 -4
  189. package/src/widgets-v2/category/category.tsx +10 -1
  190. package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
  191. package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
  192. package/src/widgets-v2/category/style.ts +35 -4
  193. package/src/widgets-v2/category/types.ts +2 -0
  194. package/src/widgets-v2/index.ts +3 -0
  195. package/src/widgets-v2/selection-summary/labels.ts +8 -4
  196. package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
  197. package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
  198. package/src/widgets-v2/selection-summary/style.ts +15 -0
  199. package/src/widgets-v2/table/table-ui.tsx +4 -4
  200. package/src/widgets-v2/toolbox/toolbox.tsx +1 -1
  201. package/src/widgets-v2/wrapper/widget-wrapper.tsx +1 -1
  202. package/dist/category-DwaeYjpX.js +0 -656
  203. package/dist/category-DwaeYjpX.js.map +0 -1
  204. package/dist/change-column-B4IT0rh6.js +0 -1110
  205. package/dist/change-column-B4IT0rh6.js.map +0 -1
  206. package/dist/png-item-CS4z1iSH.js.map +0 -1
  207. package/dist/table-CQCAnDLb.js.map +0 -1
@@ -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: isExpanded ? '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
+ })
@@ -0,0 +1,192 @@
1
+ import {
2
+ Box,
3
+ CircularProgress,
4
+ Typography,
5
+ Collapse,
6
+ Button,
7
+ } from '@mui/material'
8
+ import { ArrowRight, CheckCircle, Error } from '@mui/icons-material'
9
+ import type { ChatToolTraceProps } from '../types'
10
+ import { ChatToolCodeArea } from './chat-tool-code-area'
11
+ import { styles } from './styles'
12
+ import { ChatThinking } from './chat-thinking'
13
+ import { getToolLabel } from './get-tool-label'
14
+ import { McpTool } from '@carto/meridian-ds/custom-icons'
15
+
16
+ function TraceStatusIndicator({
17
+ status,
18
+ labels,
19
+ }: {
20
+ status: string
21
+ labels: ChatToolTraceProps['labels']
22
+ }) {
23
+ switch (status) {
24
+ case 'complete':
25
+ return (
26
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
27
+ <CheckCircle sx={{ fontSize: 12 }} color='success' />
28
+ <Typography variant='caption'>
29
+ {labels?.success ?? 'Success'}
30
+ </Typography>
31
+ </Box>
32
+ )
33
+ case 'error':
34
+ return (
35
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
36
+ <Error sx={{ fontSize: 12 }} color='error' />
37
+ <Typography variant='code3'>{labels?.error ?? 'Error'}</Typography>
38
+ </Box>
39
+ )
40
+ case 'running':
41
+ return <CircularProgress size={14} />
42
+ default:
43
+ return null
44
+ }
45
+ }
46
+
47
+ export function ChatToolTraceDetails({
48
+ tool,
49
+ labels = {},
50
+ }: Pick<ChatToolTraceProps, 'tool' | 'labels'>) {
51
+ const isError = tool.status === 'error'
52
+ const toolExecutedLabel = labels.toolExecuted ?? 'Tool executed'
53
+ const referenceLabel = labels.reference ?? 'Reference:'
54
+ const durationLabel = labels.duration ?? 'Duration:'
55
+ const statusLabel = labels.status ?? 'Status:'
56
+ const inputArgumentsLabel = labels.inputArguments ?? 'Input arguments:'
57
+ const outputLabel = labels.output ?? 'Output:'
58
+
59
+ return (
60
+ <Box>
61
+ {tool.reference && (
62
+ <Box sx={styles.traceField}>
63
+ <Typography
64
+ variant='code3'
65
+ fontWeight={600}
66
+ color='text.secondary'
67
+ sx={styles.traceFieldLabel}
68
+ >
69
+ {referenceLabel}
70
+ </Typography>
71
+ <Box sx={styles.traceReference}>
72
+ <McpTool color='success' />
73
+ <Typography variant='code3' fontWeight={600}>
74
+ {tool.reference}
75
+ </Typography>
76
+ </Box>
77
+ </Box>
78
+ )}
79
+
80
+ {tool.duration != null && (
81
+ <Box sx={styles.traceField}>
82
+ <Typography
83
+ variant='code3'
84
+ fontWeight={600}
85
+ color='text.secondary'
86
+ sx={styles.traceFieldLabel}
87
+ >
88
+ {durationLabel}
89
+ </Typography>
90
+ <Typography variant='code3'>{tool.duration}s</Typography>
91
+ </Box>
92
+ )}
93
+
94
+ <Box sx={styles.traceField}>
95
+ <Typography
96
+ variant='code3'
97
+ fontWeight={600}
98
+ color='text.secondary'
99
+ sx={styles.traceFieldLabel}
100
+ >
101
+ {statusLabel}
102
+ </Typography>
103
+ <TraceStatusIndicator status={tool.status} labels={labels} />
104
+ </Box>
105
+
106
+ {tool.inputArguments && (
107
+ <Box>
108
+ <Typography
109
+ variant='code3'
110
+ fontWeight={600}
111
+ color='text.secondary'
112
+ sx={styles.traceField}
113
+ >
114
+ {inputArgumentsLabel}
115
+ </Typography>
116
+ <ChatToolCodeArea
117
+ sx={{ mt: 0.5, mb: tool.output ? 1.5 : 0 }}
118
+ content={tool.inputArguments}
119
+ title={`${toolExecutedLabel}: ${inputArgumentsLabel}`}
120
+ isError={isError}
121
+ />
122
+ </Box>
123
+ )}
124
+
125
+ {tool.output && (
126
+ <Box>
127
+ <Typography
128
+ variant='code3'
129
+ fontWeight={600}
130
+ color='text.secondary'
131
+ sx={styles.traceField}
132
+ >
133
+ {outputLabel}
134
+ </Typography>
135
+ <ChatToolCodeArea
136
+ sx={{ mt: 0.5 }}
137
+ content={tool.output}
138
+ title={`${toolExecutedLabel}: ${outputLabel}`}
139
+ isError={isError}
140
+ />
141
+ </Box>
142
+ )}
143
+ </Box>
144
+ )
145
+ }
146
+
147
+ export function ChatToolTrace({
148
+ tool,
149
+ expanded,
150
+ onExpandedChange,
151
+ labels = {},
152
+ sx,
153
+ }: ChatToolTraceProps) {
154
+ const isRunning = tool.status === 'running'
155
+ const toolExecutedLabel = labels.toolExecuted ?? 'Tool executed'
156
+
157
+ if (isRunning) {
158
+ return <ChatThinking>{getToolLabel(tool)}</ChatThinking>
159
+ }
160
+
161
+ return (
162
+ <Box
163
+ width='100%'
164
+ sx={sx}
165
+ data-tool={tool.name}
166
+ className={`PsChat--tool-trace`}
167
+ >
168
+ <Button
169
+ size='small'
170
+ variant='text'
171
+ onClick={() => onExpandedChange?.(!expanded)}
172
+ aria-expanded={expanded}
173
+ sx={styles.traceHeader}
174
+ >
175
+ <Typography variant='caption' fontWeight={600} color='text.secondary'>
176
+ {toolExecutedLabel}
177
+ </Typography>
178
+ <ArrowRight
179
+ sx={{
180
+ ...styles.traceChevron,
181
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
182
+ }}
183
+ />
184
+ </Button>
185
+ <Collapse in={expanded} unmountOnExit>
186
+ <Box sx={styles.traceDetailsWrapper}>
187
+ <ChatToolTraceDetails tool={tool} labels={labels} />
188
+ </Box>
189
+ </Collapse>
190
+ </Box>
191
+ )
192
+ }