@botonic/react 0.54.0 → 0.55.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/lib/cjs/components/index-types.d.ts +0 -2
- package/lib/cjs/components/message/index.js +2 -4
- package/lib/cjs/components/message/index.js.map +1 -1
- package/lib/cjs/components/message/message-footer.d.ts +1 -4
- package/lib/cjs/components/message/message-footer.js +2 -3
- package/lib/cjs/components/message/message-footer.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/debug-message.js +0 -2
- package/lib/cjs/components/system-debug-trace/debug-message.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.d.ts +1 -1
- package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.js +6 -2
- package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.d.ts +6 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js +55 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js.map +1 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/types.d.ts +2 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.d.ts +8 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js +41 -0
- package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js.map +1 -0
- package/lib/cjs/components/system-debug-trace/events/constants.d.ts +0 -2
- package/lib/cjs/components/system-debug-trace/events/constants.js +0 -2
- package/lib/cjs/components/system-debug-trace/events/constants.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/events/index.d.ts +0 -1
- package/lib/cjs/components/system-debug-trace/events/index.js +0 -1
- package/lib/cjs/components/system-debug-trace/events/index.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.d.ts +1 -4
- package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.js +1 -23
- package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.js.map +1 -1
- package/lib/cjs/components/system-debug-trace/types.d.ts +2 -2
- package/lib/cjs/components/text.js +0 -2
- package/lib/cjs/components/text.js.map +1 -1
- package/lib/cjs/debug-system-messages.d.ts +0 -22
- package/lib/cjs/debug-system-messages.js +0 -22
- package/lib/cjs/debug-system-messages.js.map +1 -1
- package/lib/cjs/dev-app.d.ts +0 -22
- package/lib/cjs/index-types.d.ts +5 -1
- package/lib/cjs/index-types.js.map +1 -1
- package/lib/cjs/webchat/input-panel/index.js +7 -17
- package/lib/cjs/webchat/input-panel/index.js.map +1 -1
- package/lib/cjs/webchat/input-panel/textarea.d.ts +3 -2
- package/lib/cjs/webchat/input-panel/textarea.js +16 -46
- package/lib/cjs/webchat/input-panel/textarea.js.map +1 -1
- package/lib/cjs/webchat/input-panel/typing-network-sender.d.ts +52 -0
- package/lib/cjs/webchat/input-panel/typing-network-sender.js +122 -0
- package/lib/cjs/webchat/input-panel/typing-network-sender.js.map +1 -0
- package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.d.ts +16 -0
- package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.js +45 -0
- package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.js.map +1 -0
- package/lib/cjs/webchat/input-panel/use-typing-session.d.ts +15 -0
- package/lib/cjs/webchat/input-panel/use-typing-session.js +98 -0
- package/lib/cjs/webchat/input-panel/use-typing-session.js.map +1 -0
- package/lib/cjs/webchat/tracking.d.ts +0 -10
- package/lib/cjs/webchat/tracking.js +1 -32
- package/lib/cjs/webchat/tracking.js.map +1 -1
- package/lib/esm/components/index-types.d.ts +0 -2
- package/lib/esm/components/message/index.js +2 -4
- package/lib/esm/components/message/index.js.map +1 -1
- package/lib/esm/components/message/message-footer.d.ts +1 -4
- package/lib/esm/components/message/message-footer.js +2 -3
- package/lib/esm/components/message/message-footer.js.map +1 -1
- package/lib/esm/components/system-debug-trace/debug-message.js +0 -2
- package/lib/esm/components/system-debug-trace/debug-message.js.map +1 -1
- package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.d.ts +1 -1
- package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.js +6 -2
- package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.js.map +1 -1
- package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.d.ts +6 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js +55 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js.map +1 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/types.d.ts +2 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.d.ts +8 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js +41 -0
- package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js.map +1 -0
- package/lib/esm/components/system-debug-trace/events/constants.d.ts +0 -2
- package/lib/esm/components/system-debug-trace/events/constants.js +0 -2
- package/lib/esm/components/system-debug-trace/events/constants.js.map +1 -1
- package/lib/esm/components/system-debug-trace/events/index.d.ts +0 -1
- package/lib/esm/components/system-debug-trace/events/index.js +0 -1
- package/lib/esm/components/system-debug-trace/events/index.js.map +1 -1
- package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.d.ts +1 -4
- package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.js +1 -23
- package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.js.map +1 -1
- package/lib/esm/components/system-debug-trace/types.d.ts +2 -2
- package/lib/esm/components/text.js +0 -2
- package/lib/esm/components/text.js.map +1 -1
- package/lib/esm/debug-system-messages.d.ts +0 -22
- package/lib/esm/debug-system-messages.js +0 -22
- package/lib/esm/debug-system-messages.js.map +1 -1
- package/lib/esm/dev-app.d.ts +0 -22
- package/lib/esm/index-types.d.ts +5 -1
- package/lib/esm/index-types.js.map +1 -1
- package/lib/esm/webchat/input-panel/index.js +7 -17
- package/lib/esm/webchat/input-panel/index.js.map +1 -1
- package/lib/esm/webchat/input-panel/textarea.d.ts +3 -2
- package/lib/esm/webchat/input-panel/textarea.js +16 -46
- package/lib/esm/webchat/input-panel/textarea.js.map +1 -1
- package/lib/esm/webchat/input-panel/typing-network-sender.d.ts +52 -0
- package/lib/esm/webchat/input-panel/typing-network-sender.js +122 -0
- package/lib/esm/webchat/input-panel/typing-network-sender.js.map +1 -0
- package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.d.ts +16 -0
- package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.js +45 -0
- package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.js.map +1 -0
- package/lib/esm/webchat/input-panel/use-typing-session.d.ts +15 -0
- package/lib/esm/webchat/input-panel/use-typing-session.js +98 -0
- package/lib/esm/webchat/input-panel/use-typing-session.js.map +1 -0
- package/lib/esm/webchat/tracking.d.ts +0 -10
- package/lib/esm/webchat/tracking.js +1 -32
- package/lib/esm/webchat/tracking.js.map +1 -1
- package/package.json +2 -2
- package/src/components/index-types.ts +0 -2
- package/src/components/message/index.jsx +1 -8
- package/src/components/message/message-footer.tsx +0 -14
- package/src/components/system-debug-trace/debug-message.tsx +0 -3
- package/src/components/system-debug-trace/events/ai-agent/ai-agent.tsx +22 -10
- package/src/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.ts +70 -0
- package/src/components/system-debug-trace/events/ai-agent/types.ts +2 -0
- package/src/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.ts +67 -0
- package/src/components/system-debug-trace/events/constants.ts +0 -2
- package/src/components/system-debug-trace/events/index.ts +0 -1
- package/src/components/system-debug-trace/hooks/use-knowledge-base-info.tsx +1 -27
- package/src/components/system-debug-trace/types.ts +0 -2
- package/src/components/text.tsx +0 -2
- package/src/debug-system-messages.ts +0 -25
- package/src/index-types.ts +9 -1
- package/src/webchat/input-panel/index.tsx +7 -17
- package/src/webchat/input-panel/textarea.tsx +20 -60
- package/src/webchat/input-panel/typing-network-sender.ts +168 -0
- package/src/webchat/input-panel/use-typing-chat-event-sender.ts +67 -0
- package/src/webchat/input-panel/use-typing-session.ts +124 -0
- package/src/webchat/tracking.ts +2 -56
- package/lib/cjs/components/message/message-feedback.d.ts +0 -7
- package/lib/cjs/components/message/message-feedback.js +0 -55
- package/lib/cjs/components/message/message-feedback.js.map +0 -1
- package/lib/cjs/components/system-debug-trace/events/knowledge-base.d.ts +0 -17
- package/lib/cjs/components/system-debug-trace/events/knowledge-base.js +0 -42
- package/lib/cjs/components/system-debug-trace/events/knowledge-base.js.map +0 -1
- package/lib/esm/components/message/message-feedback.d.ts +0 -7
- package/lib/esm/components/message/message-feedback.js +0 -55
- package/lib/esm/components/message/message-feedback.js.map +0 -1
- package/lib/esm/components/system-debug-trace/events/knowledge-base.d.ts +0 -17
- package/lib/esm/components/system-debug-trace/events/knowledge-base.js +0 -42
- package/lib/esm/components/system-debug-trace/events/knowledge-base.js.map +0 -1
- package/src/components/message/message-feedback.tsx +0 -90
- package/src/components/system-debug-trace/events/knowledge-base.tsx +0 -115
package/src/index-types.ts
CHANGED
|
@@ -87,12 +87,17 @@ export interface MinimalHubtypeMessage {
|
|
|
87
87
|
type: string
|
|
88
88
|
action: string
|
|
89
89
|
text: string
|
|
90
|
+
event_data?: Record<string, unknown> | null
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export interface WebchatLocaleContents {
|
|
93
94
|
inputPlaceholder: string
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export interface GetMessageByIdOptions {
|
|
98
|
+
includeDebugEvents?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
96
101
|
export interface PreviewUtils {
|
|
97
102
|
getChunkIdsGroupedBySource: (
|
|
98
103
|
chunkIds: string[]
|
|
@@ -101,7 +106,10 @@ export interface PreviewUtils {
|
|
|
101
106
|
chunkIdsGroupedBySource: ChunkIdsGroupedBySourceData[]
|
|
102
107
|
) => void
|
|
103
108
|
onClickOpenToolResults: (toolExecution: ToolExecution) => void
|
|
104
|
-
getMessageById: (
|
|
109
|
+
getMessageById: (
|
|
110
|
+
messageId: string,
|
|
111
|
+
options?: GetMessageByIdOptions
|
|
112
|
+
) => Promise<MinimalHubtypeMessage>
|
|
105
113
|
trackPreviewEventOpened: (eventProperties: Record<string, unknown>) => void
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { INPUT } from '@botonic/core'
|
|
2
1
|
import type React from 'react'
|
|
3
2
|
import { useContext } from 'react'
|
|
4
|
-
import { v7 as uuidv7 } from 'uuid'
|
|
5
3
|
|
|
6
4
|
import { WEBCHAT } from '../../constants'
|
|
7
5
|
import { getFullMimeWhitelist } from '../../message-utils'
|
|
@@ -14,6 +12,7 @@ import { PersistentMenu } from './persistent-menu'
|
|
|
14
12
|
import { SendButton } from './send-button'
|
|
15
13
|
import { UserInputContainer } from './styles'
|
|
16
14
|
import { Textarea } from './textarea'
|
|
15
|
+
import { useTypingChatEventSender } from './use-typing-chat-event-sender'
|
|
17
16
|
|
|
18
17
|
interface InputPanelProps {
|
|
19
18
|
handleAttachment: (event: any) => void
|
|
@@ -63,21 +62,12 @@ export const InputPanel = ({
|
|
|
63
62
|
textareaRef.current.value = ''
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (onUserInput) {
|
|
73
|
-
onUserInput({
|
|
74
|
-
user: webchatState.session.user,
|
|
75
|
-
input: chatEventInput,
|
|
76
|
-
session: webchatState.session,
|
|
77
|
-
lastRoutePath: webchatState.lastRoutePath,
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
}
|
|
65
|
+
// Network layer: deduplicates typing_on/off before they reach onUserInput.
|
|
66
|
+
const sendChatEvent = useTypingChatEventSender({
|
|
67
|
+
onUserInput,
|
|
68
|
+
session: webchatState.session,
|
|
69
|
+
lastRoutePath: webchatState.lastRoutePath,
|
|
70
|
+
})
|
|
81
71
|
|
|
82
72
|
return (
|
|
83
73
|
<UserInputContainer
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
1
|
import type React from 'react'
|
|
3
2
|
import { useContext } from 'react'
|
|
4
3
|
import TextareaAutosize from 'react-textarea-autosize'
|
|
5
4
|
|
|
6
|
-
import { Typing } from '../../index-types'
|
|
7
5
|
import { WebchatContext } from '../../webchat/context'
|
|
8
6
|
import { useDeviceAdapter } from '../hooks'
|
|
9
7
|
import { TextAreaContainer } from './styles'
|
|
8
|
+
import type { TypingChatEvent } from './typing-network-sender'
|
|
9
|
+
import { useTypingSession } from './use-typing-session'
|
|
10
10
|
|
|
11
11
|
interface TextareaProps {
|
|
12
12
|
host: HTMLElement
|
|
13
13
|
textareaRef: React.MutableRefObject<HTMLTextAreaElement | undefined>
|
|
14
|
-
sendChatEvent: (event:
|
|
14
|
+
sendChatEvent: (event: TypingChatEvent) => Promise<boolean>
|
|
15
15
|
sendTextAreaText: () => Promise<void>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export { TYPING_IDLE_MS } from './use-typing-session'
|
|
19
|
+
|
|
18
20
|
export const Textarea = ({
|
|
19
21
|
host,
|
|
20
22
|
textareaRef,
|
|
@@ -22,86 +24,44 @@ export const Textarea = ({
|
|
|
22
24
|
sendTextAreaText,
|
|
23
25
|
}: TextareaProps) => {
|
|
24
26
|
const { webchatState, setIsInputFocused } = useContext(WebchatContext)
|
|
27
|
+
// UI layer: decides when the user is typing. Network dedup lives in sendChatEvent.
|
|
28
|
+
const { stopTyping, onTextChange } = useTypingSession(sendChatEvent)
|
|
25
29
|
|
|
26
30
|
useDeviceAdapter(host, webchatState.isWebchatOpen)
|
|
27
31
|
|
|
28
|
-
let isTyping = false
|
|
29
|
-
let typingTimeout: NodeJS.Timeout | null = null
|
|
30
|
-
|
|
31
32
|
const persistentMenuOptions = webchatState.theme.userInput?.persistentMenu
|
|
32
|
-
|
|
33
33
|
const placeholder = webchatState.theme.userInput?.box?.placeholder
|
|
34
|
-
|
|
35
34
|
const userInputBoxStyle = webchatState.theme.userInput?.box?.style
|
|
36
35
|
|
|
37
|
-
const onKeyDown = event => {
|
|
38
|
-
if (event.
|
|
36
|
+
const onKeyDown = async event => {
|
|
37
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
39
38
|
event.preventDefault()
|
|
40
|
-
sendTextAreaText
|
|
41
|
-
stopTyping()
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const onKeyUp = () => {
|
|
46
|
-
if (!textareaRef.current) {
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (textareaRef.current.value === '') {
|
|
51
|
-
stopTyping()
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
if (!isTyping) {
|
|
55
|
-
startTyping()
|
|
56
|
-
}
|
|
57
|
-
clearTimeoutWithReset(true)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const clearTimeoutWithReset = (reset: boolean) => {
|
|
61
|
-
const waitTime = 20 * 1000
|
|
62
|
-
if (typingTimeout) {
|
|
63
|
-
clearTimeout(typingTimeout)
|
|
64
|
-
}
|
|
65
|
-
if (reset) {
|
|
66
|
-
typingTimeout = setTimeout(stopTyping, waitTime)
|
|
39
|
+
// Run in parallel: stop typing must not wait for sendTextAreaText to finish.
|
|
40
|
+
await Promise.all([stopTyping(), sendTextAreaText()])
|
|
67
41
|
}
|
|
68
42
|
}
|
|
69
43
|
|
|
70
|
-
const startTyping = () => {
|
|
71
|
-
isTyping = true
|
|
72
|
-
sendChatEvent(Typing.On)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const stopTyping = () => {
|
|
76
|
-
clearTimeoutWithReset(false)
|
|
77
|
-
isTyping = false
|
|
78
|
-
sendChatEvent(Typing.Off)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const handleFocus = () => {
|
|
82
|
-
setIsInputFocused(true)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const handleBlur = () => {
|
|
86
|
-
setIsInputFocused(false)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
44
|
return (
|
|
90
45
|
<TextAreaContainer>
|
|
91
46
|
<TextareaAutosize
|
|
92
47
|
ref={(ref: HTMLTextAreaElement) => {
|
|
93
48
|
textareaRef.current = ref
|
|
94
49
|
}}
|
|
95
|
-
onFocus={
|
|
96
|
-
onBlur={
|
|
50
|
+
onFocus={() => setIsInputFocused(true)}
|
|
51
|
+
onBlur={async () => {
|
|
52
|
+
setIsInputFocused(false)
|
|
53
|
+
await stopTyping()
|
|
54
|
+
}}
|
|
55
|
+
onChange={async event => {
|
|
56
|
+
await onTextChange(event.target.value)
|
|
57
|
+
}}
|
|
97
58
|
name='text'
|
|
98
59
|
maxRows={4}
|
|
99
60
|
wrap='soft'
|
|
100
61
|
maxLength={1000}
|
|
101
62
|
placeholder={placeholder}
|
|
102
63
|
autoFocus={false}
|
|
103
|
-
onKeyDown={
|
|
104
|
-
onKeyUp={onKeyUp}
|
|
64
|
+
onKeyDown={onKeyDown}
|
|
105
65
|
style={{
|
|
106
66
|
display: 'flex',
|
|
107
67
|
fontSize: 16,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { Session as CoreSession } from '@botonic/core'
|
|
2
|
+
import { INPUT } from '@botonic/core'
|
|
3
|
+
import { v7 as uuidv7 } from 'uuid'
|
|
4
|
+
|
|
5
|
+
import { Typing } from '../../index-types'
|
|
6
|
+
|
|
7
|
+
/** Network debounce before `typing_off` is sent after a stop signal. */
|
|
8
|
+
export const TYPING_OFF_DEBOUNCE_MS = 2000
|
|
9
|
+
|
|
10
|
+
export type TypingChatEvent = Typing.On | Typing.Off
|
|
11
|
+
|
|
12
|
+
/** `true` when `typing_on` was delivered to the server. */
|
|
13
|
+
export type TypingOnSendResult = boolean
|
|
14
|
+
|
|
15
|
+
/** `true` when `typing_off` was scheduled; delivery happens after debounce. */
|
|
16
|
+
export type TypingOffSendResult = boolean
|
|
17
|
+
|
|
18
|
+
export interface ChatEventContext {
|
|
19
|
+
session: Partial<CoreSession>
|
|
20
|
+
lastRoutePath?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SendToServer = (
|
|
24
|
+
chatEvent: TypingChatEvent,
|
|
25
|
+
context: ChatEventContext
|
|
26
|
+
) => Promise<boolean>
|
|
27
|
+
|
|
28
|
+
export function buildChatEventPayload(
|
|
29
|
+
chatEvent: TypingChatEvent,
|
|
30
|
+
{ session, lastRoutePath }: ChatEventContext
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
user: session.user,
|
|
34
|
+
input: {
|
|
35
|
+
id: uuidv7(),
|
|
36
|
+
type: INPUT.CHAT_EVENT,
|
|
37
|
+
data: chatEvent,
|
|
38
|
+
},
|
|
39
|
+
session,
|
|
40
|
+
lastRoutePath,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Network-layer sender for typing_on / typing_off.
|
|
46
|
+
*
|
|
47
|
+
* Guarantees at most one typing_on per session, debounced typing_off,
|
|
48
|
+
* and matching session context between on/off pairs.
|
|
49
|
+
*/
|
|
50
|
+
export class TypingNetworkSender {
|
|
51
|
+
private disposed = false
|
|
52
|
+
private isTypingOnServer = false
|
|
53
|
+
// Snapshot of session/route captured when typing_on was delivered.
|
|
54
|
+
private activeContext: ChatEventContext | null = null
|
|
55
|
+
private pendingOffTimer: ReturnType<typeof setTimeout> | null = null
|
|
56
|
+
private pendingTypingOn: Promise<TypingOnSendResult> | null = null
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly getContext: () => ChatEventContext,
|
|
60
|
+
private readonly sendToServer: SendToServer
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* For `Typing.On`, returns whether the event was delivered.
|
|
65
|
+
* For `Typing.Off`, returns whether the stop was scheduled.
|
|
66
|
+
*/
|
|
67
|
+
async send(
|
|
68
|
+
chatEvent: TypingChatEvent
|
|
69
|
+
): Promise<TypingOnSendResult | TypingOffSendResult> {
|
|
70
|
+
if (chatEvent === Typing.On) {
|
|
71
|
+
return this.userStartedTyping()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.userStoppedTyping()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private sendTypingOffBestEffort(context: ChatEventContext) {
|
|
78
|
+
return this.sendToServer(Typing.Off, context).catch(() => {})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dispose() {
|
|
82
|
+
this.disposed = true
|
|
83
|
+
this.cancelPendingOff()
|
|
84
|
+
|
|
85
|
+
if (!this.isTypingOnServer || !this.activeContext) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unmount should not leave the server thinking the user is still typing.
|
|
90
|
+
const context = this.activeContext
|
|
91
|
+
this.resetTypingState()
|
|
92
|
+
this.sendTypingOffBestEffort(context)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async userStartedTyping(): Promise<TypingOnSendResult> {
|
|
96
|
+
if (this.pendingTypingOn) {
|
|
97
|
+
return this.pendingTypingOn
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// User resumed typing before the debounced typing_off was sent.
|
|
101
|
+
this.cancelPendingOff()
|
|
102
|
+
|
|
103
|
+
if (this.isTypingOnServer) {
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.pendingTypingOn = (async () => {
|
|
108
|
+
const context = this.getContext()
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const delivered = await this.sendToServer(Typing.On, context)
|
|
112
|
+
|
|
113
|
+
if (this.disposed) {
|
|
114
|
+
// typing_on resolved after unmount: immediately balance with typing_off.
|
|
115
|
+
if (delivered) {
|
|
116
|
+
this.sendTypingOffBestEffort(context)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (delivered) {
|
|
123
|
+
this.isTypingOnServer = true
|
|
124
|
+
this.activeContext = context
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return delivered
|
|
128
|
+
} finally {
|
|
129
|
+
this.pendingTypingOn = null
|
|
130
|
+
}
|
|
131
|
+
})()
|
|
132
|
+
|
|
133
|
+
return this.pendingTypingOn
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private userStoppedTyping(): Promise<TypingOffSendResult> {
|
|
137
|
+
if (!this.isTypingOnServer || !this.activeContext) {
|
|
138
|
+
return Promise.resolve(false)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.cancelPendingOff()
|
|
142
|
+
|
|
143
|
+
const context = this.activeContext
|
|
144
|
+
|
|
145
|
+
// Coalesce blur + clear + send into a single typing_off.
|
|
146
|
+
this.pendingOffTimer = setTimeout(() => {
|
|
147
|
+
this.pendingOffTimer = null
|
|
148
|
+
this.resetTypingState()
|
|
149
|
+
this.sendTypingOffBestEffort(context)
|
|
150
|
+
}, TYPING_OFF_DEBOUNCE_MS)
|
|
151
|
+
|
|
152
|
+
return Promise.resolve(true)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private resetTypingState() {
|
|
156
|
+
this.isTypingOnServer = false
|
|
157
|
+
this.activeContext = null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private cancelPendingOff() {
|
|
161
|
+
if (!this.pendingOffTimer) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
clearTimeout(this.pendingOffTimer)
|
|
166
|
+
this.pendingOffTimer = null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Session as CoreSession } from '@botonic/core'
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
buildChatEventPayload,
|
|
5
|
+
type TypingChatEvent,
|
|
6
|
+
TypingNetworkSender,
|
|
7
|
+
} from './typing-network-sender'
|
|
8
|
+
|
|
9
|
+
export type TypingChatEventPayload = ReturnType<typeof buildChatEventPayload>
|
|
10
|
+
|
|
11
|
+
interface UseTypingChatEventSenderArgs {
|
|
12
|
+
onUserInput?: (args: TypingChatEventPayload) => Promise<void>
|
|
13
|
+
session: Partial<CoreSession>
|
|
14
|
+
lastRoutePath?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* React bridge from Textarea to TypingNetworkSender.
|
|
19
|
+
*
|
|
20
|
+
* Keeps one sender instance per input panel and forwards typing events
|
|
21
|
+
* to onUserInput with the current session context.
|
|
22
|
+
*/
|
|
23
|
+
export function useTypingChatEventSender({
|
|
24
|
+
onUserInput,
|
|
25
|
+
session,
|
|
26
|
+
lastRoutePath,
|
|
27
|
+
}: UseTypingChatEventSenderArgs) {
|
|
28
|
+
// Always read the latest handler/session without recreating the sender.
|
|
29
|
+
const contextRef = useRef({ onUserInput, session, lastRoutePath })
|
|
30
|
+
const typingSenderRef = useRef<TypingNetworkSender | null>(null)
|
|
31
|
+
|
|
32
|
+
contextRef.current = { onUserInput, session, lastRoutePath }
|
|
33
|
+
|
|
34
|
+
if (!typingSenderRef.current) {
|
|
35
|
+
typingSenderRef.current = new TypingNetworkSender(
|
|
36
|
+
() => ({
|
|
37
|
+
session: contextRef.current.session,
|
|
38
|
+
lastRoutePath: contextRef.current.lastRoutePath,
|
|
39
|
+
}),
|
|
40
|
+
async (chatEvent, context) => {
|
|
41
|
+
const handler = contextRef.current.onUserInput
|
|
42
|
+
|
|
43
|
+
if (!handler) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await handler(buildChatEventPayload(chatEvent, context))
|
|
49
|
+
return true
|
|
50
|
+
} catch {
|
|
51
|
+
// Let the UI retry on the next keystroke.
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
return () => {
|
|
60
|
+
typingSenderRef.current?.dispose()
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
return useCallback(async (chatEvent: TypingChatEvent): Promise<boolean> => {
|
|
65
|
+
return (await typingSenderRef.current?.send(chatEvent)) ?? false
|
|
66
|
+
}, [])
|
|
67
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Typing } from '../../index-types'
|
|
4
|
+
import type { TypingChatEvent } from './typing-network-sender'
|
|
5
|
+
|
|
6
|
+
/** Local UI inactivity before the textarea stops emitting typing events. */
|
|
7
|
+
export const TYPING_IDLE_MS = 10 * 1000
|
|
8
|
+
|
|
9
|
+
type SendTypingEvent = (event: TypingChatEvent) => Promise<boolean>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* UI-layer typing session for the textarea.
|
|
13
|
+
*
|
|
14
|
+
* Decides *when* the user is typing and requests typing_on / typing_off.
|
|
15
|
+
* Deduplication and network debouncing live in TypingNetworkSender instead.
|
|
16
|
+
*/
|
|
17
|
+
export function useTypingSession(sendChatEvent: SendTypingEvent) {
|
|
18
|
+
const isTyping = useRef(false)
|
|
19
|
+
// Bumps on each start/stop so stale async stopTyping() calls can be ignored.
|
|
20
|
+
const startOrStopTypingCount = useRef(0)
|
|
21
|
+
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
22
|
+
// Shares one in-flight typing_on while delivery is still pending.
|
|
23
|
+
const pendingTypingOn = useRef<Promise<boolean> | null>(null)
|
|
24
|
+
|
|
25
|
+
const stopTyping = useCallback(async () => {
|
|
26
|
+
const countWhenStopWasRequested = ++startOrStopTypingCount.current
|
|
27
|
+
|
|
28
|
+
if (idleTimer.current) {
|
|
29
|
+
clearTimeout(idleTimer.current)
|
|
30
|
+
idleTimer.current = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Wait for typing_on to settle before deciding whether to send typing_off.
|
|
34
|
+
if (pendingTypingOn.current) {
|
|
35
|
+
await pendingTypingOn.current
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Let same-turn input events (e.g. Enter then type next message) bump the counter first.
|
|
39
|
+
await Promise.resolve()
|
|
40
|
+
|
|
41
|
+
if (countWhenStopWasRequested !== startOrStopTypingCount.current) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!isTyping.current) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isTyping.current = false
|
|
50
|
+
await sendChatEvent(Typing.Off)
|
|
51
|
+
}, [sendChatEvent])
|
|
52
|
+
|
|
53
|
+
const startTyping = useCallback(async () => {
|
|
54
|
+
if (isTyping.current) {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (pendingTypingOn.current) {
|
|
59
|
+
return pendingTypingOn.current
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pendingTypingOn.current = (async () => {
|
|
63
|
+
try {
|
|
64
|
+
const accepted = await sendChatEvent(Typing.On)
|
|
65
|
+
|
|
66
|
+
// Only mark active after the network layer confirms delivery.
|
|
67
|
+
if (accepted) {
|
|
68
|
+
isTyping.current = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return accepted
|
|
72
|
+
} finally {
|
|
73
|
+
pendingTypingOn.current = null
|
|
74
|
+
}
|
|
75
|
+
})()
|
|
76
|
+
|
|
77
|
+
return pendingTypingOn.current
|
|
78
|
+
}, [sendChatEvent])
|
|
79
|
+
|
|
80
|
+
const onTextChange = useCallback(
|
|
81
|
+
async (value: string) => {
|
|
82
|
+
if (value === '') {
|
|
83
|
+
await stopTyping()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const countWhenUserTyped = ++startOrStopTypingCount.current
|
|
88
|
+
await startTyping()
|
|
89
|
+
|
|
90
|
+
if (countWhenUserTyped !== startOrStopTypingCount.current) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!isTyping.current) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Reset the idle timer on every keystroke.
|
|
99
|
+
if (idleTimer.current) {
|
|
100
|
+
clearTimeout(idleTimer.current)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
idleTimer.current = setTimeout(() => {
|
|
104
|
+
stopTyping().catch(() => {})
|
|
105
|
+
}, TYPING_IDLE_MS)
|
|
106
|
+
},
|
|
107
|
+
[startTyping, stopTyping]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const stopTypingRef = useRef(stopTyping)
|
|
111
|
+
stopTypingRef.current = stopTyping
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
return () => {
|
|
115
|
+
if (idleTimer.current) {
|
|
116
|
+
clearTimeout(idleTimer.current)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stopTypingRef.current().catch(() => {})
|
|
120
|
+
}
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
123
|
+
return { stopTyping, onTextChange }
|
|
124
|
+
}
|
package/src/webchat/tracking.ts
CHANGED
|
@@ -1,36 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EventCustom,
|
|
3
|
-
EventFeedback,
|
|
4
|
-
EventFeedbackKnowledgebase,
|
|
5
|
-
} from '@botonic/core'
|
|
1
|
+
import type { EventCustom, EventFeedback } from '@botonic/core'
|
|
6
2
|
import { useContext } from 'react'
|
|
7
|
-
import { v7 as uuidv7 } from 'uuid'
|
|
8
3
|
|
|
9
4
|
import type { ActionRequest } from '../index-types'
|
|
10
5
|
import { WebchatContext } from './context'
|
|
11
6
|
|
|
12
|
-
export enum EventAction {
|
|
13
|
-
FeedbackKnowledgebase = 'feedback_knowledgebase',
|
|
14
|
-
}
|
|
15
|
-
interface TrackKnowledgebaseFeedbackArgs {
|
|
16
|
-
messageId: string
|
|
17
|
-
isUseful: boolean
|
|
18
|
-
botInteractionId?: string
|
|
19
|
-
inferenceId?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
enum FeedbackOption {
|
|
23
|
-
ThumbsUp = 'thumbsUp',
|
|
24
|
-
ThumbsDown = 'thumbsDown',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
7
|
interface UseTracking {
|
|
28
|
-
trackKnowledgebaseFeedback: ({
|
|
29
|
-
messageId,
|
|
30
|
-
isUseful,
|
|
31
|
-
botInteractionId,
|
|
32
|
-
inferenceId,
|
|
33
|
-
}: TrackKnowledgebaseFeedbackArgs) => Promise<void>
|
|
34
8
|
trackCustomEvent: (event: EventCustom) => Promise<void>
|
|
35
9
|
trackFeedbackEvent: (event: EventFeedback) => Promise<void>
|
|
36
10
|
}
|
|
@@ -53,34 +27,6 @@ export function useTracking(): UseTracking {
|
|
|
53
27
|
return request
|
|
54
28
|
}
|
|
55
29
|
|
|
56
|
-
const trackKnowledgebaseFeedback = async ({
|
|
57
|
-
messageId,
|
|
58
|
-
isUseful,
|
|
59
|
-
botInteractionId,
|
|
60
|
-
inferenceId,
|
|
61
|
-
}: TrackKnowledgebaseFeedbackArgs) => {
|
|
62
|
-
if (!trackEvent) {
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
const request = getRequest()
|
|
66
|
-
|
|
67
|
-
// inferenceId and botInteractionId are strings, but in local development they are undefined
|
|
68
|
-
const event: EventFeedbackKnowledgebase = {
|
|
69
|
-
action: EventAction.FeedbackKnowledgebase,
|
|
70
|
-
knowledgebaseInferenceId: inferenceId as string,
|
|
71
|
-
feedbackBotInteractionId: botInteractionId as string,
|
|
72
|
-
feedbackTargetId: messageId,
|
|
73
|
-
feedbackGroupId: uuidv7(),
|
|
74
|
-
possibleOptions: [FeedbackOption.ThumbsDown, FeedbackOption.ThumbsUp],
|
|
75
|
-
possibleValues: [0, 1],
|
|
76
|
-
option: isUseful ? FeedbackOption.ThumbsUp : FeedbackOption.ThumbsDown,
|
|
77
|
-
value: isUseful ? 1 : 0,
|
|
78
|
-
}
|
|
79
|
-
const { action, ...eventArgs } = event
|
|
80
|
-
|
|
81
|
-
await trackEvent(request, action, eventArgs)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
30
|
const trackCustomEvent = async (event: EventCustom) => {
|
|
85
31
|
if (!trackEvent) {
|
|
86
32
|
return
|
|
@@ -101,5 +47,5 @@ export function useTracking(): UseTracking {
|
|
|
101
47
|
await trackEvent(request, action, eventArgs)
|
|
102
48
|
}
|
|
103
49
|
|
|
104
|
-
return {
|
|
50
|
+
return { trackCustomEvent, trackFeedbackEvent }
|
|
105
51
|
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MessageFeedback = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
-
const react_1 = require("react");
|
|
7
|
-
const thumbs_down_svg_1 = tslib_1.__importDefault(require("../../assets/thumbs-down.svg"));
|
|
8
|
-
const thumbs_up_svg_1 = tslib_1.__importDefault(require("../../assets/thumbs-up.svg"));
|
|
9
|
-
const util_1 = require("../../util");
|
|
10
|
-
const context_1 = require("../../webchat/context");
|
|
11
|
-
const tracking_1 = require("../../webchat/tracking");
|
|
12
|
-
const styles_1 = require("./styles");
|
|
13
|
-
const MessageFeedback = ({ botInteractionId, inferenceId, messageId, }) => {
|
|
14
|
-
const { webchatState, updateMessage } = (0, react_1.useContext)(context_1.WebchatContext);
|
|
15
|
-
const { trackKnowledgebaseFeedback } = (0, tracking_1.useTracking)();
|
|
16
|
-
const [className, setClassName] = (0, react_1.useState)('');
|
|
17
|
-
const [disabled, setDisabled] = (0, react_1.useState)({
|
|
18
|
-
positive: false,
|
|
19
|
-
negative: false,
|
|
20
|
-
});
|
|
21
|
-
const updateMsgWithFeedback = (feedbackEnabled) => {
|
|
22
|
-
const message = webchatState.messagesJSON.find(message => message.id === messageId);
|
|
23
|
-
const updatedMsg = {
|
|
24
|
-
...message,
|
|
25
|
-
feedbackEnabled,
|
|
26
|
-
};
|
|
27
|
-
updateMessage(updatedMsg);
|
|
28
|
-
};
|
|
29
|
-
(0, react_1.useEffect)(() => {
|
|
30
|
-
updateMsgWithFeedback(true);
|
|
31
|
-
}, []);
|
|
32
|
-
(0, react_1.useEffect)(() => {
|
|
33
|
-
if (disabled.positive || disabled.negative) {
|
|
34
|
-
setClassName('clicked');
|
|
35
|
-
updateMsgWithFeedback(false);
|
|
36
|
-
}
|
|
37
|
-
}, [disabled]);
|
|
38
|
-
const handleClick = async (isUseful) => {
|
|
39
|
-
if (isUseful) {
|
|
40
|
-
setDisabled({ positive: false, negative: true });
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
setDisabled({ positive: true, negative: false });
|
|
44
|
-
}
|
|
45
|
-
await trackKnowledgebaseFeedback({
|
|
46
|
-
messageId,
|
|
47
|
-
isUseful,
|
|
48
|
-
botInteractionId,
|
|
49
|
-
inferenceId,
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
return ((0, jsx_runtime_1.jsxs)(styles_1.FeedbackMessageContainer, { children: [(0, jsx_runtime_1.jsx)(styles_1.FeedbackButton, { className: className, disabled: disabled.positive, onClick: () => handleClick(true), children: (0, jsx_runtime_1.jsx)("img", { src: (0, util_1.resolveImage)(thumbs_up_svg_1.default) }) }), (0, jsx_runtime_1.jsx)(styles_1.FeedbackButton, { className: className, disabled: disabled.negative, onClick: () => handleClick(false), children: (0, jsx_runtime_1.jsx)("img", { src: (0, util_1.resolveImage)(thumbs_down_svg_1.default) }) })] }));
|
|
53
|
-
};
|
|
54
|
-
exports.MessageFeedback = MessageFeedback;
|
|
55
|
-
//# sourceMappingURL=message-feedback.js.map
|