@carto/ps-react-ui 4.3.9 → 4.4.0-chat-ui.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 (154) hide show
  1. package/dist/chat.js +798 -0
  2. package/dist/chat.js.map +1 -0
  3. package/dist/components.js +723 -711
  4. package/dist/components.js.map +1 -1
  5. package/dist/{lasso-tool-jl4YK02H.js → lasso-tool-BYbxrJ-7.js} +184 -190
  6. package/dist/lasso-tool-BYbxrJ-7.js.map +1 -0
  7. package/dist/{row-BKmVAUN5.js → row-DTCV0Ocm.js} +2 -2
  8. package/dist/row-DTCV0Ocm.js.map +1 -0
  9. package/dist/{series-D1pynfeh.js → series-CYNOu2Ju.js} +2 -2
  10. package/dist/{series-D1pynfeh.js.map → series-CYNOu2Ju.js.map} +1 -1
  11. package/dist/smart-tooltip-D4vwQpFf.js +37 -0
  12. package/dist/smart-tooltip-D4vwQpFf.js.map +1 -0
  13. package/dist/{styles-DrPyd0y5.js → styles-CAroD5Rc.js} +12 -12
  14. package/dist/styles-CAroD5Rc.js.map +1 -0
  15. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  16. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  17. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  18. package/dist/types/chat/bubbles/index.d.ts +4 -0
  19. package/dist/types/chat/const.d.ts +3 -0
  20. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  21. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  22. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  23. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  24. package/dist/types/chat/containers/index.d.ts +4 -0
  25. package/dist/types/chat/containers/styles.d.ts +101 -0
  26. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  27. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  28. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  29. package/dist/types/chat/feedback/chat-tools.d.ts +2 -0
  30. package/dist/types/chat/feedback/index.d.ts +5 -0
  31. package/dist/types/chat/feedback/styles.d.ts +65 -0
  32. package/dist/types/chat/index.d.ts +16 -0
  33. package/dist/types/chat/types.d.ts +99 -0
  34. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  35. package/dist/types/components/copy-button/types.d.ts +6 -0
  36. package/dist/types/components/index.d.ts +2 -0
  37. package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +1 -1
  38. package/dist/types/widgets/category/config.d.ts +3 -10
  39. package/dist/types/widgets/range/config.d.ts +0 -4
  40. package/dist/types/widgets/spread/config.d.ts +0 -5
  41. package/dist/types/widgets/table/config.d.ts +1 -2
  42. package/dist/types/widgets/table/table-ui.d.ts +1 -1
  43. package/dist/types/widgets/wrapper/components/options.d.ts +1 -1
  44. package/dist/widgets/actions.js +614 -627
  45. package/dist/widgets/actions.js.map +1 -1
  46. package/dist/widgets/bar.js +49 -49
  47. package/dist/widgets/bar.js.map +1 -1
  48. package/dist/widgets/category.js +37 -36
  49. package/dist/widgets/category.js.map +1 -1
  50. package/dist/widgets/formula.js +14 -13
  51. package/dist/widgets/formula.js.map +1 -1
  52. package/dist/widgets/histogram.js +53 -52
  53. package/dist/widgets/histogram.js.map +1 -1
  54. package/dist/widgets/markdown.js +10 -9
  55. package/dist/widgets/markdown.js.map +1 -1
  56. package/dist/widgets/pie.js +16 -15
  57. package/dist/widgets/pie.js.map +1 -1
  58. package/dist/widgets/range.js +1 -1
  59. package/dist/widgets/range.js.map +1 -1
  60. package/dist/widgets/scatterplot.js +20 -19
  61. package/dist/widgets/scatterplot.js.map +1 -1
  62. package/dist/widgets/spread.js +60 -59
  63. package/dist/widgets/spread.js.map +1 -1
  64. package/dist/widgets/table.js +68 -67
  65. package/dist/widgets/table.js.map +1 -1
  66. package/dist/widgets/timeseries.js +26 -25
  67. package/dist/widgets/timeseries.js.map +1 -1
  68. package/dist/widgets/wrapper.js +153 -162
  69. package/dist/widgets/wrapper.js.map +1 -1
  70. package/package.json +7 -3
  71. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  72. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  73. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  74. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  75. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  76. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  77. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  78. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  79. package/src/chat/bubbles/index.ts +4 -0
  80. package/src/chat/bubbles/styles.ts +106 -0
  81. package/src/chat/const.ts +3 -0
  82. package/src/chat/containers/chat-content.test.tsx +15 -0
  83. package/src/chat/containers/chat-content.tsx +32 -0
  84. package/src/chat/containers/chat-footer.test.tsx +34 -0
  85. package/src/chat/containers/chat-footer.tsx +78 -0
  86. package/src/chat/containers/chat-header.test.tsx +28 -0
  87. package/src/chat/containers/chat-header.tsx +29 -0
  88. package/src/chat/containers/chat-starter.test.tsx +32 -0
  89. package/src/chat/containers/chat-starter.tsx +75 -0
  90. package/src/chat/containers/index.ts +4 -0
  91. package/src/chat/containers/styles.ts +107 -0
  92. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  93. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  94. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  95. package/src/chat/feedback/chat-loader.tsx +31 -0
  96. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  97. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  98. package/src/chat/feedback/chat-thinking.tsx +23 -0
  99. package/src/chat/feedback/chat-tools.test.tsx +23 -0
  100. package/src/chat/feedback/chat-tools.tsx +54 -0
  101. package/src/chat/feedback/index.ts +5 -0
  102. package/src/chat/feedback/styles.ts +80 -0
  103. package/src/chat/index.ts +45 -0
  104. package/src/chat/types.ts +124 -0
  105. package/src/components/basemaps/basemaps.tsx +3 -1
  106. package/src/components/copy-button/copy-button.test.tsx +41 -0
  107. package/src/components/copy-button/copy-button.tsx +31 -0
  108. package/src/components/copy-button/types.ts +10 -0
  109. package/src/components/geolocation-controls/geolocation-controls.tsx +10 -6
  110. package/src/components/index.ts +3 -0
  111. package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -2
  112. package/src/components/lasso-tool/lasso-tool.tsx +9 -3
  113. package/src/components/list-data/list-data-skeleton.tsx +1 -1
  114. package/src/components/list-data/list-data.tsx +5 -3
  115. package/src/components/measurement-tools/measurement-tools.tsx +5 -1
  116. package/src/components/smart-tooltip/smart-tooltip.tsx +3 -1
  117. package/src/widgets/_shared/chart-config/option-builders.test.ts +2 -2
  118. package/src/widgets/_shared/chart-config/option-builders.ts +6 -4
  119. package/src/widgets/actions/download/download.test.tsx +6 -2
  120. package/src/widgets/actions/download/download.tsx +3 -1
  121. package/src/widgets/actions/fullscreen/fullscreen.tsx +8 -1
  122. package/src/widgets/actions/relative-data/relative-data.tsx +2 -6
  123. package/src/widgets/actions/searcher/searcher.tsx +0 -6
  124. package/src/widgets/bar/config.ts +8 -4
  125. package/src/widgets/bar/skeleton.tsx +1 -1
  126. package/src/widgets/category/components/category-row-multi.tsx +1 -1
  127. package/src/widgets/category/config.ts +1 -11
  128. package/src/widgets/formula/components/row.tsx +1 -1
  129. package/src/widgets/histogram/config.ts +7 -2
  130. package/src/widgets/histogram/skeleton.tsx +2 -2
  131. package/src/widgets/pie/skeleton.tsx +1 -1
  132. package/src/widgets/range/config.ts +0 -5
  133. package/src/widgets/scatterplot/skeleton.tsx +2 -2
  134. package/src/widgets/spread/config.ts +0 -6
  135. package/src/widgets/table/config.ts +1 -1
  136. package/src/widgets/table/table-ui.tsx +1 -1
  137. package/src/widgets/timeseries/skeleton.tsx +1 -1
  138. package/src/widgets/wrapper/components/actions.test.tsx +6 -2
  139. package/src/widgets/wrapper/components/actions.tsx +3 -1
  140. package/src/widgets/wrapper/components/options.test.tsx +12 -4
  141. package/src/widgets/wrapper/components/options.tsx +8 -3
  142. package/src/widgets/wrapper/wrapper-ui.tsx +5 -2
  143. package/src/widgets/wrapper/wrapper.tsx +2 -4
  144. package/dist/lasso-tool-jl4YK02H.js.map +0 -1
  145. package/dist/row-BKmVAUN5.js.map +0 -1
  146. package/dist/smart-tooltip-BEtBaIdz.js +0 -39
  147. package/dist/smart-tooltip-BEtBaIdz.js.map +0 -1
  148. package/dist/styles-DrPyd0y5.js.map +0 -1
  149. package/dist/types/widgets/actions/relative-data/style.d.ts +0 -8
  150. package/dist/types/widgets/actions/zoom-toggle/index.d.ts +0 -2
  151. package/dist/types/widgets/table/components/index.d.ts +0 -4
  152. package/src/widgets/actions/relative-data/style.ts +0 -9
  153. package/src/widgets/actions/zoom-toggle/index.ts +0 -2
  154. package/src/widgets/table/components/index.ts +0 -4
