@carto/ps-react-ui 4.3.10 → 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 +260 -240
- package/dist/components.js.map +1 -1
- 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/widgets/actions.js +22 -21
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +7 -6
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +9 -8
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/formula.js +11 -10
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +7 -6
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +9 -8
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/pie.js +7 -6
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js +7 -6
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/spread.js +57 -56
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/table.js +67 -66
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +23 -22
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/wrapper.js +21 -20
- 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/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/index.ts +3 -0
|
@@ -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,107 @@
|
|
|
1
|
+
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
import { keyframes } from '@mui/material/styles'
|
|
3
|
+
import { CHAT_MAX_WIDTH } from '../const'
|
|
4
|
+
|
|
5
|
+
const scrollBorders = keyframes`
|
|
6
|
+
0% { border-top-color: transparent; border-bottom-color: var(--chat-border-color) }
|
|
7
|
+
0.1% { border-top-color: var(--chat-border-color); border-bottom-color: var(--chat-border-color) }
|
|
8
|
+
99.9% { border-top-color: var(--chat-border-color); border-bottom-color: var(--chat-border-color) }
|
|
9
|
+
100% { border-top-color: var(--chat-border-color); border-bottom-color: transparent }
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
const showFab = keyframes`
|
|
13
|
+
0% { opacity: 1; pointer-events: auto }
|
|
14
|
+
95% { opacity: 1; pointer-events: auto }
|
|
15
|
+
100% { opacity: 0; pointer-events: none }
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
export const styles = {
|
|
19
|
+
header: {
|
|
20
|
+
display: 'flex',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'space-between',
|
|
23
|
+
padding: ({ spacing }) => spacing(1),
|
|
24
|
+
},
|
|
25
|
+
headerTitle: {
|
|
26
|
+
flexGrow: 1,
|
|
27
|
+
px: ({ spacing }) => spacing(0.5),
|
|
28
|
+
},
|
|
29
|
+
headerActions: {
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
marginLeft: ({ spacing }) => spacing(2),
|
|
33
|
+
gap: ({ spacing }) => spacing(0.5),
|
|
34
|
+
},
|
|
35
|
+
footerWrapper: {
|
|
36
|
+
padding: ({ spacing }) => spacing(1),
|
|
37
|
+
position: 'relative',
|
|
38
|
+
},
|
|
39
|
+
footerCorner: {
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
bottom: '38px',
|
|
42
|
+
right: `max(16px, calc(50% - ${CHAT_MAX_WIDTH / 2}px + 6px))`,
|
|
43
|
+
margin: '0 auto',
|
|
44
|
+
},
|
|
45
|
+
footer: {
|
|
46
|
+
maxWidth: CHAT_MAX_WIDTH,
|
|
47
|
+
margin: '0 auto',
|
|
48
|
+
},
|
|
49
|
+
footerCaption: {
|
|
50
|
+
textAlign: 'center',
|
|
51
|
+
},
|
|
52
|
+
content: {
|
|
53
|
+
overflowY: 'auto',
|
|
54
|
+
flex: 1,
|
|
55
|
+
position: 'relative',
|
|
56
|
+
mx: 'auto',
|
|
57
|
+
maxWidth: CHAT_MAX_WIDTH,
|
|
58
|
+
width: '100%',
|
|
59
|
+
maxHeight: '100%',
|
|
60
|
+
pt: 1,
|
|
61
|
+
pb: 2,
|
|
62
|
+
px: 2,
|
|
63
|
+
display: 'flex',
|
|
64
|
+
flexDirection: 'column',
|
|
65
|
+
'--chat-border-color': ({ palette }) => palette.divider,
|
|
66
|
+
borderTop: '1px solid transparent',
|
|
67
|
+
borderBottom: '1px solid transparent',
|
|
68
|
+
'@supports (animation-timeline: scroll())': {
|
|
69
|
+
animation: `${scrollBorders} linear both`,
|
|
70
|
+
animationTimeline: 'scroll(self)',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
jumpToLatestWrapper: {
|
|
74
|
+
position: 'sticky',
|
|
75
|
+
bottom: ({ spacing }) => spacing(3),
|
|
76
|
+
height: 0,
|
|
77
|
+
display: 'flex',
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
zIndex: 1,
|
|
80
|
+
},
|
|
81
|
+
jumpToLatest: {
|
|
82
|
+
opacity: 0,
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
'@supports (animation-timeline: scroll())': {
|
|
85
|
+
animation: `${showFab} linear both`,
|
|
86
|
+
animationTimeline: 'scroll(nearest)',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
starter: {
|
|
90
|
+
display: 'flex',
|
|
91
|
+
flexDirection: 'column',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
padding: ({ spacing }) => spacing(3),
|
|
95
|
+
textAlign: 'center',
|
|
96
|
+
maxWidth: CHAT_MAX_WIDTH,
|
|
97
|
+
margin: '0 auto',
|
|
98
|
+
},
|
|
99
|
+
starterItems: {
|
|
100
|
+
display: 'grid',
|
|
101
|
+
width: '100%',
|
|
102
|
+
marginTop: ({ spacing }) => spacing(2),
|
|
103
|
+
},
|
|
104
|
+
starterItemsTwoCol: {
|
|
105
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
106
|
+
},
|
|
107
|
+
} 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,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 { ChatTools } from './chat-tools'
|
|
4
|
+
|
|
5
|
+
describe('ChatTools', () => {
|
|
6
|
+
const defaultTools = [
|
|
7
|
+
{ id: '1', name: 'Search', status: 'complete' as const },
|
|
8
|
+
{ id: '2', name: 'Analyze', status: 'loading' as const },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
test('renders tool names', () => {
|
|
12
|
+
render(<ChatTools tools={defaultTools} />)
|
|
13
|
+
expect(screen.getByText('Search')).toBeTruthy()
|
|
14
|
+
expect(screen.getByText('Analyze')).toBeTruthy()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('renders title label when provided', () => {
|
|
18
|
+
render(
|
|
19
|
+
<ChatTools tools={defaultTools} labels={{ title: 'Tool Activity' }} />,
|
|
20
|
+
)
|
|
21
|
+
expect(screen.getByText('Tool Activity')).toBeTruthy()
|
|
22
|
+
})
|
|
23
|
+
})
|