@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.
Files changed (89) hide show
  1. package/dist/chat.js +798 -0
  2. package/dist/chat.js.map +1 -0
  3. package/dist/components.js +260 -240
  4. package/dist/components.js.map +1 -1
  5. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  6. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  7. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  8. package/dist/types/chat/bubbles/index.d.ts +4 -0
  9. package/dist/types/chat/const.d.ts +3 -0
  10. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  11. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  12. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  13. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  14. package/dist/types/chat/containers/index.d.ts +4 -0
  15. package/dist/types/chat/containers/styles.d.ts +101 -0
  16. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  17. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  18. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  19. package/dist/types/chat/feedback/chat-tools.d.ts +2 -0
  20. package/dist/types/chat/feedback/index.d.ts +5 -0
  21. package/dist/types/chat/feedback/styles.d.ts +65 -0
  22. package/dist/types/chat/index.d.ts +16 -0
  23. package/dist/types/chat/types.d.ts +99 -0
  24. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  25. package/dist/types/components/copy-button/types.d.ts +6 -0
  26. package/dist/types/components/index.d.ts +2 -0
  27. package/dist/widgets/actions.js +22 -21
  28. package/dist/widgets/actions.js.map +1 -1
  29. package/dist/widgets/bar.js +7 -6
  30. package/dist/widgets/bar.js.map +1 -1
  31. package/dist/widgets/category.js +9 -8
  32. package/dist/widgets/category.js.map +1 -1
  33. package/dist/widgets/formula.js +11 -10
  34. package/dist/widgets/formula.js.map +1 -1
  35. package/dist/widgets/histogram.js +7 -6
  36. package/dist/widgets/histogram.js.map +1 -1
  37. package/dist/widgets/markdown.js +9 -8
  38. package/dist/widgets/markdown.js.map +1 -1
  39. package/dist/widgets/pie.js +7 -6
  40. package/dist/widgets/pie.js.map +1 -1
  41. package/dist/widgets/scatterplot.js +7 -6
  42. package/dist/widgets/scatterplot.js.map +1 -1
  43. package/dist/widgets/spread.js +57 -56
  44. package/dist/widgets/spread.js.map +1 -1
  45. package/dist/widgets/table.js +67 -66
  46. package/dist/widgets/table.js.map +1 -1
  47. package/dist/widgets/timeseries.js +23 -22
  48. package/dist/widgets/timeseries.js.map +1 -1
  49. package/dist/widgets/wrapper.js +21 -20
  50. package/dist/widgets/wrapper.js.map +1 -1
  51. package/package.json +7 -3
  52. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  53. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  54. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  55. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  56. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  57. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  58. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  59. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  60. package/src/chat/bubbles/index.ts +4 -0
  61. package/src/chat/bubbles/styles.ts +106 -0
  62. package/src/chat/const.ts +3 -0
  63. package/src/chat/containers/chat-content.test.tsx +15 -0
  64. package/src/chat/containers/chat-content.tsx +32 -0
  65. package/src/chat/containers/chat-footer.test.tsx +34 -0
  66. package/src/chat/containers/chat-footer.tsx +78 -0
  67. package/src/chat/containers/chat-header.test.tsx +28 -0
  68. package/src/chat/containers/chat-header.tsx +29 -0
  69. package/src/chat/containers/chat-starter.test.tsx +32 -0
  70. package/src/chat/containers/chat-starter.tsx +75 -0
  71. package/src/chat/containers/index.ts +4 -0
  72. package/src/chat/containers/styles.ts +107 -0
  73. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  74. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  75. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  76. package/src/chat/feedback/chat-loader.tsx +31 -0
  77. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  78. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  79. package/src/chat/feedback/chat-thinking.tsx +23 -0
  80. package/src/chat/feedback/chat-tools.test.tsx +23 -0
  81. package/src/chat/feedback/chat-tools.tsx +54 -0
  82. package/src/chat/feedback/index.ts +5 -0
  83. package/src/chat/feedback/styles.ts +80 -0
  84. package/src/chat/index.ts +45 -0
  85. package/src/chat/types.ts +124 -0
  86. package/src/components/copy-button/copy-button.test.tsx +41 -0
  87. package/src/components/copy-button/copy-button.tsx +31 -0
  88. package/src/components/copy-button/types.ts +10 -0
  89. 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,4 @@