@@ -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,106 @@
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
+ }),
25
+ userMessageContainer: {
26
+ width: '100%',
27
+ display: 'flex',
28
+ flexDirection: 'column',
29
+ alignItems: 'flex-end',
30
+ paddingLeft: ({ spacing }) => spacing(4),
31
+ '&:has(+ .PsChat--user-message) > .PsChat--user-message-inner': {
32
+ borderRadius: ({ spacing }) => spacing(2),
33
+ },
34
+ '& + .PsChat--user-message': {
35
+ marginTop: ({ spacing }) => spacing(0.5),
36
+ },
37
+ '& + .PsChat--agent-message': {
38
+ marginTop: ({ spacing }) => spacing(3),
39
+ },
40
+ },
41
+ userMessage: {
42
+ padding: ({ spacing }) => spacing(0.75, 1.5),
43
+ borderRadius: ({ spacing }) => spacing(2, 2, 0.25, 2),
44
+ border: ({ palette }) => `1px solid ${palette.divider}`,
45
+ backgroundColor: ({ palette }) => palette.background.default,
46
+ color: ({ palette }) => palette.text.primary,
47
+ width: 'fit-content',
48
+ },
49
+ muted: {
50
+ color: ({ palette }) => palette.text.disabled,
51
+ },
52
+ userMessageTop: {
53
+ textAlign: 'right',
54
+ marginBottom: ({ spacing }) => spacing(0.5),
55
+ },
56
+ errorMessage: {
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ flexWrap: 'wrap',
60
+ gap: ({ spacing }) => spacing(1),
61
+ },
62
+ errorActions: {
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ gap: ({ spacing }) => spacing(0.5),
66
+ },
67
+ errorAction: {
68
+ color: ({ palette }) => palette.text.primary,
69
+ },
70
+ errorActionSeparator: {
71
+ mr: 0.5,
72
+ color: ({ palette }) => palette.black[25],
73
+ fontWeight: 600,
74
+ },
75
+ suggestionButton: {
76
+ transition: 'border-color 0.2s',
77
+ width: '100%',
78
+ alignItems: 'flex-start',
79
+ justifyContent: 'space-between',
80
+ textAlign: 'left',
81
+ gap: ({ spacing }) => spacing(1),
82
+ padding: ({ spacing }) => spacing(0.75, 1.5),
83
+ borderRadius: ({ spacing }) => spacing(2),
84
+ border: ({ palette }) => `1px solid ${palette.divider}`,
85
+ '& .MuiSvgIcon-root': {
86
+ transition: 'color 0.2s',
87
+ color: 'inherit',
88
+ fontSize: ({ spacing }) => spacing(1.5),
89
+ width: ({ spacing }) => spacing(1.5),
90
+ minWidth: ({ spacing }) => spacing(1.5),
91
+ height: ({ spacing }) => spacing(2.5),
92
+ },
93
+ '&:not(.Mui-disabled) .MuiSvgIcon-root': {
94
+ color: ({ palette }) => palette.text.secondary,
95
+ },
96
+ '&:hover': {
97
+ borderColor: ({ palette }) => palette.text.hint,
98
+ '& .MuiSvgIcon-root': {
99
+ color: ({ palette }) => palette.text.primary,
100
+ },
101
+ },
102
+ '&.Mui-disabled': {
103
+ color: ({ palette }) => palette.text.disabled,
104
+ },
105
+ },
106
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,3 @@
1
+ export const CHAT_MAX_WIDTH = 768
2
+ export const CHAT_SCROLL_DELAY = 300
3
+ export const CHAT_DIVIDER_DELAY = 100
@@ -0,0 +1,15 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ChatContent } from './chat-content'
4
+
5
+ describe('ChatContent', () => {
6
+ test('renders children', () => {
7
+ render(<ChatContent>Scrollable content</ChatContent>)
8
+ expect(screen.getByText('Scrollable content')).toBeTruthy()
9
+ })
10
+
11
+ // TODO: Test scroll behaviour - verify "Jump to latest" button appears
12
+ // when user scrolls up and disappears when scrolled to bottom.
13
+ // This requires mocking scroll container dimensions and dispatching
14
+ // scroll events with appropriate scrollHeight/scrollTop values.
15
+ })
@@ -0,0 +1,32 @@
1
+ import { useRef } from 'react'
2
+ import { Box, Fab } from '@mui/material'
3
+ import { KeyboardArrowDown } from '@mui/icons-material'
4
+ import type { ChatContentProps } from '../types'
5
+ import { styles } from './styles'
6
+
7
+ export function ChatContent({ children, labels = {}, sx }: ChatContentProps) {
8
+ const scrollRef = useRef<HTMLDivElement>(null)
9
+
10
+ const scrollToBottom = () => {
11
+ scrollRef.current?.scrollTo({
12
+ top: scrollRef.current.scrollHeight,
13
+ behavior: 'smooth',
14
+ })
15
+ }
16
+
17
+ return (
18
+ <Box ref={scrollRef} sx={{ ...styles.content, ...sx }}>
19
+ {children}
20
+ <Box sx={styles.jumpToLatestWrapper}>
21
+ <Fab
22
+ size='small'
23
+ onClick={scrollToBottom}
24
+ aria-label={labels.jumpToLatest ?? 'Jump to latest'}
25
+ sx={styles.jumpToLatest}
26
+ >
27
+ <KeyboardArrowDown />
28
+ </Fab>
29
+ </Box>
30
+ </Box>
31
+ )
32
+ }
@@ -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) => (
65
+ <ChatSuggestionButton
66
+ key={item.label}
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'