@carto/ps-react-ui 4.4.0-chat-ui.2 → 4.4.0-chat-ui.4
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 +369 -292
- package/dist/chat.js.map +1 -1
- package/dist/types/chat/containers/styles.d.ts +6 -0
- package/dist/types/chat/index.d.ts +1 -0
- package/dist/types/chat/use-typewriter.d.ts +30 -0
- package/package.json +1 -1
- package/src/chat/bubbles/styles.ts +42 -0
- package/src/chat/containers/styles.ts +6 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/use-typewriter.test.tsx +38 -0
- package/src/chat/use-typewriter.ts +82 -0
|
@@ -21,6 +21,48 @@ export const styles = {
|
|
|
21
21
|
'& + .PsChat--user-message': {
|
|
22
22
|
marginTop: theme.spacing(3),
|
|
23
23
|
},
|
|
24
|
+
'& > p:first-of-type': {
|
|
25
|
+
marginTop: 0,
|
|
26
|
+
},
|
|
27
|
+
'& table': {
|
|
28
|
+
alignSelf: 'stretch',
|
|
29
|
+
display: 'block',
|
|
30
|
+
overflowX: 'auto',
|
|
31
|
+
maxWidth: '100%',
|
|
32
|
+
borderCollapse: 'collapse',
|
|
33
|
+
margin: theme.spacing(1, 0),
|
|
34
|
+
fontSize: '0.875em',
|
|
35
|
+
},
|
|
36
|
+
'& th, & td': {
|
|
37
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
38
|
+
padding: theme.spacing(0.5, 1),
|
|
39
|
+
textAlign: 'left',
|
|
40
|
+
},
|
|
41
|
+
'& th': {
|
|
42
|
+
backgroundColor: theme.palette.action.hover,
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
},
|
|
45
|
+
'& tr:nth-of-type(even) td': {
|
|
46
|
+
backgroundColor: theme.palette.action.hover,
|
|
47
|
+
},
|
|
48
|
+
'& ul.contains-task-list': {
|
|
49
|
+
listStyle: 'none',
|
|
50
|
+
paddingLeft: 0,
|
|
51
|
+
},
|
|
52
|
+
'& .task-list-item input[type="checkbox"]': {
|
|
53
|
+
marginRight: theme.spacing(0.5),
|
|
54
|
+
},
|
|
55
|
+
'& del': {
|
|
56
|
+
textDecoration: 'line-through',
|
|
57
|
+
opacity: 0.7,
|
|
58
|
+
},
|
|
59
|
+
'& > ul, & > ol': {
|
|
60
|
+
margin: theme.spacing(1, 0),
|
|
61
|
+
paddingLeft: theme.spacing(3),
|
|
62
|
+
'& > li + li': {
|
|
63
|
+
marginTop: theme.spacing(1),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
24
66
|
}),
|
|
25
67
|
userMessageContainer: {
|
|
26
68
|
width: '100%',
|
|
@@ -45,6 +45,12 @@ export const styles = {
|
|
|
45
45
|
footer: {
|
|
46
46
|
maxWidth: CHAT_MAX_WIDTH,
|
|
47
47
|
margin: '0 auto',
|
|
48
|
+
'& textarea': {
|
|
49
|
+
resize: 'none',
|
|
50
|
+
maxHeight: '10em',
|
|
51
|
+
overflowY: 'auto !important',
|
|
52
|
+
paddingRight: ({ spacing }) => spacing(5),
|
|
53
|
+
},
|
|
48
54
|
},
|
|
49
55
|
footerCaption: {
|
|
50
56
|
textAlign: 'center',
|
package/src/chat/index.ts
CHANGED
|
@@ -30,6 +30,9 @@ export {
|
|
|
30
30
|
CHAT_TOOL_CODE_AREA_MAX_HEIGHT,
|
|
31
31
|
} from './const'
|
|
32
32
|
|
|
33
|
+
// Hooks
|
|
34
|
+
export { useTypewriter } from './use-typewriter'
|
|
35
|
+
|
|
33
36
|
// Messages
|
|
34
37
|
export { ChatUserMessage } from './bubbles/chat-user-message'
|
|
35
38
|
export { ChatAgentMessage } from './bubbles/chat-agent-message'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { renderHook } from '@testing-library/react'
|
|
3
|
+
import { useTypewriter } from './use-typewriter'
|
|
4
|
+
|
|
5
|
+
describe('useTypewriter', () => {
|
|
6
|
+
test('starts with empty text and isTyping=true when fullText is non-empty', () => {
|
|
7
|
+
const { result } = renderHook(() => useTypewriter('hello'))
|
|
8
|
+
expect(result.current.displayedText).toBe('')
|
|
9
|
+
expect(result.current.isTyping).toBe(true)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('returns the full text immediately when skipAnimation is true', () => {
|
|
13
|
+
const { result } = renderHook(() =>
|
|
14
|
+
useTypewriter('hello', { skipAnimation: true }),
|
|
15
|
+
)
|
|
16
|
+
expect(result.current.displayedText).toBe('hello')
|
|
17
|
+
expect(result.current.isTyping).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('isTyping=false for empty fullText', () => {
|
|
21
|
+
const { result } = renderHook(() => useTypewriter(''))
|
|
22
|
+
expect(result.current.displayedText).toBe('')
|
|
23
|
+
expect(result.current.isTyping).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('skipAnimation captured at mount — toggling later does not retrigger reveal', () => {
|
|
27
|
+
const { result, rerender } = renderHook(
|
|
28
|
+
({ skip }: { skip: boolean }) =>
|
|
29
|
+
useTypewriter('hello', { skipAnimation: skip }),
|
|
30
|
+
{ initialProps: { skip: true } },
|
|
31
|
+
)
|
|
32
|
+
expect(result.current.displayedText).toBe('hello')
|
|
33
|
+
|
|
34
|
+
rerender({ skip: false })
|
|
35
|
+
expect(result.current.displayedText).toBe('hello')
|
|
36
|
+
expect(result.current.isTyping).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseTypewriterOptions {
|
|
4
|
+
/** Characters revealed per second (default: `500`). */
|
|
5
|
+
speed?: number
|
|
6
|
+
/** When true on mount, skip the animation and reveal the full text immediately. */
|
|
7
|
+
skipAnimation?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseTypewriterResult {
|
|
11
|
+
/** The portion of `fullText` revealed so far. */
|
|
12
|
+
displayedText: string
|
|
13
|
+
/** `true` while characters are still being revealed. */
|
|
14
|
+
isTyping: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reveals a string character-by-character at a steady rate via
|
|
19
|
+
* `requestAnimationFrame`. Useful for smoothing out bursty WebSocket-streamed
|
|
20
|
+
* agent message text — pair it with `ChatAgentMessage`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { displayedText, isTyping } = useTypewriter(message)
|
|
25
|
+
* return (
|
|
26
|
+
* <ChatAgentMessage>
|
|
27
|
+
* <Markdown>{displayedText}</Markdown>
|
|
28
|
+
* {isTyping ? <Cursor /> : null}
|
|
29
|
+
* </ChatAgentMessage>
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useTypewriter(
|
|
34
|
+
fullText: string,
|
|
35
|
+
options: UseTypewriterOptions = {},
|
|
36
|
+
): UseTypewriterResult {
|
|
37
|
+
const { speed = 500, skipAnimation = false } = options
|
|
38
|
+
|
|
39
|
+
const skipRef = useRef(skipAnimation)
|
|
40
|
+
|
|
41
|
+
const [charIndex, setCharIndex] = useState(() =>
|
|
42
|
+
skipRef.current ? fullText.length : 0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (skipRef.current) {
|
|
47
|
+
setCharIndex(fullText.length)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (charIndex >= fullText.length) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const msPerChar = 1000 / speed
|
|
56
|
+
let rafId: number
|
|
57
|
+
let lastTime: number | null = null
|
|
58
|
+
|
|
59
|
+
function tick(timestamp: number) {
|
|
60
|
+
lastTime ??= timestamp
|
|
61
|
+
|
|
62
|
+
const elapsed = timestamp - lastTime
|
|
63
|
+
const charsToAdd = Math.floor(elapsed / msPerChar)
|
|
64
|
+
|
|
65
|
+
if (charsToAdd > 0) {
|
|
66
|
+
lastTime = timestamp
|
|
67
|
+
setCharIndex((prev) => Math.min(prev + charsToAdd, fullText.length))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
rafId = requestAnimationFrame(tick)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rafId = requestAnimationFrame(tick)
|
|
74
|
+
|
|
75
|
+
return () => cancelAnimationFrame(rafId)
|
|
76
|
+
}, [fullText, charIndex, speed])
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
displayedText: fullText.slice(0, charIndex),
|
|
80
|
+
isTyping: charIndex < fullText.length,
|
|
81
|
+
}
|
|
82
|
+
}
|