@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.
- package/dist/chat.js +798 -0
- package/dist/chat.js.map +1 -0
- package/dist/components.js +723 -711
- package/dist/components.js.map +1 -1
- package/dist/{lasso-tool-jl4YK02H.js → lasso-tool-BYbxrJ-7.js} +184 -190
- package/dist/lasso-tool-BYbxrJ-7.js.map +1 -0
- package/dist/{row-BKmVAUN5.js → row-DTCV0Ocm.js} +2 -2
- package/dist/row-DTCV0Ocm.js.map +1 -0
- package/dist/{series-D1pynfeh.js → series-CYNOu2Ju.js} +2 -2
- package/dist/{series-D1pynfeh.js.map → series-CYNOu2Ju.js.map} +1 -1
- package/dist/smart-tooltip-D4vwQpFf.js +37 -0
- package/dist/smart-tooltip-D4vwQpFf.js.map +1 -0
- package/dist/{styles-DrPyd0y5.js → styles-CAroD5Rc.js} +12 -12
- package/dist/styles-CAroD5Rc.js.map +1 -0
- package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
- package/dist/types/chat/bubbles/index.d.ts +4 -0
- package/dist/types/chat/const.d.ts +3 -0
- package/dist/types/chat/containers/chat-content.d.ts +2 -0
- package/dist/types/chat/containers/chat-footer.d.ts +2 -0
- package/dist/types/chat/containers/chat-header.d.ts +2 -0
- package/dist/types/chat/containers/chat-starter.d.ts +2 -0
- package/dist/types/chat/containers/index.d.ts +4 -0
- package/dist/types/chat/containers/styles.d.ts +101 -0
- package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
- package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
- package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tools.d.ts +2 -0
- package/dist/types/chat/feedback/index.d.ts +5 -0
- package/dist/types/chat/feedback/styles.d.ts +65 -0
- package/dist/types/chat/index.d.ts +16 -0
- package/dist/types/chat/types.d.ts +99 -0
- package/dist/types/components/copy-button/copy-button.d.ts +2 -0
- package/dist/types/components/copy-button/types.d.ts +6 -0
- package/dist/types/components/index.d.ts +2 -0
- package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +1 -1
- package/dist/types/widgets/category/config.d.ts +3 -10
- package/dist/types/widgets/range/config.d.ts +0 -4
- package/dist/types/widgets/spread/config.d.ts +0 -5
- package/dist/types/widgets/table/config.d.ts +1 -2
- package/dist/types/widgets/table/table-ui.d.ts +1 -1
- package/dist/types/widgets/wrapper/components/options.d.ts +1 -1
- package/dist/widgets/actions.js +614 -627
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +49 -49
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +37 -36
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/formula.js +14 -13
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +53 -52
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +10 -9
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/pie.js +16 -15
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/range.js +1 -1
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +20 -19
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/spread.js +60 -59
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/table.js +68 -67
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +26 -25
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/wrapper.js +153 -162
- package/dist/widgets/wrapper.js.map +1 -1
- package/package.json +7 -3
- package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
- package/src/chat/bubbles/chat-agent-message.tsx +11 -0
- package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
- package/src/chat/bubbles/chat-error-message.tsx +47 -0
- package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
- package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.tsx +27 -0
- package/src/chat/bubbles/index.ts +4 -0
- package/src/chat/bubbles/styles.ts +106 -0
- package/src/chat/const.ts +3 -0
- package/src/chat/containers/chat-content.test.tsx +15 -0
- package/src/chat/containers/chat-content.tsx +32 -0
- package/src/chat/containers/chat-footer.test.tsx +34 -0
- package/src/chat/containers/chat-footer.tsx +78 -0
- package/src/chat/containers/chat-header.test.tsx +28 -0
- package/src/chat/containers/chat-header.tsx +29 -0
- package/src/chat/containers/chat-starter.test.tsx +32 -0
- package/src/chat/containers/chat-starter.tsx +75 -0
- package/src/chat/containers/index.ts +4 -0
- package/src/chat/containers/styles.ts +107 -0
- package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
- package/src/chat/feedback/chat-actions-container.tsx +7 -0
- package/src/chat/feedback/chat-loader.test.tsx +10 -0
- package/src/chat/feedback/chat-loader.tsx +31 -0
- package/src/chat/feedback/chat-rating-action.tsx +43 -0
- package/src/chat/feedback/chat-thinking.test.tsx +15 -0
- package/src/chat/feedback/chat-thinking.tsx +23 -0
- package/src/chat/feedback/chat-tools.test.tsx +23 -0
- package/src/chat/feedback/chat-tools.tsx +54 -0
- package/src/chat/feedback/index.ts +5 -0
- package/src/chat/feedback/styles.ts +80 -0
- package/src/chat/index.ts +45 -0
- package/src/chat/types.ts +124 -0
- package/src/components/basemaps/basemaps.tsx +3 -1
- package/src/components/copy-button/copy-button.test.tsx +41 -0
- package/src/components/copy-button/copy-button.tsx +31 -0
- package/src/components/copy-button/types.ts +10 -0
- package/src/components/geolocation-controls/geolocation-controls.tsx +10 -6
- package/src/components/index.ts +3 -0
- package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -2
- package/src/components/lasso-tool/lasso-tool.tsx +9 -3
- package/src/components/list-data/list-data-skeleton.tsx +1 -1
- package/src/components/list-data/list-data.tsx +5 -3
- package/src/components/measurement-tools/measurement-tools.tsx +5 -1
- package/src/components/smart-tooltip/smart-tooltip.tsx +3 -1
- package/src/widgets/_shared/chart-config/option-builders.test.ts +2 -2
- package/src/widgets/_shared/chart-config/option-builders.ts +6 -4
- package/src/widgets/actions/download/download.test.tsx +6 -2
- package/src/widgets/actions/download/download.tsx +3 -1
- package/src/widgets/actions/fullscreen/fullscreen.tsx +8 -1
- package/src/widgets/actions/relative-data/relative-data.tsx +2 -6
- package/src/widgets/actions/searcher/searcher.tsx +0 -6
- package/src/widgets/bar/config.ts +8 -4
- package/src/widgets/bar/skeleton.tsx +1 -1
- package/src/widgets/category/components/category-row-multi.tsx +1 -1
- package/src/widgets/category/config.ts +1 -11
- package/src/widgets/formula/components/row.tsx +1 -1
- package/src/widgets/histogram/config.ts +7 -2
- package/src/widgets/histogram/skeleton.tsx +2 -2
- package/src/widgets/pie/skeleton.tsx +1 -1
- package/src/widgets/range/config.ts +0 -5
- package/src/widgets/scatterplot/skeleton.tsx +2 -2
- package/src/widgets/spread/config.ts +0 -6
- package/src/widgets/table/config.ts +1 -1
- package/src/widgets/table/table-ui.tsx +1 -1
- package/src/widgets/timeseries/skeleton.tsx +1 -1
- package/src/widgets/wrapper/components/actions.test.tsx +6 -2
- package/src/widgets/wrapper/components/actions.tsx +3 -1
- package/src/widgets/wrapper/components/options.test.tsx +12 -4
- package/src/widgets/wrapper/components/options.tsx +8 -3
- package/src/widgets/wrapper/wrapper-ui.tsx +5 -2
- package/src/widgets/wrapper/wrapper.tsx +2 -4
- package/dist/lasso-tool-jl4YK02H.js.map +0 -1
- package/dist/row-BKmVAUN5.js.map +0 -1
- package/dist/smart-tooltip-BEtBaIdz.js +0 -39
- package/dist/smart-tooltip-BEtBaIdz.js.map +0 -1
- package/dist/styles-DrPyd0y5.js.map +0 -1
- package/dist/types/widgets/actions/relative-data/style.d.ts +0 -8
- package/dist/types/widgets/actions/zoom-toggle/index.d.ts +0 -2
- package/dist/types/widgets/table/components/index.d.ts +0 -4
- package/src/widgets/actions/relative-data/style.ts +0 -9
- package/src/widgets/actions/zoom-toggle/index.ts +0 -2
- 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
|
+
·
|
|
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,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,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
|
+
}
|