@carto/ps-react-ui 4.9.1 → 4.11.0

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 (188) 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-BiuuHCDN.js +1156 -0
  4. package/dist/change-column-BiuuHCDN.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--YiY6ko_.js} +4 -3
  12. package/dist/{data-zoom-layout-0QSptXG_.js.map → data-zoom-layout--YiY6ko_.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/{spread-Y9R1f5dm.js → spread-CPis22AE.js} +4 -3
  16. package/dist/{spread-Y9R1f5dm.js.map → spread-CPis22AE.js.map} +1 -1
  17. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  18. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  19. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  20. package/dist/types/chat/bubbles/index.d.ts +4 -0
  21. package/dist/types/chat/const.d.ts +4 -0
  22. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  23. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  24. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  25. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  26. package/dist/types/chat/containers/index.d.ts +4 -0
  27. package/dist/types/chat/containers/styles.d.ts +93 -0
  28. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  29. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  30. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  31. package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
  32. package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
  33. package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
  34. package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
  35. package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
  36. package/dist/types/chat/feedback/index.d.ts +8 -0
  37. package/dist/types/chat/feedback/styles.d.ts +211 -0
  38. package/dist/types/chat/index.d.ts +20 -0
  39. package/dist/types/chat/types.d.ts +184 -0
  40. package/dist/types/chat/use-typewriter.d.ts +30 -0
  41. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  42. package/dist/types/components/copy-button/types.d.ts +6 -0
  43. package/dist/types/components/index.d.ts +2 -0
  44. package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
  45. package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
  46. package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
  47. package/dist/types/widgets/echart/types.d.ts +1 -1
  48. package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
  49. package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
  50. package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
  51. package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
  52. package/dist/types/widgets-v2/actions/index.d.ts +1 -0
  53. package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
  54. package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
  55. package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
  56. package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
  57. package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
  58. package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
  59. package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
  60. package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
  61. package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
  62. package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
  63. package/dist/types/widgets-v2/category/category.d.ts +9 -2
  64. package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
  65. package/dist/types/widgets-v2/category/style.d.ts +21 -2
  66. package/dist/types/widgets-v2/category/types.d.ts +2 -0
  67. package/dist/types/widgets-v2/index.d.ts +3 -2
  68. package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
  69. package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
  70. package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
  71. package/dist/widgets/actions.js +115 -114
  72. package/dist/widgets/actions.js.map +1 -1
  73. package/dist/widgets/bar.js +1 -1
  74. package/dist/widgets/category.js +9 -8
  75. package/dist/widgets/category.js.map +1 -1
  76. package/dist/widgets/formula.js +11 -10
  77. package/dist/widgets/formula.js.map +1 -1
  78. package/dist/widgets/histogram.js +7 -6
  79. package/dist/widgets/histogram.js.map +1 -1
  80. package/dist/widgets/markdown.js +9 -8
  81. package/dist/widgets/markdown.js.map +1 -1
  82. package/dist/widgets/pie.js +1 -1
  83. package/dist/widgets/scatterplot.js +1 -1
  84. package/dist/widgets/spread.js +9 -8
  85. package/dist/widgets/spread.js.map +1 -1
  86. package/dist/widgets/table.js +17 -16
  87. package/dist/widgets/table.js.map +1 -1
  88. package/dist/widgets/timeseries.js +1 -1
  89. package/dist/widgets/utils.js +1 -1
  90. package/dist/widgets/wrapper.js +3 -2
  91. package/dist/widgets/wrapper.js.map +1 -1
  92. package/dist/widgets-v2/actions.js +41 -37
  93. package/dist/widgets-v2/bar.js +8 -7
  94. package/dist/widgets-v2/bar.js.map +1 -1
  95. package/dist/widgets-v2/category.js +22 -21
  96. package/dist/widgets-v2/category.js.map +1 -1
  97. package/dist/widgets-v2/formula.js +23 -22
  98. package/dist/widgets-v2/formula.js.map +1 -1
  99. package/dist/widgets-v2/histogram.js +10 -9
  100. package/dist/widgets-v2/histogram.js.map +1 -1
  101. package/dist/widgets-v2/markdown.js +9 -8
  102. package/dist/widgets-v2/markdown.js.map +1 -1
  103. package/dist/widgets-v2/pie.js +7 -6
  104. package/dist/widgets-v2/pie.js.map +1 -1
  105. package/dist/widgets-v2/scatterplot.js +9 -8
  106. package/dist/widgets-v2/scatterplot.js.map +1 -1
  107. package/dist/widgets-v2/spread.js +9 -8
  108. package/dist/widgets-v2/spread.js.map +1 -1
  109. package/dist/widgets-v2/table.js +16 -15
  110. package/dist/widgets-v2/table.js.map +1 -1
  111. package/dist/widgets-v2/timeseries.js +8 -7
  112. package/dist/widgets-v2/timeseries.js.map +1 -1
  113. package/dist/widgets-v2/utils.js +1 -1
  114. package/dist/widgets-v2.js +276 -271
  115. package/dist/widgets-v2.js.map +1 -1
  116. package/package.json +7 -3
  117. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  118. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  119. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  120. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  121. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  122. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  123. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  124. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  125. package/src/chat/bubbles/index.ts +4 -0
  126. package/src/chat/bubbles/styles.ts +148 -0
  127. package/src/chat/const.ts +4 -0
  128. package/src/chat/containers/chat-content.test.tsx +269 -0
  129. package/src/chat/containers/chat-content.tsx +142 -0
  130. package/src/chat/containers/chat-footer.test.tsx +34 -0
  131. package/src/chat/containers/chat-footer.tsx +78 -0
  132. package/src/chat/containers/chat-header.test.tsx +28 -0
  133. package/src/chat/containers/chat-header.tsx +29 -0
  134. package/src/chat/containers/chat-starter.test.tsx +32 -0
  135. package/src/chat/containers/chat-starter.tsx +75 -0
  136. package/src/chat/containers/index.ts +4 -0
  137. package/src/chat/containers/styles.ts +96 -0
  138. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  139. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  140. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  141. package/src/chat/feedback/chat-loader.tsx +31 -0
  142. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  143. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  144. package/src/chat/feedback/chat-thinking.tsx +23 -0
  145. package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
  146. package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
  147. package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
  148. package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
  149. package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
  150. package/src/chat/feedback/chat-tool-group.tsx +156 -0
  151. package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
  152. package/src/chat/feedback/chat-tool-trace.tsx +192 -0
  153. package/src/chat/feedback/get-tool-label.test.tsx +91 -0
  154. package/src/chat/feedback/get-tool-label.ts +13 -0
  155. package/src/chat/feedback/index.ts +8 -0
  156. package/src/chat/feedback/styles.ts +229 -0
  157. package/src/chat/index.ts +59 -0
  158. package/src/chat/types.ts +215 -0
  159. package/src/chat/use-typewriter.test.tsx +38 -0
  160. package/src/chat/use-typewriter.ts +82 -0
  161. package/src/components/copy-button/copy-button.test.tsx +41 -0
  162. package/src/components/copy-button/copy-button.tsx +31 -0
  163. package/src/components/copy-button/types.ts +10 -0
  164. package/src/components/index.ts +3 -0
  165. package/src/widgets/echart/types.ts +1 -1
  166. package/src/widgets-v2/actions/index.ts +8 -0
  167. package/src/widgets-v2/actions/show-all/index.ts +7 -0
  168. package/src/widgets-v2/actions/show-all/labels.ts +8 -0
  169. package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
  170. package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
  171. package/src/widgets-v2/actions/show-all/style.ts +8 -0
  172. package/src/widgets-v2/category/category-ui.test.tsx +26 -10
  173. package/src/widgets-v2/category/category-ui.tsx +13 -3
  174. package/src/widgets-v2/category/category.test.tsx +4 -4
  175. package/src/widgets-v2/category/category.tsx +10 -1
  176. package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
  177. package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
  178. package/src/widgets-v2/category/style.ts +35 -4
  179. package/src/widgets-v2/category/types.ts +2 -0
  180. package/src/widgets-v2/index.ts +3 -0
  181. package/src/widgets-v2/selection-summary/labels.ts +8 -4
  182. package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
  183. package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
  184. package/src/widgets-v2/selection-summary/style.ts +15 -0
  185. package/dist/category-DwaeYjpX.js +0 -656
  186. package/dist/category-DwaeYjpX.js.map +0 -1
  187. package/dist/change-column-B4IT0rh6.js +0 -1110
  188. package/dist/change-column-B4IT0rh6.js.map +0 -1
@@ -0,0 +1,47 @@
1
+ import { Box, Link, Typography } from '@mui/material'
2
+ import { ErrorOutline } from '@mui/icons-material'
3
+ import type { ChatErrorMessageProps } from '../types'
4
+ import { styles } from './styles'
5
+
6
+ export function ChatErrorMessage({
7
+ errors,
8
+ icon,
9
+ actions,
10
+ sx,
11
+ }: ChatErrorMessageProps) {
12
+ return (
13
+ <Box sx={{ ...styles.errorMessage, ...sx }}>
14
+ {icon ?? <ErrorOutline fontSize='medium' color='error' />}
15
+ {errors.map((error, index) => (
16
+ <Typography key={index} variant='subtitle2' color='error.relatedDark'>
17
+ {error}
18
+ </Typography>
19
+ ))}
20
+ {actions?.length ? (
21
+ <Box sx={styles.errorActions}>
22
+ {(actions ?? []).map((action, index) => (
23
+ <span key={index}>
24
+ {index > 0 && (
25
+ <Typography
26
+ component='span'
27
+ variant='body2'
28
+ sx={styles.errorActionSeparator}
29
+ >
30
+ &middot;
31
+ </Typography>
32
+ )}
33
+ <Link
34
+ component='button'
35
+ variant='body2'
36
+ onClick={action.onClick}
37
+ sx={styles.errorAction}
38
+ >
39
+ {action.label}
40
+ </Link>
41
+ </span>
42
+ ))}
43
+ </Box>
44
+ ) : null}
45
+ </Box>
46
+ )
47
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChatSuggestionButton } from './chat-suggestion-button'
4
+
5
+ describe('ChatSuggestionButton', () => {
6
+ test('renders label text', () => {
7
+ render(<ChatSuggestionButton label='Try this' onClick={vi.fn()} />)
8
+ expect(screen.getByText('Try this')).toBeTruthy()
9
+ })
10
+
11
+ test('calls onClick when clicked', () => {
12
+ const onClick = vi.fn()
13
+ render(<ChatSuggestionButton label='Click me' onClick={onClick} />)
14
+ fireEvent.click(screen.getByText('Click me'))
15
+ expect(onClick).toHaveBeenCalledTimes(1)
16
+ })
17
+
18
+ test('is disabled when disabled prop is true', () => {
19
+ const onClick = vi.fn()
20
+ render(<ChatSuggestionButton label='Disabled' onClick={onClick} disabled />)
21
+ const button = screen.getByRole('button')
22
+ expect(button.hasAttribute('disabled')).toBeTruthy()
23
+ })
24
+ })
@@ -0,0 +1,27 @@
1
+ import { ButtonBase, Typography } from '@mui/material'
2
+ import { ArrowUpward } from '@mui/icons-material'
3
+ import type { ChatSuggestionButtonProps } from '../types'
4
+ import { styles } from './styles'
5
+
6
+ export function ChatSuggestionButton({
7
+ label,
8
+ color,
9
+ sx,
10
+ ...props
11
+ }: ChatSuggestionButtonProps) {
12
+ return (
13
+ <ButtonBase
14
+ sx={{
15
+ ...styles.suggestionButton,
16
+ ...(color ? { backgroundColor: color } : undefined),
17
+ ...sx,
18
+ }}
19
+ {...props}
20
+ >
21
+ <Typography color='inherit' variant='body2'>
22
+ {label}
23
+ </Typography>
24
+ <ArrowUpward />
25
+ </ButtonBase>
26
+ )
27
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ChatUserMessage } from './chat-user-message'
4
+
5
+ describe('ChatUserMessage', () => {
6
+ test('renders children content', () => {
7
+ render(<ChatUserMessage>Hello world</ChatUserMessage>)
8
+ expect(screen.getByText('Hello world')).toBeTruthy()
9
+ })
10
+
11
+ test('renders secondary content when provided', () => {
12
+ render(
13
+ <ChatUserMessage topContext={<span>Secondary</span>}>
14
+ Primary
15
+ </ChatUserMessage>,
16
+ )
17
+ expect(screen.getByText('Primary')).toBeTruthy()
18
+ expect(screen.getByText('Secondary')).toBeTruthy()
19
+ })
20
+
21
+ test('does not render secondary content when not provided', () => {
22
+ const { container } = render(
23
+ <ChatUserMessage>Only primary</ChatUserMessage>,
24
+ )
25
+ expect(container.textContent).toBe('Only primary')
26
+ })
27
+ })
@@ -0,0 +1,27 @@
1
+ import { Box } from '@mui/material'
2
+ import type { ChatUserMessageProps } from '../types'
3
+ import { ChatMessageOverflow, styles } from './styles'
4
+
5
+ export function ChatUserMessage({
6
+ children,
7
+ muted = false,
8
+ topContext,
9
+ sx,
10
+ }: ChatUserMessageProps) {
11
+ return (
12
+ <Box className='PsChat--user-message' sx={styles.userMessageContainer}>
13
+ {topContext && <Box sx={styles.userMessageTop}>{topContext}</Box>}
14
+ <ChatMessageOverflow
15
+ className='PsChat--user-message-inner'
16
+ variant='body2'
17
+ sx={{
18
+ ...styles.userMessage,
19
+ ...(muted ? styles.muted : undefined),
20
+ ...sx,
21
+ }}
22
+ >
23
+ {children}
24
+ </ChatMessageOverflow>
25
+ </Box>
26
+ )
27
+ }
@@ -0,0 +1,4 @@
1
+ export { ChatUserMessage } from './chat-user-message'
2
+ export { ChatAgentMessage } from './chat-agent-message'
3
+ export { ChatErrorMessage } from './chat-error-message'
4
+ export { ChatSuggestionButton } from './chat-suggestion-button'
@@ -0,0 +1,148 @@
1
+ import { styled, Typography, type SxProps, type Theme } from '@mui/material'
2
+
3
+ export const ChatMessageOverflow = styled(Typography)(() => ({
4
+ whiteSpace: 'pre-wrap',
5
+ overflowX: 'clip',
6
+ overflowWrap: 'break-word',
7
+ maxWidth: '100%',
8
+ }))
9
+ ChatMessageOverflow.displayName = 'ChatMessageOverflow'
10
+
11
+ export const styles = {
12
+ agentMessageContainer: (theme: Theme) => ({
13
+ width: '100%',
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ alignItems: 'flex-start',
17
+ paddingRight: theme.spacing(4),
18
+ '& + .PsChat--agent-message': {
19
+ marginTop: theme.spacing(1),
20
+ },
21
+ '& + .PsChat--user-message': {
22
+ marginTop: theme.spacing(3),
23
+ },
24
+ '& > p:first-of-type': {
25
+ marginTop: 0,
26
+ },
27
+ '& table': {
28
+ alignSelf: 'stretch',
29
+ display: 'block',
30
+ overflowX: 'auto',
31
+ maxWidth: '100%',
32
+ borderCollapse: 'collapse',
33
+ margin: theme.spacing(1, 0),
34
+ fontSize: '0.875em',
35
+ },
36
+ '& th, & td': {
37
+ border: `1px solid ${theme.palette.divider}`,
38
+ padding: theme.spacing(0.5, 1),
39
+ textAlign: 'left',
40
+ },
41
+ '& th': {
42
+ backgroundColor: theme.palette.action.hover,
43
+ fontWeight: 600,
44
+ },
45
+ '& tr:nth-of-type(even) td': {
46
+ backgroundColor: theme.palette.action.hover,
47
+ },
48
+ '& ul.contains-task-list': {
49
+ listStyle: 'none',
50
+ paddingLeft: 0,
51
+ },
52
+ '& .task-list-item input[type="checkbox"]': {
53
+ marginRight: theme.spacing(0.5),
54
+ },
55
+ '& del': {
56
+ textDecoration: 'line-through',
57
+ opacity: 0.7,
58
+ },
59
+ '& > ul, & > ol': {
60
+ margin: theme.spacing(1, 0),
61
+ paddingLeft: theme.spacing(3),
62
+ '& > li + li': {
63
+ marginTop: theme.spacing(1),
64
+ },
65
+ },
66
+ }),
67
+ userMessageContainer: {
68
+ width: '100%',
69
+ display: 'flex',
70
+ flexDirection: 'column',
71
+ alignItems: 'flex-end',
72
+ paddingLeft: ({ spacing }) => spacing(4),
73
+ '&:has(+ .PsChat--user-message) > .PsChat--user-message-inner': {
74
+ borderRadius: ({ spacing }) => spacing(2),
75
+ },
76
+ '& + .PsChat--user-message': {
77
+ marginTop: ({ spacing }) => spacing(0.5),
78
+ },
79
+ '& + .PsChat--agent-message': {
80
+ marginTop: ({ spacing }) => spacing(3),
81
+ },
82
+ },
83
+ userMessage: {
84
+ padding: ({ spacing }) => spacing(0.75, 1.5),
85
+ borderRadius: ({ spacing }) => spacing(2, 2, 0.25, 2),
86
+ border: ({ palette }) => `1px solid ${palette.divider}`,
87
+ backgroundColor: ({ palette }) => palette.background.default,
88
+ color: ({ palette }) => palette.text.primary,
89
+ width: 'fit-content',
90
+ },
91
+ muted: {
92
+ color: ({ palette }) => palette.text.disabled,
93
+ },
94
+ userMessageTop: {
95
+ textAlign: 'right',
96
+ marginBottom: ({ spacing }) => spacing(0.5),
97
+ },
98
+ errorMessage: {
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ flexWrap: 'wrap',
102
+ gap: ({ spacing }) => spacing(1),
103
+ },
104
+ errorActions: {
105
+ display: 'flex',
106
+ alignItems: 'center',
107
+ gap: ({ spacing }) => spacing(0.5),
108
+ },
109
+ errorAction: {
110
+ color: ({ palette }) => palette.text.primary,
111
+ },
112
+ errorActionSeparator: {
113
+ mr: 0.5,
114
+ color: ({ palette }) => palette.black[25],
115
+ fontWeight: 600,
116
+ },
117
+ suggestionButton: {
118
+ transition: 'border-color 0.2s',
119
+ width: '100%',
120
+ alignItems: 'flex-start',
121
+ justifyContent: 'space-between',
122
+ textAlign: 'left',
123
+ gap: ({ spacing }) => spacing(1),
124
+ padding: ({ spacing }) => spacing(0.75, 1.5),
125
+ borderRadius: ({ spacing }) => spacing(2),
126
+ border: ({ palette }) => `1px solid ${palette.divider}`,
127
+ '& .MuiSvgIcon-root': {
128
+ transition: 'color 0.2s',
129
+ color: 'inherit',
130
+ fontSize: ({ spacing }) => spacing(1.5),
131
+ width: ({ spacing }) => spacing(1.5),
132
+ minWidth: ({ spacing }) => spacing(1.5),
133
+ height: ({ spacing }) => spacing(2.5),
134
+ },
135
+ '&:not(.Mui-disabled) .MuiSvgIcon-root': {
136
+ color: ({ palette }) => palette.text.secondary,
137
+ },
138
+ '&:hover': {
139
+ borderColor: ({ palette }) => palette.text.hint,
140
+ '& .MuiSvgIcon-root': {
141
+ color: ({ palette }) => palette.text.primary,
142
+ },
143
+ },
144
+ '&.Mui-disabled': {
145
+ color: ({ palette }) => palette.text.disabled,
146
+ },
147
+ },
148
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,4 @@
1
+ export const CHAT_MAX_WIDTH = 768
2
+ export const CHAT_SCROLL_DELAY = 300
3
+ export const CHAT_DIVIDER_DELAY = 100
4
+ export const CHAT_TOOL_CODE_AREA_MAX_HEIGHT = 126
@@ -0,0 +1,269 @@
1
+ import {
2
+ describe,
3
+ test,
4
+ expect,
5
+ vi,
6
+ beforeEach,
7
+ afterEach,
8
+ type MockInstance,
9
+ } from 'vitest'
10
+ import { createRef } from 'react'
11
+ import { act, render, screen } from '@testing-library/react'
12
+ import { ChatContent } from './chat-content'
13
+ import type { ChatContentRef } from '../types'
14
+
15
+ type IOCallback = (entries: IntersectionObserverEntry[]) => void
16
+
17
+ interface FakeObserver {
18
+ callback: IOCallback
19
+ observed: Element[]
20
+ disconnected: boolean
21
+ }
22
+
23
+ interface FakeMutationObserverInstance {
24
+ callback: MutationCallback
25
+ target: Node | null
26
+ disconnected: boolean
27
+ }
28
+
29
+ let observers: FakeObserver[] = []
30
+ let mutationObservers: FakeMutationObserverInstance[] = []
31
+ let scrollToSpy: MockInstance<Element['scrollTo']>
32
+
33
+ // Mutable "DOM measurements" the prototype getters read from.
34
+ let scrollHeightValue = 0
35
+ let scrollTopValue = 0
36
+ let clientHeightValue = 0
37
+
38
+ class FakeIntersectionObserver {
39
+ _obs: FakeObserver
40
+ constructor(cb: IOCallback) {
41
+ this._obs = { callback: cb, observed: [], disconnected: false }
42
+ observers.push(this._obs)
43
+ }
44
+ observe(el: Element) {
45
+ this._obs.observed.push(el)
46
+ }
47
+ unobserve() {
48
+ // NOOP
49
+ }
50
+ disconnect() {
51
+ this._obs.disconnected = true
52
+ }
53
+ takeRecords(): IntersectionObserverEntry[] {
54
+ return []
55
+ }
56
+ }
57
+
58
+ class FakeMutationObserver {
59
+ _inst: FakeMutationObserverInstance
60
+ constructor(cb: MutationCallback) {
61
+ this._inst = { callback: cb, target: null, disconnected: false }
62
+ mutationObservers.push(this._inst)
63
+ }
64
+ observe(target: Node) {
65
+ this._inst.target = target
66
+ }
67
+ disconnect() {
68
+ this._inst.disconnected = true
69
+ }
70
+ takeRecords(): MutationRecord[] {
71
+ return []
72
+ }
73
+ }
74
+
75
+ /**
76
+ * The component creates the top observer first, then the bottom one — so
77
+ * `observers[0]` is top and `observers[1]` is bottom. Helper trips one of them.
78
+ */
79
+ function fireIntersection(index: 0 | 1, isIntersecting: boolean) {
80
+ const obs = observers[index]
81
+ if (!obs) throw new Error(`No observer at index ${index}`)
82
+ act(() => {
83
+ obs.callback([
84
+ { isIntersecting, target: obs.observed[0]! } as IntersectionObserverEntry,
85
+ ])
86
+ })
87
+ }
88
+
89
+ /** Triggers the most recent MutationObserver as if a DOM mutation happened. */
90
+ function fireMutation() {
91
+ const obs = mutationObservers.at(-1)
92
+ if (!obs) throw new Error('No MutationObserver registered')
93
+ act(() => {
94
+ obs.callback([], obs as unknown as MutationObserver)
95
+ })
96
+ }
97
+
98
+ describe('ChatContent', () => {
99
+ beforeEach(() => {
100
+ observers = []
101
+ mutationObservers = []
102
+ scrollHeightValue = 0
103
+ scrollTopValue = 0
104
+ clientHeightValue = 0
105
+ vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
106
+ vi.stubGlobal('MutationObserver', FakeMutationObserver)
107
+ // Run rAF synchronously so MutationObserver-driven scroll checks happen
108
+ // inside `act`.
109
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
110
+ cb(0)
111
+ return 1
112
+ })
113
+ vi.stubGlobal('cancelAnimationFrame', () => {
114
+ // NOOP
115
+ })
116
+ scrollToSpy = vi
117
+ .spyOn(Element.prototype, 'scrollTo')
118
+ .mockImplementation(() => {
119
+ // NOOP
120
+ })
121
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(
122
+ () => scrollHeightValue,
123
+ )
124
+ vi.spyOn(HTMLElement.prototype, 'scrollTop', 'get').mockImplementation(
125
+ () => scrollTopValue,
126
+ )
127
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(
128
+ () => clientHeightValue,
129
+ )
130
+ })
131
+
132
+ afterEach(() => {
133
+ vi.unstubAllGlobals()
134
+ vi.restoreAllMocks()
135
+ })
136
+
137
+ test('renders children', () => {
138
+ render(<ChatContent>Scrollable content</ChatContent>)
139
+ expect(screen.getByText('Scrollable content')).toBeTruthy()
140
+ })
141
+
142
+ test('observes the top and bottom sentinels', () => {
143
+ render(<ChatContent>content</ChatContent>)
144
+ expect(observers).toHaveLength(2)
145
+ expect(observers[0]?.observed).toHaveLength(1)
146
+ expect(observers[1]?.observed).toHaveLength(1)
147
+ })
148
+
149
+ test('ref starts with isAtTop and isAtBottom both true', () => {
150
+ const ref = createRef<ChatContentRef>()
151
+ render(<ChatContent ref={ref}>content</ChatContent>)
152
+ expect(ref.current?.isAtTop).toBe(true)
153
+ expect(ref.current?.isAtBottom).toBe(true)
154
+ })
155
+
156
+ test('isAtBottom flips when the bottom sentinel leaves the viewport', () => {
157
+ const ref = createRef<ChatContentRef>()
158
+ render(<ChatContent ref={ref}>content</ChatContent>)
159
+ fireIntersection(1, false)
160
+ expect(ref.current?.isAtBottom).toBe(false)
161
+ fireIntersection(1, true)
162
+ expect(ref.current?.isAtBottom).toBe(true)
163
+ })
164
+
165
+ test('isAtTop flips when the top sentinel leaves the viewport', () => {
166
+ const ref = createRef<ChatContentRef>()
167
+ render(<ChatContent ref={ref}>content</ChatContent>)
168
+ fireIntersection(0, false)
169
+ expect(ref.current?.isAtTop).toBe(false)
170
+ fireIntersection(0, true)
171
+ expect(ref.current?.isAtTop).toBe(true)
172
+ })
173
+
174
+ test('scrollToBottom() scrolls to scrollHeight with smooth behaviour', () => {
175
+ scrollHeightValue = 1000
176
+ const ref = createRef<ChatContentRef>()
177
+ render(<ChatContent ref={ref}>content</ChatContent>)
178
+ act(() => ref.current?.scrollToBottom())
179
+ expect(scrollToSpy).toHaveBeenCalledWith({
180
+ top: 1000,
181
+ behavior: 'smooth',
182
+ })
183
+ })
184
+
185
+ test('scrollToTop() scrolls the container to top 0', () => {
186
+ const ref = createRef<ChatContentRef>()
187
+ render(<ChatContent ref={ref}>content</ChatContent>)
188
+ act(() => ref.current?.scrollToTop())
189
+ expect(scrollToSpy).toHaveBeenCalledWith({
190
+ top: 0,
191
+ behavior: 'smooth',
192
+ })
193
+ })
194
+
195
+ test('clicking the jump-to-latest FAB scrolls to the bottom', () => {
196
+ render(<ChatContent>content</ChatContent>)
197
+ fireIntersection(1, false) // not at bottom anymore
198
+ const fab = screen.getByLabelText('Jump to latest')
199
+ act(() => fab.click())
200
+ expect(scrollToSpy).toHaveBeenCalled()
201
+ })
202
+
203
+ test('uses custom jumpToLatest label', () => {
204
+ render(
205
+ <ChatContent labels={{ jumpToLatest: 'Ir al final' }}>x</ChatContent>,
206
+ )
207
+ expect(screen.getByLabelText('Ir al final')).toBeTruthy()
208
+ })
209
+
210
+ test('disconnects observers on unmount', () => {
211
+ const { unmount } = render(<ChatContent>content</ChatContent>)
212
+ unmount()
213
+ expect(observers[0]?.disconnected).toBe(true)
214
+ expect(observers[1]?.disconnected).toBe(true)
215
+ })
216
+
217
+ describe('autoScroll', () => {
218
+ test('attaches a MutationObserver by default', () => {
219
+ render(<ChatContent>content</ChatContent>)
220
+ expect(mutationObservers).toHaveLength(1)
221
+ })
222
+
223
+ test('does not attach a MutationObserver when autoScroll={false}', () => {
224
+ render(<ChatContent autoScroll={false}>content</ChatContent>)
225
+ expect(mutationObservers).toHaveLength(0)
226
+ })
227
+
228
+ test('scrolls to the new bottom when content grows and the user is at the bottom', () => {
229
+ // At-bottom: scrollHeight - scrollTop - clientHeight = 0
230
+ scrollHeightValue = 1000
231
+ clientHeightValue = 500
232
+ scrollTopValue = 500
233
+
234
+ render(<ChatContent autoScroll>content</ChatContent>)
235
+ expect(mutationObservers).toHaveLength(1)
236
+
237
+ // Content grows by 200px while the user is still at the bottom.
238
+ scrollHeightValue = 1200
239
+ fireMutation()
240
+
241
+ expect(scrollToSpy).toHaveBeenCalledWith({
242
+ top: 1200,
243
+ behavior: 'smooth',
244
+ })
245
+ })
246
+
247
+ test('does NOT scroll when the user has scrolled up to read history', () => {
248
+ // 400px from the bottom: 1000 - 100 - 500 = 400 (well past the 32px slack)
249
+ scrollHeightValue = 1000
250
+ clientHeightValue = 500
251
+ scrollTopValue = 100
252
+
253
+ render(<ChatContent autoScroll>content</ChatContent>)
254
+
255
+ // Content grows by 200px — distance-from-bottom is now 600, growth is
256
+ // 200, so 600 > 200 + 32 → leave the user alone.
257
+ scrollHeightValue = 1200
258
+ fireMutation()
259
+
260
+ expect(scrollToSpy).not.toHaveBeenCalled()
261
+ })
262
+
263
+ test('disconnects the MutationObserver on unmount', () => {
264
+ const { unmount } = render(<ChatContent autoScroll>content</ChatContent>)
265
+ unmount()
266
+ expect(mutationObservers[0]?.disconnected).toBe(true)
267
+ })
268
+ })
269
+ })