@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.
Files changed (142) hide show
  1. package/lib/cjs/components/index-types.d.ts +0 -2
  2. package/lib/cjs/components/message/index.js +2 -4
  3. package/lib/cjs/components/message/index.js.map +1 -1
  4. package/lib/cjs/components/message/message-footer.d.ts +1 -4
  5. package/lib/cjs/components/message/message-footer.js +2 -3
  6. package/lib/cjs/components/message/message-footer.js.map +1 -1
  7. package/lib/cjs/components/system-debug-trace/debug-message.js +0 -2
  8. package/lib/cjs/components/system-debug-trace/debug-message.js.map +1 -1
  9. package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.d.ts +1 -1
  10. package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.js +6 -2
  11. package/lib/cjs/components/system-debug-trace/events/ai-agent/ai-agent.js.map +1 -1
  12. package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.d.ts +6 -0
  13. package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js +55 -0
  14. package/lib/cjs/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js.map +1 -0
  15. package/lib/cjs/components/system-debug-trace/events/ai-agent/types.d.ts +2 -0
  16. package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.d.ts +8 -0
  17. package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js +41 -0
  18. package/lib/cjs/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js.map +1 -0
  19. package/lib/cjs/components/system-debug-trace/events/constants.d.ts +0 -2
  20. package/lib/cjs/components/system-debug-trace/events/constants.js +0 -2
  21. package/lib/cjs/components/system-debug-trace/events/constants.js.map +1 -1
  22. package/lib/cjs/components/system-debug-trace/events/index.d.ts +0 -1
  23. package/lib/cjs/components/system-debug-trace/events/index.js +0 -1
  24. package/lib/cjs/components/system-debug-trace/events/index.js.map +1 -1
  25. package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.d.ts +1 -4
  26. package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.js +1 -23
  27. package/lib/cjs/components/system-debug-trace/hooks/use-knowledge-base-info.js.map +1 -1
  28. package/lib/cjs/components/system-debug-trace/types.d.ts +2 -2
  29. package/lib/cjs/components/text.js +0 -2
  30. package/lib/cjs/components/text.js.map +1 -1
  31. package/lib/cjs/debug-system-messages.d.ts +0 -22
  32. package/lib/cjs/debug-system-messages.js +0 -22
  33. package/lib/cjs/debug-system-messages.js.map +1 -1
  34. package/lib/cjs/dev-app.d.ts +0 -22
  35. package/lib/cjs/index-types.d.ts +5 -1
  36. package/lib/cjs/index-types.js.map +1 -1
  37. package/lib/cjs/webchat/input-panel/index.js +7 -17
  38. package/lib/cjs/webchat/input-panel/index.js.map +1 -1
  39. package/lib/cjs/webchat/input-panel/textarea.d.ts +3 -2
  40. package/lib/cjs/webchat/input-panel/textarea.js +16 -46
  41. package/lib/cjs/webchat/input-panel/textarea.js.map +1 -1
  42. package/lib/cjs/webchat/input-panel/typing-network-sender.d.ts +52 -0
  43. package/lib/cjs/webchat/input-panel/typing-network-sender.js +122 -0
  44. package/lib/cjs/webchat/input-panel/typing-network-sender.js.map +1 -0
  45. package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.d.ts +16 -0
  46. package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.js +45 -0
  47. package/lib/cjs/webchat/input-panel/use-typing-chat-event-sender.js.map +1 -0
  48. package/lib/cjs/webchat/input-panel/use-typing-session.d.ts +15 -0
  49. package/lib/cjs/webchat/input-panel/use-typing-session.js +98 -0
  50. package/lib/cjs/webchat/input-panel/use-typing-session.js.map +1 -0
  51. package/lib/cjs/webchat/tracking.d.ts +0 -10
  52. package/lib/cjs/webchat/tracking.js +1 -32
  53. package/lib/cjs/webchat/tracking.js.map +1 -1
  54. package/lib/esm/components/index-types.d.ts +0 -2
  55. package/lib/esm/components/message/index.js +2 -4
  56. package/lib/esm/components/message/index.js.map +1 -1
  57. package/lib/esm/components/message/message-footer.d.ts +1 -4
  58. package/lib/esm/components/message/message-footer.js +2 -3
  59. package/lib/esm/components/message/message-footer.js.map +1 -1
  60. package/lib/esm/components/system-debug-trace/debug-message.js +0 -2
  61. package/lib/esm/components/system-debug-trace/debug-message.js.map +1 -1
  62. package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.d.ts +1 -1
  63. package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.js +6 -2
  64. package/lib/esm/components/system-debug-trace/events/ai-agent/ai-agent.js.map +1 -1
  65. package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.d.ts +6 -0
  66. package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js +55 -0
  67. package/lib/esm/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.js.map +1 -0
  68. package/lib/esm/components/system-debug-trace/events/ai-agent/types.d.ts +2 -0
  69. package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.d.ts +8 -0
  70. package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js +41 -0
  71. package/lib/esm/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.js.map +1 -0
  72. package/lib/esm/components/system-debug-trace/events/constants.d.ts +0 -2
  73. package/lib/esm/components/system-debug-trace/events/constants.js +0 -2
  74. package/lib/esm/components/system-debug-trace/events/constants.js.map +1 -1
  75. package/lib/esm/components/system-debug-trace/events/index.d.ts +0 -1
  76. package/lib/esm/components/system-debug-trace/events/index.js +0 -1
  77. package/lib/esm/components/system-debug-trace/events/index.js.map +1 -1
  78. package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.d.ts +1 -4
  79. package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.js +1 -23
  80. package/lib/esm/components/system-debug-trace/hooks/use-knowledge-base-info.js.map +1 -1
  81. package/lib/esm/components/system-debug-trace/types.d.ts +2 -2
  82. package/lib/esm/components/text.js +0 -2
  83. package/lib/esm/components/text.js.map +1 -1
  84. package/lib/esm/debug-system-messages.d.ts +0 -22
  85. package/lib/esm/debug-system-messages.js +0 -22
  86. package/lib/esm/debug-system-messages.js.map +1 -1
  87. package/lib/esm/dev-app.d.ts +0 -22
  88. package/lib/esm/index-types.d.ts +5 -1
  89. package/lib/esm/index-types.js.map +1 -1
  90. package/lib/esm/webchat/input-panel/index.js +7 -17
  91. package/lib/esm/webchat/input-panel/index.js.map +1 -1
  92. package/lib/esm/webchat/input-panel/textarea.d.ts +3 -2
  93. package/lib/esm/webchat/input-panel/textarea.js +16 -46
  94. package/lib/esm/webchat/input-panel/textarea.js.map +1 -1
  95. package/lib/esm/webchat/input-panel/typing-network-sender.d.ts +52 -0
  96. package/lib/esm/webchat/input-panel/typing-network-sender.js +122 -0
  97. package/lib/esm/webchat/input-panel/typing-network-sender.js.map +1 -0
  98. package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.d.ts +16 -0
  99. package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.js +45 -0
  100. package/lib/esm/webchat/input-panel/use-typing-chat-event-sender.js.map +1 -0
  101. package/lib/esm/webchat/input-panel/use-typing-session.d.ts +15 -0
  102. package/lib/esm/webchat/input-panel/use-typing-session.js +98 -0
  103. package/lib/esm/webchat/input-panel/use-typing-session.js.map +1 -0
  104. package/lib/esm/webchat/tracking.d.ts +0 -10
  105. package/lib/esm/webchat/tracking.js +1 -32
  106. package/lib/esm/webchat/tracking.js.map +1 -1
  107. package/package.json +2 -2
  108. package/src/components/index-types.ts +0 -2
  109. package/src/components/message/index.jsx +1 -8
  110. package/src/components/message/message-footer.tsx +0 -14
  111. package/src/components/system-debug-trace/debug-message.tsx +0 -3
  112. package/src/components/system-debug-trace/events/ai-agent/ai-agent.tsx +22 -10
  113. package/src/components/system-debug-trace/events/ai-agent/rehydrate-ai-agent-event.ts +70 -0
  114. package/src/components/system-debug-trace/events/ai-agent/types.ts +2 -0
  115. package/src/components/system-debug-trace/events/ai-agent/use-rehydrated-ai-agent-event.ts +67 -0
  116. package/src/components/system-debug-trace/events/constants.ts +0 -2
  117. package/src/components/system-debug-trace/events/index.ts +0 -1
  118. package/src/components/system-debug-trace/hooks/use-knowledge-base-info.tsx +1 -27
  119. package/src/components/system-debug-trace/types.ts +0 -2
  120. package/src/components/text.tsx +0 -2
  121. package/src/debug-system-messages.ts +0 -25
  122. package/src/index-types.ts +9 -1
  123. package/src/webchat/input-panel/index.tsx +7 -17
  124. package/src/webchat/input-panel/textarea.tsx +20 -60
  125. package/src/webchat/input-panel/typing-network-sender.ts +168 -0
  126. package/src/webchat/input-panel/use-typing-chat-event-sender.ts +67 -0
  127. package/src/webchat/input-panel/use-typing-session.ts +124 -0
  128. package/src/webchat/tracking.ts +2 -56
  129. package/lib/cjs/components/message/message-feedback.d.ts +0 -7
  130. package/lib/cjs/components/message/message-feedback.js +0 -55
  131. package/lib/cjs/components/message/message-feedback.js.map +0 -1
  132. package/lib/cjs/components/system-debug-trace/events/knowledge-base.d.ts +0 -17
  133. package/lib/cjs/components/system-debug-trace/events/knowledge-base.js +0 -42
  134. package/lib/cjs/components/system-debug-trace/events/knowledge-base.js.map +0 -1
  135. package/lib/esm/components/message/message-feedback.d.ts +0 -7
  136. package/lib/esm/components/message/message-feedback.js +0 -55
  137. package/lib/esm/components/message/message-feedback.js.map +0 -1
  138. package/lib/esm/components/system-debug-trace/events/knowledge-base.d.ts +0 -17
  139. package/lib/esm/components/system-debug-trace/events/knowledge-base.js +0 -42
  140. package/lib/esm/components/system-debug-trace/events/knowledge-base.js.map +0 -1
  141. package/src/components/message/message-feedback.tsx +0 -90
  142. package/src/components/system-debug-trace/events/knowledge-base.tsx +0 -115
@@ -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: (messageId: string) => Promise<MinimalHubtypeMessage>
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
- const sendChatEvent = async chatEvent => {
67
- const chatEventInput = {
68
- id: uuidv7(),
69
- type: INPUT.CHAT_EVENT,
70
- data: chatEvent,
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: string) => Promise<void>
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.keyCode === 13 && event.shiftKey === false) {
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={handleFocus}
96
- onBlur={handleBlur}
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={e => onKeyDown(e)}
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
+ }
@@ -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 { trackKnowledgebaseFeedback, trackCustomEvent, trackFeedbackEvent }
50
+ return { trackCustomEvent, trackFeedbackEvent }
105
51
  }
@@ -1,7 +0,0 @@
1
- interface RatingProps {
2
- botInteractionId?: string;
3
- inferenceId?: string;
4
- messageId: string;
5
- }
6
- export declare const MessageFeedback: ({ botInteractionId, inferenceId, messageId, }: RatingProps) => import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -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