@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,34 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChatFooter } from './chat-footer'
4
+
5
+ describe('ChatFooter', () => {
6
+ const defaultProps = {
7
+ value: '',
8
+ onChange: vi.fn(),
9
+ onSend: vi.fn(),
10
+ }
11
+
12
+ test('renders send button', () => {
13
+ render(<ChatFooter {...defaultProps} />)
14
+ expect(screen.getByLabelText('Send')).toBeTruthy()
15
+ })
16
+
17
+ test('send button is disabled when value is empty', () => {
18
+ render(<ChatFooter {...defaultProps} />)
19
+ expect(screen.getByLabelText('Send').hasAttribute('disabled')).toBeTruthy()
20
+ })
21
+
22
+ test('renders stop button when generating', () => {
23
+ render(<ChatFooter {...defaultProps} isGenerating onStop={vi.fn()} />)
24
+ expect(screen.getByLabelText('Stop')).toBeTruthy()
25
+ })
26
+
27
+ test('calls onChange when typing', () => {
28
+ const onChange = vi.fn()
29
+ render(<ChatFooter {...defaultProps} onChange={onChange} />)
30
+ const input = screen.getByPlaceholderText('Type a message...')
31
+ fireEvent.change(input, { target: { value: 'hello' } })
32
+ expect(onChange).toHaveBeenCalledWith('hello')
33
+ })
34
+ })
@@ -0,0 +1,78 @@
1
+ import {
2
+ Box,
3
+ FilledInput,
4
+ FormControl,
5
+ FormHelperText,
6
+ IconButton,
7
+ } from '@mui/material'
8
+ import { ArrowUpwardOutlined, StopCircleOutlined } from '@mui/icons-material'
9
+ import type { ChatFooterProps } from '../types'
10
+ import { styles } from './styles'
11
+
12
+ const DEFAULT_CAPTION =
13
+ 'Responses are AI generated. Please verify key information.'
14
+
15
+ export function ChatFooter({
16
+ value,
17
+ onChange,
18
+ onSend,
19
+ onStop,
20
+ isGenerating = false,
21
+ disabled = false,
22
+ placeholder = 'Type a message...',
23
+ labels = {},
24
+ caption = DEFAULT_CAPTION,
25
+ sx,
26
+ }: ChatFooterProps) {
27
+ const canSend = value.trim() && !disabled && !isGenerating
28
+ const handleKeyDown = (e: React.KeyboardEvent) => {
29
+ if (e.key === 'Enter' && !e.shiftKey) {
30
+ e.preventDefault()
31
+ if (canSend) {
32
+ onSend()
33
+ }
34
+ }
35
+ }
36
+
37
+ return (
38
+ <FormControl fullWidth sx={{ ...styles.footerWrapper, ...sx }}>
39
+ <FilledInput
40
+ multiline
41
+ value={value}
42
+ onChange={(e) => onChange(e.target.value)}
43
+ onKeyDown={handleKeyDown}
44
+ placeholder={placeholder}
45
+ disabled={disabled || isGenerating}
46
+ sx={styles.footer}
47
+ style={{ minHeight: 0 }}
48
+ fullWidth
49
+ size='small'
50
+ />
51
+ <Box sx={styles.footerCorner}>
52
+ {isGenerating && onStop ? (
53
+ <IconButton
54
+ size='small'
55
+ onClick={onStop}
56
+ disabled={disabled}
57
+ aria-label={labels.stop ?? 'Stop'}
58
+ >
59
+ <StopCircleOutlined />
60
+ </IconButton>
61
+ ) : (
62
+ <IconButton
63
+ size='small'
64
+ variant='contained'
65
+ onClick={onSend}
66
+ disabled={!canSend}
67
+ aria-label={labels.send ?? 'Send'}
68
+ >
69
+ <ArrowUpwardOutlined />
70
+ </IconButton>
71
+ )}
72
+ </Box>
73
+ {caption ? (
74
+ <FormHelperText sx={styles.footerCaption}>{caption}</FormHelperText>
75
+ ) : null}
76
+ </FormControl>
77
+ )
78
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChatHeader } from './chat-header'
4
+
5
+ describe('ChatHeader', () => {
6
+ test('renders title', () => {
7
+ render(<ChatHeader title='Chat Assistant' />)
8
+ expect(screen.getByText('Chat Assistant')).toBeTruthy()
9
+ })
10
+
11
+ test('renders close button when onClose provided', () => {
12
+ const onClose = vi.fn()
13
+ render(<ChatHeader title='Test' onClose={onClose} />)
14
+ const closeButton = screen.getByLabelText('Close')
15
+ fireEvent.click(closeButton)
16
+ expect(onClose).toHaveBeenCalledTimes(1)
17
+ })
18
+
19
+ test('renders rightSlot', () => {
20
+ render(<ChatHeader title='Test' rightSlot={<button>Custom</button>} />)
21
+ expect(screen.getByText('Custom')).toBeTruthy()
22
+ })
23
+
24
+ test('renders leftSlot', () => {
25
+ render(<ChatHeader title='Test' leftSlot={<button>Back</button>} />)
26
+ expect(screen.getByText('Back')).toBeTruthy()
27
+ })
28
+ })
@@ -0,0 +1,29 @@
1
+ import { Box, IconButton, Typography } from '@mui/material'
2
+ import { Close } from '@mui/icons-material'
3
+ import type { ChatHeaderProps } from '../types'
4
+ import { styles } from './styles'
5
+
6
+ export function ChatHeader({
7
+ leftSlot,
8
+ title,
9
+ rightSlot,
10
+ onClose,
11
+ sx,
12
+ }: ChatHeaderProps) {
13
+ return (
14
+ <Box sx={{ ...styles.header, ...sx }}>
15
+ {leftSlot}
16
+ <Typography variant='subtitle2' sx={styles.headerTitle}>
17
+ {title}
18
+ </Typography>
19
+ <Box sx={styles.headerActions}>
20
+ {rightSlot}
21
+ {onClose && (
22
+ <IconButton size='medium' onClick={onClose} aria-label='Close'>
23
+ <Close />
24
+ </IconButton>
25
+ )}
26
+ </Box>
27
+ </Box>
28
+ )
29
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChatStarter } from './chat-starter'
4
+
5
+ describe('ChatStarter', () => {
6
+ const defaultItems = ['Suggestion 1', 'Suggestion 2']
7
+
8
+ test('renders title and description', () => {
9
+ render(
10
+ <ChatStarter
11
+ title='Welcome'
12
+ description='How can I help?'
13
+ items={defaultItems}
14
+ />,
15
+ )
16
+ expect(screen.getByText('Welcome')).toBeTruthy()
17
+ expect(screen.getByText('How can I help?')).toBeTruthy()
18
+ })
19
+
20
+ test('renders suggestion items', () => {
21
+ render(<ChatStarter items={defaultItems} />)
22
+ expect(screen.getByText('Suggestion 1')).toBeTruthy()
23
+ expect(screen.getByText('Suggestion 2')).toBeTruthy()
24
+ })
25
+
26
+ test('calls item onClick when clicked', () => {
27
+ const onSelect = vi.fn()
28
+ render(<ChatStarter items={defaultItems} onSelect={onSelect} />)
29
+ fireEvent.click(screen.getByText('Suggestion 1'))
30
+ expect(onSelect).toHaveBeenCalledTimes(1)
31
+ })
32
+ })
@@ -0,0 +1,75 @@
1
+ import { Box, Typography } from '@mui/material'
2
+ import type { ChatStarterProps } from '../types'
3
+ import { ChatSuggestionButton } from '../bubbles/chat-suggestion-button'
4
+ import { styles } from './styles'
5
+
6
+ const DEFAULT_PALETTE = [
7
+ '#C9DB7440', // pastel green
8
+ '#FE88B140', // pastel red
9
+ '#9EB9F340', // pastel blue
10
+ '#F6CF7140', // pastel yellow
11
+ ]
12
+
13
+ export function ChatStarter({
14
+ icon,
15
+ title,
16
+ description,
17
+ items,
18
+ size = 'small',
19
+ onSelect,
20
+ sx,
21
+ }: ChatStarterProps) {
22
+ const fullItems = items.map((item, index) => {
23
+ if (typeof item === 'string') {
24
+ return {
25
+ label: item,
26
+ color: DEFAULT_PALETTE[index],
27
+ }
28
+ } else {
29
+ return {
30
+ label: item.label,
31
+ color: item.color ?? DEFAULT_PALETTE[index],
32
+ }
33
+ }
34
+ })
35
+ return (
36
+ <Box
37
+ sx={{
38
+ ...styles.starter,
39
+ gap: size === 'small' ? 1 : 2,
40
+ ...sx,
41
+ }}
42
+ >
43
+ {icon}
44
+ {title && (
45
+ <Typography variant={size === 'small' ? 'h6' : 'h5'}>
46
+ {title}
47
+ </Typography>
48
+ )}
49
+ {description && (
50
+ <Typography
51
+ variant={size === 'small' ? 'body2' : 'body1'}
52
+ color='text.secondary'
53
+ >
54
+ {description}
55
+ </Typography>
56
+ )}
57
+ <Box
58
+ sx={{
59
+ ...styles.starterItems,
60
+ gap: size === 'small' ? 1 : 2,
61
+ ...(items.length > 2 ? styles.starterItemsTwoCol : undefined),
62
+ }}
63
+ >
64
+ {fullItems.map((item, i) => (
65
+ <ChatSuggestionButton
66
+ key={i}
67
+ label={item.label}
68
+ onClick={() => onSelect?.(item.label)}
69
+ color={item.color}
70
+ />
71
+ ))}
72
+ </Box>
73
+ </Box>
74
+ )
75
+ }
@@ -0,0 +1,4 @@
1
+ export { ChatContent } from './chat-content'
2
+ export { ChatHeader } from './chat-header'
3
+ export { ChatFooter } from './chat-footer'
4
+ export { ChatStarter } from './chat-starter'
@@ -0,0 +1,96 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+ import { CHAT_MAX_WIDTH } from '../const'
3
+
4
+ export const styles = {
5
+ header: {
6
+ display: 'flex',
7
+ alignItems: 'center',
8
+ justifyContent: 'space-between',
9
+ padding: ({ spacing }) => spacing(1),
10
+ },
11
+ headerTitle: {
12
+ flexGrow: 1,
13
+ px: ({ spacing }) => spacing(0.5),
14
+ },
15
+ headerActions: {
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ marginLeft: ({ spacing }) => spacing(2),
19
+ gap: ({ spacing }) => spacing(0.5),
20
+ },
21
+ footerWrapper: {
22
+ padding: ({ spacing }) => spacing(1),
23
+ position: 'relative',
24
+ },
25
+ footerCorner: {
26
+ position: 'absolute',
27
+ bottom: '38px',
28
+ right: `max(16px, calc(50% - ${CHAT_MAX_WIDTH / 2}px + 6px))`,
29
+ margin: '0 auto',
30
+ },
31
+ footer: {
32
+ maxWidth: CHAT_MAX_WIDTH,
33
+ margin: '0 auto',
34
+ '&.MuiFilledInput-root.MuiInputBase-multiline.MuiInputBase-sizeSmall textarea':
35
+ {
36
+ resize: 'none',
37
+ maxHeight: '10em',
38
+ overflowY: 'auto !important',
39
+ paddingRight: ({ spacing }) => spacing(5),
40
+ },
41
+ },
42
+ footerCaption: {
43
+ textAlign: 'center',
44
+ },
45
+ content: {
46
+ overflowY: 'auto',
47
+ flex: 1,
48
+ position: 'relative',
49
+ mx: 'auto',
50
+ maxWidth: CHAT_MAX_WIDTH,
51
+ width: '100%',
52
+ maxHeight: '100%',
53
+ pt: 1,
54
+ pb: 2,
55
+ px: 2,
56
+ display: 'flex',
57
+ flexDirection: 'column',
58
+ borderTopWidth: '1px',
59
+ borderTopStyle: 'solid',
60
+ borderBottomWidth: '1px',
61
+ borderBottomStyle: 'solid',
62
+ },
63
+ sentinel: {
64
+ height: '1px',
65
+ flexShrink: 0,
66
+ },
67
+ jumpToLatestWrapper: {
68
+ position: 'sticky',
69
+ bottom: ({ spacing }) => spacing(3),
70
+ height: 0,
71
+ display: 'flex',
72
+ justifyContent: 'center',
73
+ zIndex: 1,
74
+ },
75
+ jumpToLatest: {
76
+ transition: 'opacity 0.2s',
77
+ },
78
+ starter: {
79
+ display: 'flex',
80
+ flexDirection: 'column',
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ padding: ({ spacing }) => spacing(3),
84
+ textAlign: 'center',
85
+ maxWidth: CHAT_MAX_WIDTH,
86
+ margin: '0 auto',
87
+ },
88
+ starterItems: {
89
+ display: 'grid',
90
+ width: '100%',
91
+ marginTop: ({ spacing }) => spacing(2),
92
+ },
93
+ starterItemsTwoCol: {
94
+ gridTemplateColumns: 'repeat(2, 1fr)',
95
+ },
96
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChatActionsContainer } from './chat-actions-container'
4
+ import { ChatRatingAction } from './chat-rating-action'
5
+
6
+ describe('ChatActionsContainer', () => {
7
+ test('renders children', () => {
8
+ render(
9
+ <ChatActionsContainer>
10
+ <span>child content</span>
11
+ </ChatActionsContainer>,
12
+ )
13
+ expect(screen.getByText('child content')).toBeTruthy()
14
+ })
15
+ })
16
+
17
+ describe('ChatRatingAction', () => {
18
+ test('renders thumb up and thumb down buttons', () => {
19
+ render(<ChatRatingAction />)
20
+ expect(screen.getByLabelText('Thumbs up')).toBeTruthy()
21
+ expect(screen.getByLabelText('Thumbs down')).toBeTruthy()
22
+ })
23
+
24
+ test('calls onRatingChange with up when thumbs up clicked', () => {
25
+ const onRatingChange = vi.fn()
26
+ render(<ChatRatingAction onRatingChange={onRatingChange} />)
27
+ fireEvent.click(screen.getByLabelText('Thumbs up'))
28
+ expect(onRatingChange).toHaveBeenCalledWith('up')
29
+ })
30
+
31
+ test('calls onRatingChange with null when active thumb up clicked', () => {
32
+ const onRatingChange = vi.fn()
33
+ render(<ChatRatingAction onRatingChange={onRatingChange} rating='up' />)
34
+ fireEvent.click(screen.getByLabelText('Thumbs up'))
35
+ expect(onRatingChange).toHaveBeenCalledWith(null)
36
+ })
37
+
38
+ test('calls onRatingChange with down when thumbs down clicked', () => {
39
+ const onRatingChange = vi.fn()
40
+ render(<ChatRatingAction onRatingChange={onRatingChange} />)
41
+ fireEvent.click(screen.getByLabelText('Thumbs down'))
42
+ expect(onRatingChange).toHaveBeenCalledWith('down')
43
+ })
44
+
45
+ test('highlights active rating up', () => {
46
+ const { container } = render(<ChatRatingAction rating='up' />)
47
+ expect(container.querySelector('[data-testid="ThumbUpIcon"]')).toBeTruthy()
48
+ })
49
+
50
+ test('highlights active rating down', () => {
51
+ const { container } = render(<ChatRatingAction rating='down' />)
52
+ expect(
53
+ container.querySelector('[data-testid="ThumbDownIcon"]'),
54
+ ).toBeTruthy()
55
+ })
56
+
57
+ test('renders with custom labels', () => {
58
+ render(
59
+ <ChatRatingAction labels={{ thumbUp: 'Like', thumbDown: 'Dislike' }} />,
60
+ )
61
+ expect(screen.getByLabelText('Like')).toBeTruthy()
62
+ expect(screen.getByLabelText('Dislike')).toBeTruthy()
63
+ })
64
+ })
@@ -0,0 +1,7 @@
1
+ import { Box, styled } from '@mui/material'
2
+
3
+ export const ChatActionsContainer = styled(Box)(({ theme }) => ({
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ gap: theme.spacing(0.5),
7
+ }))
@@ -0,0 +1,10 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render } from '@testing-library/react'
3
+ import { ChatLoader } from './chat-loader'
4
+
5
+ describe('ChatLoader', () => {
6
+ test('renders loading indicator', () => {
7
+ const { container } = render(<ChatLoader />)
8
+ expect(container.querySelector('[role="status"]')).toBeTruthy()
9
+ })
10
+ })
@@ -0,0 +1,31 @@
1
+ import { Box } from '@mui/material'
2
+ import type { ChatLoaderProps } from '../types'
3
+ import { styles } from './styles'
4
+
5
+ export function ChatLoader({ size = 24, labels = {}, sx }: ChatLoaderProps) {
6
+ return (
7
+ <Box
8
+ role='status'
9
+ aria-busy={true}
10
+ aria-label={labels.loading ?? 'Loading'}
11
+ sx={{ ...styles.loader, width: size, height: size, ...sx }}
12
+ >
13
+ <Box
14
+ component='span'
15
+ sx={{
16
+ ...styles.loaderOuterCircle,
17
+ width: size * 0.75,
18
+ height: size * 0.75,
19
+ }}
20
+ />
21
+ <Box
22
+ component='span'
23
+ sx={{
24
+ ...styles.loaderInnerCircle,
25
+ width: size * 0.32,
26
+ height: size * 0.32,
27
+ }}
28
+ />
29
+ </Box>
30
+ )
31
+ }
@@ -0,0 +1,43 @@
1
+ import { IconButton } from '@mui/material'
2
+ import {
3
+ ThumbUpOutlined,
4
+ ThumbDownOutlined,
5
+ ThumbUp,
6
+ ThumbDown,
7
+ } from '@mui/icons-material'
8
+ import type { ChatRatingActionProps } from '../types'
9
+
10
+ export function ChatRatingAction({
11
+ rating,
12
+ onRatingChange,
13
+ labels = {},
14
+ }: ChatRatingActionProps) {
15
+ return (
16
+ <>
17
+ <IconButton
18
+ size='small'
19
+ onClick={() => onRatingChange?.(rating === 'up' ? null : 'up')}
20
+ aria-label={labels.thumbUp ?? 'Thumbs up'}
21
+ color={rating === 'up' ? 'primary' : undefined}
22
+ >
23
+ {rating === 'up' ? (
24
+ <ThumbUp fontSize='small' />
25
+ ) : (
26
+ <ThumbUpOutlined fontSize='small' />
27
+ )}
28
+ </IconButton>
29
+ <IconButton
30
+ size='small'
31
+ onClick={() => onRatingChange?.(rating === 'down' ? null : 'down')}
32
+ aria-label={labels.thumbDown ?? 'Thumbs down'}
33
+ color={rating === 'down' ? 'primary' : undefined}
34
+ >
35
+ {rating === 'down' ? (
36
+ <ThumbDown fontSize='small' />
37
+ ) : (
38
+ <ThumbDownOutlined fontSize='small' />
39
+ )}
40
+ </IconButton>
41
+ </>
42
+ )
43
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ChatThinking } from './chat-thinking'
4
+
5
+ describe('ChatThinking', () => {
6
+ test('renders default thinking text', () => {
7
+ render(<ChatThinking />)
8
+ expect(screen.getByText('Thinking...')).toBeTruthy()
9
+ })
10
+
11
+ test('renders custom text', () => {
12
+ render(<ChatThinking>Processing your request...</ChatThinking>)
13
+ expect(screen.getByText('Processing your request...')).toBeTruthy()
14
+ })
15
+ })
@@ -0,0 +1,23 @@
1
+ import { Typography } from '@mui/material'
2
+ import type { ChatThinkingProps } from '../types'
3
+ import { styles } from './styles'
4
+
5
+ export function ChatThinking({
6
+ children = 'Thinking...',
7
+ duration = 2000,
8
+ sx,
9
+ }: ChatThinkingProps) {
10
+ const durSeconds = `${duration / 1000}s`
11
+ return (
12
+ <Typography
13
+ variant='body2'
14
+ sx={{
15
+ ...styles.thinking,
16
+ animationDuration: durSeconds,
17
+ ...sx,
18
+ }}
19
+ >
20
+ {children}
21
+ </Typography>
22
+ )
23
+ }
@@ -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) {
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
+ }