1
+ export { ChatContent } from './chat-content'
2
+ export { ChatHeader } from './chat-header'
3
+ export { ChatFooter } from './chat-footer'
4
+ export { ChatStarter } from './chat-starter'
@@ -0,0 +1,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,7 @@
1
+ import { Box, styled } from '@mui/material'
2
+
3
+ export const ChatActionsContainer = styled(Box)(({ theme }) => ({
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ gap: theme.spacing(0.5),
7
+ }))
@@ -0,0 +1,10 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render } from '@testing-library/react'
3
+ import { ChatLoader } from './chat-loader'
4
+
5
+ describe('ChatLoader', () => {
6
+ test('renders loading indicator', () => {
7
+ const { container } = render(<ChatLoader />)
8
+ expect(container.querySelector('[role="status"]')).toBeTruthy()
9
+ })
10
+ })
@@ -0,0 +1,31 @@
1
+ import { Box } from '@mui/material'
2
+ import type { ChatLoaderProps } from '../types'
3
+ import { styles } from './styles'
4
+
5
+ export function ChatLoader({ size = 24, labels = {}, sx }: ChatLoaderProps) {
6
+ return (
7
+ <Box
8
+ role='status'
9
+ aria-busy={true}
10
+ aria-label={labels.loading ?? 'Loading'}
11
+ sx={{ ...styles.loader, width: size, height: size, ...sx }}
12
+ >
13
+ <Box
14
+ component='span'
15
+ sx={{
16
+ ...styles.loaderOuterCircle,
17
+ width: size * 0.75,
18
+ height: size * 0.75,
19
+ }}
20
+ />
21
+ <Box
22
+ component='span'
23
+ sx={{
24
+ ...styles.loaderInnerCircle,
25
+ width: size * 0.32,
26
+ height: size * 0.32,
27
+ }}
28
+ />
29
+ </Box>
30
+ )
31
+ }
@@ -0,0 +1,43 @@
1
+ import { IconButton } from '@mui/material'
2
+ import {
3
+ ThumbUpOutlined,
4
+ ThumbDownOutlined,
5
+ ThumbUp,
6
+ ThumbDown,
7
+ } from '@mui/icons-material'
8
+ import type { ChatRatingActionProps } from '../types'
9
+
10
+ export function ChatRatingAction({
11
+ rating,
12
+ onRatingChange,
13
+ labels = {},
14
+ }: ChatRatingActionProps) {
15
+ return (
16
+ <>
17
+ <IconButton
18
+ size='small'
19
+ onClick={() => onRatingChange?.(rating === 'up' ? null : 'up')}
20
+ aria-label={labels.thumbUp ?? 'Thumbs up'}
21
+ color={rating === 'up' ? 'primary' : undefined}
22
+ >
23
+ {rating === 'up' ? (
24
+ <ThumbUp fontSize='small' />
25
+ ) : (
26
+ <ThumbUpOutlined fontSize='small' />
27
+ )}
28
+ </IconButton>
29
+ <IconButton
30
+ size='small'
31
+ onClick={() => onRatingChange?.(rating === 'down' ? null : 'down')}
32
+ aria-label={labels.thumbDown ?? 'Thumbs down'}
33
+ color={rating === 'down' ? 'primary' : undefined}
34
+ >
35
+ {rating === 'down' ? (
36
+ <ThumbDown fontSize='small' />
37
+ ) : (
38
+ <ThumbDownOutlined fontSize='small' />
39
+ )}
40
+ </IconButton>
41
+ </>
42
+ )
43
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ChatThinking } from './chat-thinking'
4
+
5
+ describe('ChatThinking', () => {
6
+ test('renders default thinking text', () => {
7
+ render(<ChatThinking />)
8
+ expect(screen.getByText('Thinking...')).toBeTruthy()
9
+ })
10
+
11
+ test('renders custom text', () => {
12
+ render(<ChatThinking>Processing your request...</ChatThinking>)
13
+ expect(screen.getByText('Processing your request...')).toBeTruthy()
14
+ })
15
+ })
@@ -0,0 +1,23 @@
1
+ import { Typography } from '@mui/material'
2
+ import type { ChatThinkingProps } from '../types'
3
+ import { styles } from './styles'
4
+
5
+ export function ChatThinking({
6
+ children = 'Thinking...',
7
+ duration = 2000,
8
+ sx,
9
+ }: ChatThinkingProps) {
10
+ const durSeconds = `${duration / 1000}s`
11
+ return (
12
+ <Typography
13
+ variant='body2'
14
+ sx={{
15
+ ...styles.thinking,
16
+ animationDuration: durSeconds,
17
+ ...sx,
18
+ }}
19
+ >
20
+ {children}
21
+ </Typography>
22
+ )
23
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { 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
+ })