@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.1

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/dist/NxtlinqAgentChat.d.ts +26 -0
  2. package/dist/NxtlinqAgentChat.d.ts.map +1 -0
  3. package/dist/NxtlinqAgentChat.js +28 -0
  4. package/dist/components/AgentAssistantShell.d.ts +5 -0
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -0
  6. package/dist/components/AgentAssistantShell.js +52 -0
  7. package/dist/components/AgentComposer.d.ts +3 -0
  8. package/dist/components/AgentComposer.d.ts.map +1 -0
  9. package/dist/components/AgentComposer.js +60 -0
  10. package/dist/components/AgentMessageList.d.ts +3 -0
  11. package/dist/components/AgentMessageList.d.ts.map +1 -0
  12. package/dist/components/AgentMessageList.js +37 -0
  13. package/dist/components/AgentRemoteAudio.d.ts +4 -0
  14. package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
  15. package/dist/components/AgentRemoteAudio.js +34 -0
  16. package/dist/components/AgentVoiceBar.d.ts +3 -0
  17. package/dist/components/AgentVoiceBar.d.ts.map +1 -0
  18. package/dist/components/AgentVoiceBar.js +91 -0
  19. package/dist/components/PresetMessageChips.d.ts +3 -0
  20. package/dist/components/PresetMessageChips.d.ts.map +1 -0
  21. package/dist/components/PresetMessageChips.js +23 -0
  22. package/dist/context/AgentAssistantContext.d.ts +32 -0
  23. package/dist/context/AgentAssistantContext.d.ts.map +1 -0
  24. package/dist/context/AgentAssistantContext.js +159 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts +2 -0
  29. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
  30. package/dist/legacy/assets/images/adiSideItalicDataUri.js +1 -0
  31. package/dist/legacy/chatbot/ChatBot.d.ts +5 -0
  32. package/dist/legacy/chatbot/ChatBot.d.ts.map +1 -0
  33. package/dist/legacy/chatbot/ChatBot.js +35 -0
  34. package/dist/legacy/chatbot/context/ChatBotContext.d.ts +5 -0
  35. package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -0
  36. package/dist/legacy/chatbot/context/ChatBotContext.js +2908 -0
  37. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts +166 -0
  38. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts.map +1 -0
  39. package/dist/legacy/chatbot/types/ChatBotTypes.js +1 -0
  40. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts +17 -0
  41. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts.map +1 -0
  42. package/dist/legacy/chatbot/ui/BerifyMeModal.js +110 -0
  43. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts +3 -0
  44. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -0
  45. package/dist/legacy/chatbot/ui/ChatBotUI.js +625 -0
  46. package/dist/legacy/chatbot/ui/MessageInput.d.ts +3 -0
  47. package/dist/legacy/chatbot/ui/MessageInput.d.ts.map +1 -0
  48. package/dist/legacy/chatbot/ui/MessageInput.js +321 -0
  49. package/dist/legacy/chatbot/ui/MessageList.d.ts +4 -0
  50. package/dist/legacy/chatbot/ui/MessageList.d.ts.map +1 -0
  51. package/dist/legacy/chatbot/ui/MessageList.js +455 -0
  52. package/dist/legacy/chatbot/ui/ModelSelector.d.ts +4 -0
  53. package/dist/legacy/chatbot/ui/ModelSelector.d.ts.map +1 -0
  54. package/dist/legacy/chatbot/ui/ModelSelector.js +122 -0
  55. package/dist/legacy/chatbot/ui/NotificationModal.d.ts +15 -0
  56. package/dist/legacy/chatbot/ui/NotificationModal.d.ts.map +1 -0
  57. package/dist/legacy/chatbot/ui/NotificationModal.js +53 -0
  58. package/dist/legacy/chatbot/ui/PermissionForm.d.ts +8 -0
  59. package/dist/legacy/chatbot/ui/PermissionForm.d.ts.map +1 -0
  60. package/dist/legacy/chatbot/ui/PermissionForm.js +465 -0
  61. package/dist/legacy/chatbot/ui/PresetMessages.d.ts +4 -0
  62. package/dist/legacy/chatbot/ui/PresetMessages.d.ts.map +1 -0
  63. package/dist/legacy/chatbot/ui/PresetMessages.js +33 -0
  64. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts +3 -0
  65. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts.map +1 -0
  66. package/dist/legacy/chatbot/ui/VoiceModePanel.js +95 -0
  67. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts +73 -0
  68. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts.map +1 -0
  69. package/dist/legacy/chatbot/ui/styles/isolatedStyles.js +985 -0
  70. package/dist/legacy/index.d.ts +14 -0
  71. package/dist/legacy/index.d.ts.map +1 -0
  72. package/dist/legacy/index.js +12 -0
  73. package/dist/theme/defaultTheme.d.ts +3 -0
  74. package/dist/theme/defaultTheme.d.ts.map +1 -0
  75. package/dist/theme/defaultTheme.js +20 -0
  76. package/dist/types.d.ts +62 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +1 -0
  79. package/dist/voice/useVoiceConnectOrchestration.d.ts +21 -0
  80. package/dist/voice/useVoiceConnectOrchestration.d.ts.map +1 -0
  81. package/dist/voice/useVoiceConnectOrchestration.js +86 -0
  82. package/dist/voice/useVoiceMicState.d.ts +15 -0
  83. package/dist/voice/useVoiceMicState.d.ts.map +1 -0
  84. package/dist/voice/useVoiceMicState.js +94 -0
  85. package/dist/voice/useVoiceSilenceCommit.d.ts +10 -0
  86. package/dist/voice/useVoiceSilenceCommit.d.ts.map +1 -0
  87. package/dist/voice/useVoiceSilenceCommit.js +67 -0
  88. package/dist/voice/useVoiceTranscriptMessages.d.ts +16 -0
  89. package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -0
  90. package/dist/voice/useVoiceTranscriptMessages.js +129 -0
  91. package/dist/voice/useWsRealtimeAudio.d.ts +18 -0
  92. package/dist/voice/useWsRealtimeAudio.d.ts.map +1 -0
  93. package/dist/voice/useWsRealtimeAudio.js +102 -0
  94. package/dist/voice/voiceMicConstants.d.ts +4 -0
  95. package/dist/voice/voiceMicConstants.d.ts.map +1 -0
  96. package/dist/voice/voiceMicConstants.js +10 -0
  97. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts +23 -0
  98. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts.map +1 -0
  99. package/dist/voice/ws/BrowserWsPcmPlayer.js +137 -0
  100. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts +17 -0
  101. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts.map +1 -0
  102. package/dist/voice/ws/BrowserWsPcmRecorder.js +71 -0
  103. package/dist/voice/ws/float32ToPcm16.d.ts +2 -0
  104. package/dist/voice/ws/float32ToPcm16.d.ts.map +1 -0
  105. package/dist/voice/ws/float32ToPcm16.js +8 -0
  106. package/dist/voice/ws/voiceSilenceConstants.d.ts +5 -0
  107. package/dist/voice/ws/voiceSilenceConstants.d.ts.map +1 -0
  108. package/dist/voice/ws/voiceSilenceConstants.js +4 -0
  109. package/dist/voice/ws/wsRealtimeConstants.d.ts +2 -0
  110. package/dist/voice/ws/wsRealtimeConstants.d.ts.map +1 -0
  111. package/dist/voice/ws/wsRealtimeConstants.js +1 -0
  112. package/package.json +60 -0
  113. package/src/NxtlinqAgentChat.tsx +79 -0
  114. package/src/components/AgentAssistantShell.tsx +104 -0
  115. package/src/components/AgentComposer.tsx +134 -0
  116. package/src/components/AgentMessageList.tsx +78 -0
  117. package/src/components/AgentRemoteAudio.tsx +34 -0
  118. package/src/components/AgentVoiceBar.tsx +173 -0
  119. package/src/components/PresetMessageChips.tsx +41 -0
  120. package/src/context/AgentAssistantContext.tsx +276 -0
  121. package/src/index.ts +78 -0
  122. package/src/legacy/assets/images/adiSideItalicDataUri.ts +1 -0
  123. package/src/legacy/chatbot/ChatBot.tsx +61 -0
  124. package/src/legacy/chatbot/context/ChatBotContext.tsx +3227 -0
  125. package/src/legacy/chatbot/types/ChatBotTypes.ts +195 -0
  126. package/src/legacy/chatbot/ui/BerifyMeModal.tsx +145 -0
  127. package/src/legacy/chatbot/ui/ChatBotUI.tsx +949 -0
  128. package/src/legacy/chatbot/ui/MessageInput.tsx +517 -0
  129. package/src/legacy/chatbot/ui/MessageList.tsx +764 -0
  130. package/src/legacy/chatbot/ui/ModelSelector.tsx +190 -0
  131. package/src/legacy/chatbot/ui/NotificationModal.tsx +110 -0
  132. package/src/legacy/chatbot/ui/PermissionForm.tsx +632 -0
  133. package/src/legacy/chatbot/ui/PresetMessages.tsx +50 -0
  134. package/src/legacy/chatbot/ui/VoiceModePanel.tsx +168 -0
  135. package/src/legacy/chatbot/ui/styles/isolatedStyles.ts +1058 -0
  136. package/src/legacy/index.ts +26 -0
  137. package/src/theme/defaultTheme.ts +22 -0
  138. package/src/types.ts +65 -0
  139. package/src/voice/useVoiceConnectOrchestration.ts +117 -0
  140. package/src/voice/useVoiceMicState.ts +117 -0
  141. package/src/voice/useVoiceTranscriptMessages.ts +173 -0
  142. package/src/voice/voiceMicConstants.ts +13 -0
@@ -0,0 +1,949 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import { css } from '@emotion/react';
3
+ import * as React from 'react';
4
+ import { useDraggable, useLocalStorage, useResizable, walletTextUtils } from '@bytexbyte/nxtlinq-ai-agent-web-development';
5
+ import { useChatBot } from '../context/ChatBotContext';
6
+ import GraphicEqIcon from '@mui/icons-material/GraphicEq';
7
+ import { MessageInput } from './MessageInput';
8
+ import { MessageList } from './MessageList';
9
+ import { ModelSelector } from './ModelSelector';
10
+ import { PermissionForm } from './PermissionForm';
11
+ import { PresetMessages } from './PresetMessages';
12
+ import { VoiceModePanel } from './VoiceModePanel';
13
+ import {
14
+ chatHeader,
15
+ chatWindow,
16
+ closeButton,
17
+ errorToast,
18
+ floatingButton,
19
+ headerButton,
20
+ headerTitle,
21
+ idvBanner,
22
+ idvBannerText,
23
+ idvBannerTitle,
24
+ idvDismissButton,
25
+ idvVerifyButton,
26
+ infoToast,
27
+ loadingSpinner,
28
+ modalOverlay,
29
+ resizeHandleNE,
30
+ resizeHandleNW,
31
+ resizeHandleSE,
32
+ resizeHandleSW,
33
+ sdkContainer,
34
+ successToast,
35
+ toastCloseButton,
36
+ warningToast
37
+ } from './styles/isolatedStyles';
38
+
39
+ /** Header drag band inset so it does not overlap corner resize hit areas */
40
+ const DRAG_CORNER_EXCLUSION_PX = 20;
41
+ const MOBILE_BREAKPOINT = 768;
42
+ const MOBILE_EDGE_MARGIN = 12;
43
+ const MOBILE_FAB_POSITION = { right: MOBILE_EDGE_MARGIN, bottom: MOBILE_EDGE_MARGIN } as const;
44
+
45
+ const isMobileViewport = (): boolean =>
46
+ typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT;
47
+
48
+ // Toast Notification Component
49
+ const ToastNotification: React.FC<{
50
+ type: 'success' | 'error' | 'warning' | 'info';
51
+ message: string;
52
+ onClose: () => void;
53
+ isChatOpen?: boolean;
54
+ }> = ({ type, message, onClose, isChatOpen = false }) => {
55
+ const getToastStyles = () => {
56
+ const baseStyles = css`
57
+ ${isChatOpen ? css`
58
+ top: 20px !important;
59
+ right: 20px !important;
60
+ max-width: 480px !important;
61
+ ` : css`
62
+ bottom: 20px !important;
63
+ right: 20px !important;
64
+ `}
65
+ `;
66
+
67
+ switch (type) {
68
+ case 'success':
69
+ return css`${successToast} ${baseStyles}`;
70
+ case 'error':
71
+ return css`${errorToast} ${baseStyles}`;
72
+ case 'warning':
73
+ return css`${warningToast} ${baseStyles}`;
74
+ default:
75
+ return css`${infoToast} ${baseStyles}`;
76
+ }
77
+ };
78
+
79
+ const getIcon = () => {
80
+ switch (type) {
81
+ case 'success':
82
+ return '✅';
83
+ case 'error':
84
+ return '❌';
85
+ case 'warning':
86
+ return '⚠️';
87
+ default:
88
+ return 'ℹ️';
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div css={getToastStyles()}>
94
+ <span css={css`font-size: 18px !important;`}>{getIcon()}</span>
95
+ <span css={css`flex: 1 !important;`}>{message}</span>
96
+ <button
97
+ onClick={onClose}
98
+ css={toastCloseButton}
99
+ >
100
+ ×
101
+ </button>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export const ChatBotUI: React.FC = () => {
107
+ const {
108
+ isOpen,
109
+ setIsOpen,
110
+ showPermissionForm,
111
+ setShowPermissionForm,
112
+ notification,
113
+ setNotification,
114
+ isAITLoading,
115
+ hitAddress,
116
+ walletInfo,
117
+ onVerifyWallet,
118
+ serviceId,
119
+ isNeedSignInWithWallet,
120
+ piiDisplayMode,
121
+ isVoiceMode,
122
+ isVoiceConnecting,
123
+ enterVoiceMode,
124
+ exitVoiceMode,
125
+ props
126
+ } = useChatBot();
127
+
128
+ const positionRef = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
129
+ const { dimensions, handleResizeStart, positionAdjustment } = useResizable(positionRef);
130
+
131
+ const { position, handleDragStart, isDragging, updatePosition } = useDraggable(dimensions);
132
+
133
+ const [mobileLayout, setMobileLayout] = React.useState(isMobileViewport);
134
+ React.useEffect(() => {
135
+ if (typeof window === 'undefined') return;
136
+ const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
137
+ const sync = () => setMobileLayout(mq.matches);
138
+ sync();
139
+ mq.addEventListener('change', sync);
140
+ return () => mq.removeEventListener('change', sync);
141
+ }, []);
142
+
143
+ // Update position ref when position changes
144
+ React.useEffect(() => {
145
+ positionRef.current = position;
146
+ }, [position]);
147
+
148
+ // Update position when resizing to keep bottom-right corner fixed
149
+ React.useEffect(() => {
150
+ if (positionAdjustment) {
151
+ updatePosition(positionAdjustment);
152
+ }
153
+ }, [positionAdjustment, updatePosition]);
154
+
155
+ // IDV suggestion banner state
156
+ const [showIDVSuggestion, setShowIDVSuggestion] = React.useState(true);
157
+ const [dismissUntil, setDismissUntil] = React.useState<number | null>(null);
158
+ const [timeRemaining, setTimeRemaining] = React.useState<string>('');
159
+ const countdownIntervalRef = React.useRef<NodeJS.Timeout | null>(null);
160
+
161
+ // Check if there's a berifyme token in URL (indicating recent berifyme verification)
162
+ const urlParams = new URLSearchParams(window.location.search);
163
+ const hasBerifymeToken = urlParams.get('token') && urlParams.get('method') === 'berifyme';
164
+
165
+ const isWalletVerifiedWithBerifyme = walletInfo?.id && walletInfo?.method === 'berifyme';
166
+
167
+ // Helper function to update IDV suggestion state based on storage value
168
+ const updateIDVSuggestionState = React.useCallback((dismissedValue: string | null) => {
169
+ // Clear any existing countdown interval
170
+ if (countdownIntervalRef.current) {
171
+ clearInterval(countdownIntervalRef.current);
172
+ countdownIntervalRef.current = null;
173
+ }
174
+
175
+ if (dismissedValue) {
176
+ const dismissTime = parseInt(dismissedValue);
177
+ const now = Date.now();
178
+ const timeLeft = dismissTime - now;
179
+
180
+ if (timeLeft > 0) {
181
+ setShowIDVSuggestion(false);
182
+ setDismissUntil(dismissTime);
183
+
184
+ // Calculate initial time remaining immediately
185
+ const hours = Math.floor(timeLeft / (1000 * 60 * 60));
186
+ const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
187
+ setTimeRemaining(`${hours}h ${minutes}m`);
188
+
189
+ // Set up countdown interval to update time remaining every minute
190
+ countdownIntervalRef.current = setInterval(() => {
191
+ const remaining = dismissTime - Date.now();
192
+ if (remaining > 0) {
193
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
194
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
195
+ setTimeRemaining(`${hours}h ${minutes}m`);
196
+ } else {
197
+ setShowIDVSuggestion(true);
198
+ setDismissUntil(null);
199
+ setTimeRemaining('');
200
+ localStorage.removeItem('idv-suggestion-dismissed');
201
+ if (countdownIntervalRef.current) {
202
+ clearInterval(countdownIntervalRef.current);
203
+ countdownIntervalRef.current = null;
204
+ }
205
+ }
206
+ }, 60000);
207
+ } else {
208
+ localStorage.removeItem('idv-suggestion-dismissed');
209
+ setShowIDVSuggestion(true);
210
+ setDismissUntil(null);
211
+ setTimeRemaining('');
212
+ }
213
+ } else {
214
+ setShowIDVSuggestion(true);
215
+ setDismissUntil(null);
216
+ setTimeRemaining('');
217
+ }
218
+ }, []);
219
+
220
+ // Check if IDV suggestion should be shown
221
+ React.useEffect(() => {
222
+ const shouldShowBanner = hitAddress &&
223
+ !props.requireWalletIDVVerification &&
224
+ !hasBerifymeToken &&
225
+ !isWalletVerifiedWithBerifyme &&
226
+ !isNeedSignInWithWallet;
227
+
228
+ if (shouldShowBanner) {
229
+ const timer = setTimeout(() => {
230
+ const shouldShowBannerAfterDelay = hitAddress &&
231
+ !props.requireWalletIDVVerification &&
232
+ !hasBerifymeToken &&
233
+ !isWalletVerifiedWithBerifyme &&
234
+ !isNeedSignInWithWallet;
235
+
236
+ if (shouldShowBannerAfterDelay) {
237
+ const dismissed = localStorage.getItem('idv-suggestion-dismissed');
238
+ updateIDVSuggestionState(dismissed);
239
+ } else {
240
+ setShowIDVSuggestion(false);
241
+ setDismissUntil(null);
242
+ setTimeRemaining('');
243
+ }
244
+ }, 1000);
245
+
246
+ return () => {
247
+ clearTimeout(timer);
248
+ // Clear countdown interval on cleanup
249
+ if (countdownIntervalRef.current) {
250
+ clearInterval(countdownIntervalRef.current);
251
+ countdownIntervalRef.current = null;
252
+ }
253
+ };
254
+ } else {
255
+ // Don't show when:
256
+ // - No wallet connected (no hitAddress)
257
+ // - Wallet is already verified (walletInfo?.id exists)
258
+ setShowIDVSuggestion(false);
259
+ setDismissUntil(null);
260
+ setTimeRemaining('');
261
+ }
262
+ }, [hitAddress, walletInfo, isNeedSignInWithWallet, props.requireWalletIDVVerification, updateIDVSuggestionState]);
263
+
264
+ const handleDismissIDV = () => {
265
+ const dismissSeconds = props.idvBannerDismissSeconds || 86400;
266
+ const dismissTime = Date.now() + (dismissSeconds * 1000);
267
+ localStorage.setItem('idv-suggestion-dismissed', dismissTime.toString());
268
+ setShowIDVSuggestion(false);
269
+ setDismissUntil(dismissTime);
270
+
271
+ // Calculate and set initial time remaining
272
+ const initialHours = Math.floor(dismissSeconds / 3600);
273
+ const initialMinutes = Math.floor((dismissSeconds % 3600) / 60);
274
+ setTimeRemaining(`${initialHours}h ${initialMinutes}m`);
275
+
276
+ // Set up countdown interval
277
+ if (countdownIntervalRef.current) {
278
+ clearInterval(countdownIntervalRef.current);
279
+ }
280
+ countdownIntervalRef.current = setInterval(() => {
281
+ const remaining = dismissTime - Date.now();
282
+ if (remaining > 0) {
283
+ const remainingHours = Math.floor(remaining / (1000 * 60 * 60));
284
+ const remainingMinutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
285
+ setTimeRemaining(`${remainingHours}h ${remainingMinutes}m`);
286
+ } else {
287
+ setShowIDVSuggestion(true);
288
+ setDismissUntil(null);
289
+ setTimeRemaining('');
290
+ localStorage.removeItem('idv-suggestion-dismissed');
291
+ if (countdownIntervalRef.current) {
292
+ clearInterval(countdownIntervalRef.current);
293
+ countdownIntervalRef.current = null;
294
+ }
295
+ }
296
+ }, 60000);
297
+
298
+ const hours = Math.floor(dismissSeconds / 3600);
299
+ const minutes = Math.floor((dismissSeconds % 3600) / 60);
300
+ const timeText = hours > 0
301
+ ? (minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`)
302
+ : `${minutes}m`;
303
+ setNotification({
304
+ show: true,
305
+ type: 'info',
306
+ message: walletTextUtils.getWalletText(`IDV suggestion hidden for ${timeText}. You can still verify your wallet anytime.`, serviceId),
307
+ autoHide: true,
308
+ duration: 3000
309
+ });
310
+ };
311
+
312
+ // Add CSS animation for loading spinner
313
+ React.useEffect(() => {
314
+ const style = document.createElement('style');
315
+ style.textContent = `
316
+ @keyframes spin {
317
+ 0% { transform: rotate(0deg); }
318
+ 100% { transform: rotate(360deg); }
319
+ }
320
+ `;
321
+ document.head.appendChild(style);
322
+ return () => {
323
+ document.head.removeChild(style);
324
+ };
325
+ }, []);
326
+
327
+ const handleClose = () => {
328
+ if (typeof window !== 'undefined') {
329
+ if (mobileLayout) {
330
+ setButtonPosition(MOBILE_FAB_POSITION);
331
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
332
+ } else {
333
+ const windowRight = window.innerWidth - (position.x + dimensions.width);
334
+ const windowBottom = window.innerHeight - (position.y + dimensions.height);
335
+ const newButtonPosition = clampButtonPosition({ right: windowRight, bottom: windowBottom });
336
+ setButtonPosition(newButtonPosition);
337
+ if (hasLoadedButtonPosition.current) {
338
+ setSavedButtonPosition(newButtonPosition);
339
+ }
340
+ }
341
+ }
342
+
343
+ setIsOpen(false);
344
+ };
345
+
346
+ const handleOpen = () => {
347
+ if (typeof window !== 'undefined') {
348
+ if (mobileLayout) {
349
+ updatePosition({ x: MOBILE_EDGE_MARGIN, y: MOBILE_EDGE_MARGIN });
350
+ } else if (buttonRef.current) {
351
+ // Window's bottom-right corner aligns with the FAB
352
+ const buttonRect = buttonRef.current.getBoundingClientRect();
353
+ const buttonRight = window.innerWidth - buttonRect.right;
354
+ const buttonBottom = window.innerHeight - buttonRect.bottom;
355
+ const windowLeft = window.innerWidth - dimensions.width - buttonRight;
356
+ const windowTop = window.innerHeight - dimensions.height - buttonBottom;
357
+ const clampedLeft = Math.max(BUTTON_MARGIN, Math.min(windowLeft, window.innerWidth - dimensions.width - BUTTON_MARGIN));
358
+ const clampedTop = Math.max(BUTTON_MARGIN, Math.min(windowTop, window.innerHeight - dimensions.height - BUTTON_MARGIN));
359
+ updatePosition({ x: clampedLeft, y: clampedTop });
360
+ }
361
+ }
362
+
363
+ setIsOpen(true);
364
+ };
365
+
366
+ const handleCloseNotification = () => {
367
+ setNotification((prev: any) => ({ ...prev, show: false }));
368
+ };
369
+
370
+ // Auto-hide notification when autoHide is true and duration is set
371
+ React.useEffect(() => {
372
+ if (notification.show && notification.autoHide && notification.duration && notification.duration > 0) {
373
+ const timer = setTimeout(() => {
374
+ setNotification((prev: any) => ({ ...prev, show: false }));
375
+ }, notification.duration);
376
+
377
+ return () => clearTimeout(timer);
378
+ }
379
+ }, [notification.show, notification.autoHide, notification.duration, setNotification]);
380
+
381
+ const handleSettingsClick = () => {
382
+ setShowPermissionForm(true);
383
+ };
384
+
385
+ // Constants for button positioning
386
+ const BUTTON_MARGIN = 20;
387
+ const BUTTON_MIN_MARGIN = 5;
388
+ const DRAG_THRESHOLD = 5; // Minimum distance in pixels to distinguish drag from click
389
+
390
+ // Track window size for responsive button positioning and dragging
391
+ const buttonRef = React.useRef<HTMLButtonElement>(null);
392
+ const buttonDragState = React.useRef<{
393
+ startX: number;
394
+ startY: number;
395
+ startRight: number;
396
+ startBottom: number;
397
+ buttonWidth: number;
398
+ buttonHeight: number;
399
+ } | null>(null);
400
+ const [isButtonDragging, setIsButtonDragging] = React.useState(false);
401
+ const [isButtonDragActive, setIsButtonDragActive] = React.useState(false);
402
+ const hasLoadedButtonPosition = React.useRef(false);
403
+ const buttonDragDistance = React.useRef(0);
404
+ // Track latest button position for saving (to avoid stale closure issues)
405
+ const latestButtonPositionRef = React.useRef<{ right: number; bottom: number }>({ right: BUTTON_MARGIN, bottom: BUTTON_MARGIN });
406
+
407
+ // Load saved position synchronously on initialization to avoid position jump
408
+ const getInitialButtonPosition = (): { right: number; bottom: number } => {
409
+ if (typeof window === 'undefined') {
410
+ return { right: BUTTON_MARGIN, bottom: BUTTON_MARGIN };
411
+ }
412
+ if (isMobileViewport()) {
413
+ return { ...MOBILE_FAB_POSITION };
414
+ }
415
+
416
+ try {
417
+ const saved = localStorage.getItem('ai-agent-button-position');
418
+ if (saved) {
419
+ const parsed = JSON.parse(saved);
420
+ if (parsed && typeof parsed.right === 'number' && typeof parsed.bottom === 'number') {
421
+ return parsed;
422
+ }
423
+ }
424
+ } catch (e) {
425
+ // Ignore parse errors, use default
426
+ }
427
+
428
+ return { right: BUTTON_MARGIN, bottom: BUTTON_MARGIN };
429
+ };
430
+
431
+ const [, setSavedButtonPosition, isButtonPositionInitialized] = useLocalStorage<{ right: number; bottom: number } | null>(
432
+ 'ai-agent-button-position',
433
+ null
434
+ );
435
+
436
+ const [buttonPosition, setButtonPosition] = React.useState(getInitialButtonPosition);
437
+
438
+ // Keep ref in sync with button position state and mark as loaded when initialized
439
+ React.useEffect(() => {
440
+ latestButtonPositionRef.current = buttonPosition;
441
+ if (isButtonPositionInitialized && !hasLoadedButtonPosition.current) {
442
+ hasLoadedButtonPosition.current = true;
443
+ }
444
+ }, [buttonPosition, isButtonPositionInitialized]);
445
+
446
+ // Fast clamp calculation helper (used in both drag and resize)
447
+ const clampButtonPositionFast = React.useCallback((right: number, bottom: number, buttonWidth: number, buttonHeight: number): { right: number; bottom: number } => {
448
+ const viewportWidth = window.innerWidth;
449
+ const viewportHeight = window.innerHeight;
450
+
451
+ // Clamp horizontally
452
+ const maxRight = viewportWidth - buttonWidth - BUTTON_MARGIN;
453
+ let clampedRight = right;
454
+ if (clampedRight > maxRight) {
455
+ clampedRight = Math.max(BUTTON_MIN_MARGIN, maxRight);
456
+ } else if (clampedRight < BUTTON_MARGIN) {
457
+ clampedRight = BUTTON_MARGIN;
458
+ }
459
+
460
+ // Clamp vertically
461
+ const maxBottom = viewportHeight - buttonHeight - BUTTON_MARGIN;
462
+ let clampedBottom = bottom;
463
+ if (clampedBottom > maxBottom) {
464
+ clampedBottom = Math.max(BUTTON_MIN_MARGIN, maxBottom);
465
+ } else if (clampedBottom < BUTTON_MARGIN) {
466
+ clampedBottom = BUTTON_MARGIN;
467
+ }
468
+
469
+ return { right: clampedRight, bottom: clampedBottom };
470
+ }, []);
471
+
472
+ // Clamp button position to keep it within viewport
473
+ const clampButtonPosition = React.useCallback((pos: { right: number; bottom: number }, buttonWidth?: number, buttonHeight?: number): { right: number; bottom: number } => {
474
+ if (typeof window === 'undefined') {
475
+ return pos;
476
+ }
477
+
478
+ // Use provided dimensions or get from DOM (avoid getBoundingClientRect during drag)
479
+ let width = buttonWidth;
480
+ let height = buttonHeight;
481
+
482
+ if (width === undefined || height === undefined) {
483
+ if (!buttonRef.current) {
484
+ return pos;
485
+ }
486
+ const rect = buttonRef.current.getBoundingClientRect();
487
+ width = rect.width;
488
+ height = rect.height;
489
+ }
490
+
491
+ return clampButtonPositionFast(pos.right, pos.bottom, width, height);
492
+ }, [clampButtonPositionFast]);
493
+
494
+ const handleButtonDragStart = React.useCallback((event: React.PointerEvent<HTMLButtonElement>) => {
495
+ if (mobileLayout) {
496
+ return;
497
+ }
498
+ event.stopPropagation();
499
+
500
+ if (buttonRef.current) {
501
+ // Cache button dimensions to avoid getBoundingClientRect during drag
502
+ const rect = buttonRef.current.getBoundingClientRect();
503
+ buttonDragState.current = {
504
+ startX: event.clientX,
505
+ startY: event.clientY,
506
+ startRight: buttonPosition.right,
507
+ startBottom: buttonPosition.bottom,
508
+ buttonWidth: rect.width,
509
+ buttonHeight: rect.height
510
+ };
511
+ buttonDragDistance.current = 0;
512
+ setIsButtonDragging(false);
513
+ setIsButtonDragActive(true);
514
+ document.body.style.userSelect = 'none';
515
+ }
516
+ }, [buttonPosition, mobileLayout]);
517
+
518
+ React.useEffect(() => {
519
+ if (!mobileLayout) {
520
+ return;
521
+ }
522
+ setButtonPosition(MOBILE_FAB_POSITION);
523
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
524
+ }, [mobileLayout]);
525
+
526
+ // Handle button drag move and end
527
+ React.useEffect(() => {
528
+ if (mobileLayout || !isButtonDragActive) {
529
+ return;
530
+ }
531
+
532
+ const handlePointerMove = (event: PointerEvent) => {
533
+ if (!buttonDragState.current) return;
534
+
535
+ const deltaX = event.clientX - buttonDragState.current.startX;
536
+ const deltaY = event.clientY - buttonDragState.current.startY;
537
+
538
+ const dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
539
+ buttonDragDistance.current = dragDistance;
540
+
541
+ // Only start dragging if moved more than threshold (to distinguish from click)
542
+ if (dragDistance > DRAG_THRESHOLD && !isButtonDragging) {
543
+ setIsButtonDragging(true);
544
+ document.body.style.cursor = 'grabbing';
545
+ }
546
+
547
+ if (dragDistance > DRAG_THRESHOLD) {
548
+ // Calculate new position (right decreases when moving right, bottom decreases when moving down)
549
+ const newRight = buttonDragState.current.startRight - deltaX;
550
+ const newBottom = buttonDragState.current.startBottom - deltaY;
551
+
552
+ // Use fast clamp calculation with cached dimensions
553
+ const clampedPos = clampButtonPositionFast(
554
+ newRight,
555
+ newBottom,
556
+ buttonDragState.current.buttonWidth,
557
+ buttonDragState.current.buttonHeight
558
+ );
559
+ latestButtonPositionRef.current = clampedPos;
560
+
561
+ // Update state immediately (same as window drag) for responsive dragging
562
+ setButtonPosition(clampedPos);
563
+ }
564
+ };
565
+
566
+ const handlePointerUp = () => {
567
+ const wasDragging = buttonDragDistance.current > DRAG_THRESHOLD;
568
+
569
+ // Save position to localStorage if we were dragging
570
+ if (wasDragging && hasLoadedButtonPosition.current) {
571
+ setSavedButtonPosition(latestButtonPositionRef.current);
572
+ }
573
+
574
+ setIsButtonDragging(false);
575
+ setIsButtonDragActive(false);
576
+ buttonDragDistance.current = 0;
577
+ document.body.style.userSelect = '';
578
+ document.body.style.cursor = '';
579
+ buttonDragState.current = null;
580
+ };
581
+
582
+ document.addEventListener('pointermove', handlePointerMove);
583
+ document.addEventListener('pointerup', handlePointerUp);
584
+
585
+ return () => {
586
+ document.removeEventListener('pointermove', handlePointerMove);
587
+ document.removeEventListener('pointerup', handlePointerUp);
588
+ document.body.style.userSelect = '';
589
+ document.body.style.cursor = '';
590
+ };
591
+ }, [isButtonDragActive, isButtonDragging, setSavedButtonPosition, clampButtonPositionFast, mobileLayout]);
592
+
593
+ // Update button position on window resize to ensure it stays within viewport
594
+ React.useEffect(() => {
595
+ if (typeof window === 'undefined') return;
596
+
597
+ const handleResize = () => {
598
+ requestAnimationFrame(() => {
599
+ if (mobileLayout) {
600
+ setButtonPosition(MOBILE_FAB_POSITION);
601
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
602
+ return;
603
+ }
604
+ const clampedPos = clampButtonPosition(buttonPosition);
605
+ if (clampedPos.right !== buttonPosition.right || clampedPos.bottom !== buttonPosition.bottom) {
606
+ setButtonPosition(clampedPos);
607
+ }
608
+ });
609
+ };
610
+
611
+ window.addEventListener('resize', handleResize);
612
+ return () => window.removeEventListener('resize', handleResize);
613
+ }, [buttonPosition, clampButtonPosition, mobileLayout]);
614
+
615
+ const getButtonStyle = React.useMemo(() => {
616
+ if (mobileLayout) {
617
+ return css`
618
+ position: fixed !important;
619
+ bottom: calc(${MOBILE_EDGE_MARGIN}px + env(safe-area-inset-bottom, 0px)) !important;
620
+ right: calc(${MOBILE_EDGE_MARGIN}px + env(safe-area-inset-right, 0px)) !important;
621
+ left: auto !important;
622
+ top: auto !important;
623
+ touch-action: manipulation !important;
624
+ `;
625
+ }
626
+ return css`
627
+ position: fixed !important;
628
+ bottom: ${buttonPosition.bottom}px !important;
629
+ right: ${buttonPosition.right}px !important;
630
+ `;
631
+ }, [buttonPosition, mobileLayout]);
632
+
633
+ // Show floating button when chat is closed
634
+ if (!isOpen) {
635
+ return (
636
+ <div css={sdkContainer}>
637
+ <button
638
+ ref={buttonRef}
639
+ onClick={(e) => {
640
+ // Don't open if we just finished dragging (moved more than threshold)
641
+ if (buttonDragDistance.current > DRAG_THRESHOLD) {
642
+ e.preventDefault();
643
+ e.stopPropagation();
644
+ return;
645
+ }
646
+ handleOpen();
647
+ }}
648
+ onPointerDown={mobileLayout ? undefined : handleButtonDragStart}
649
+ css={[floatingButton, getButtonStyle, css`
650
+ cursor: ${mobileLayout ? 'pointer' : isButtonDragging ? 'grabbing' : 'grab'} !important;
651
+ user-select: none !important;
652
+ `]}
653
+ title={mobileLayout ? 'Open AI Agent Chat' : 'Drag to move or click to open AI Agent Chat'}
654
+ >
655
+ AI Agent
656
+ </button>
657
+
658
+ {notification.show && (
659
+ <ToastNotification
660
+ type={notification.type}
661
+ message={notification.message}
662
+ onClose={handleCloseNotification}
663
+ isChatOpen={isOpen}
664
+ />
665
+ )}
666
+ </div>
667
+ );
668
+ }
669
+
670
+ return (
671
+ <div css={sdkContainer}>
672
+ <div
673
+ css={[chatWindow, css`
674
+ width: ${dimensions.width}px !important;
675
+ height: ${dimensions.height}px !important;
676
+ left: ${position.x}px !important;
677
+ top: ${position.y}px !important;
678
+ bottom: auto !important;
679
+ right: auto !important;
680
+ ${mobileLayout ? css`
681
+ width: calc(100vw - ${MOBILE_EDGE_MARGIN * 2}px) !important;
682
+ height: calc(100dvh - ${MOBILE_EDGE_MARGIN * 2}px) !important;
683
+ max-width: calc(100vw - ${MOBILE_EDGE_MARGIN * 2}px) !important;
684
+ max-height: calc(100dvh - ${MOBILE_EDGE_MARGIN * 2}px) !important;
685
+ left: ${MOBILE_EDGE_MARGIN}px !important;
686
+ top: ${MOBILE_EDGE_MARGIN}px !important;
687
+ ` : ''}
688
+ `]}
689
+ >
690
+ {!mobileLayout && (
691
+ <>
692
+ <div
693
+ css={resizeHandleNW}
694
+ onPointerDown={handleResizeStart('nw')}
695
+ title="Resize"
696
+ aria-label="Resize chat window from top-left"
697
+ />
698
+ <div
699
+ css={resizeHandleNE}
700
+ onPointerDown={handleResizeStart('ne')}
701
+ title="Resize"
702
+ aria-label="Resize chat window from top-right"
703
+ />
704
+ <div
705
+ css={resizeHandleSW}
706
+ onPointerDown={handleResizeStart('sw')}
707
+ title="Resize"
708
+ aria-label="Resize chat window from bottom-left"
709
+ />
710
+ <div
711
+ css={resizeHandleSE}
712
+ onPointerDown={handleResizeStart('se')}
713
+ title="Resize"
714
+ aria-label="Resize chat window from bottom-right"
715
+ />
716
+ </>
717
+ )}
718
+
719
+ <div
720
+ css={[chatHeader, css`
721
+ position: relative !important;
722
+ `]}
723
+ >
724
+ <div
725
+ css={css`
726
+ position: absolute !important;
727
+ left: ${DRAG_CORNER_EXCLUSION_PX}px !important;
728
+ right: ${DRAG_CORNER_EXCLUSION_PX}px !important;
729
+ top: 0 !important;
730
+ bottom: 0 !important;
731
+ z-index: 1 !important;
732
+ cursor: ${isDragging ? 'grabbing' : 'grab'} !important;
733
+ user-select: none !important;
734
+ `}
735
+ onPointerDown={handleDragStart}
736
+ title="Drag to move"
737
+ aria-hidden
738
+ />
739
+ <div
740
+ css={css`
741
+ position: relative !important;
742
+ z-index: 2 !important;
743
+ display: flex !important;
744
+ justify-content: space-between !important;
745
+ align-items: center !important;
746
+ width: 100% !important;
747
+ pointer-events: none !important;
748
+ `}
749
+ >
750
+ <div css={css`
751
+ display: flex !important;
752
+ align-items: center !important;
753
+ gap: 10px !important;
754
+ pointer-events: none !important;
755
+ `}>
756
+ <h3 css={headerTitle}>
757
+ AI Agent
758
+ </h3>
759
+ {!isVoiceMode && (
760
+ <div css={css`
761
+ position: relative !important;
762
+ pointer-events: auto !important;
763
+ `}>
764
+ <ModelSelector />
765
+ </div>
766
+ )}
767
+ {!isVoiceMode && piiDisplayMode === 'redacted' && (
768
+ <div
769
+ css={css`
770
+ display: inline-flex !important;
771
+ align-items: center !important;
772
+ gap: 4px !important;
773
+ padding: 2px 8px !important;
774
+ background-color: rgba(255, 255, 255, 0.2) !important;
775
+ border: 1px solid rgba(255, 255, 255, 0.4) !important;
776
+ border-radius: 10px !important;
777
+ font-size: 10px !important;
778
+ font-weight: 600 !important;
779
+ color: #ffffff !important;
780
+ white-space: nowrap !important;
781
+ line-height: 1.4 !important;
782
+ user-select: none !important;
783
+ `}
784
+ title="PII Protection is active — sensitive data is automatically anonymized before sending to AI"
785
+ >
786
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
787
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/>
788
+ </svg>
789
+ PII Protected
790
+ </div>
791
+ )}
792
+ {isAITLoading && (
793
+ <div css={css`
794
+ display: flex !important;
795
+ align-items: center !important;
796
+ gap: 5px !important;
797
+ font-size: 12px !important;
798
+ opacity: 0.8 !important;
799
+ `}>
800
+ <div css={loadingSpinner} />
801
+ Loading...
802
+ </div>
803
+ )}
804
+ </div>
805
+ <div css={css`
806
+ display: flex !important;
807
+ align-items: center !important;
808
+ gap: 10px !important;
809
+ pointer-events: auto !important;
810
+ `}>
811
+ <button
812
+ onClick={() => void ((isVoiceMode || isVoiceConnecting) ? exitVoiceMode() : enterVoiceMode())}
813
+ css={[headerButton, css`
814
+ width: auto !important;
815
+ min-width: 82px !important;
816
+ padding: 0 10px !important;
817
+ gap: 6px !important;
818
+ font-size: 12px !important;
819
+ font-weight: 600 !important;
820
+ line-height: 1 !important;
821
+ white-space: nowrap !important;
822
+ `]}
823
+ title={
824
+ isVoiceConnecting
825
+ ? 'Cancel voice connection and return to text mode'
826
+ : isVoiceMode
827
+ ? 'Switch to text mode'
828
+ : 'Switch to voice mode'
829
+ }
830
+ onPointerDown={(e) => e.stopPropagation()}
831
+ >
832
+ <GraphicEqIcon css={css`font-size: 16px !important; color: #fff !important;`} />
833
+ {(isVoiceMode || isVoiceConnecting) ? 'Text Mode' : 'Voice Mode'}
834
+ </button>
835
+ <button
836
+ onClick={handleSettingsClick}
837
+ css={headerButton}
838
+ title="AIT Settings"
839
+ onPointerDown={(e) => e.stopPropagation()}
840
+ >
841
+ ⚙️
842
+ </button>
843
+ <button
844
+ onClick={handleClose}
845
+ css={closeButton}
846
+ onPointerDown={(e) => e.stopPropagation()}
847
+ title="Minimize"
848
+ >
849
+
850
+ </button>
851
+ </div>
852
+ </div>
853
+ </div>
854
+
855
+ {showIDVSuggestion && hitAddress && !props.requireWalletIDVVerification && !hasBerifymeToken && !isWalletVerifiedWithBerifyme && (
856
+ <div
857
+ data-idv-banner
858
+ css={idvBanner}>
859
+ <div css={css`
860
+ font-size: 20px !important;
861
+ color: #f39c12 !important;
862
+ `}>💡</div>
863
+ <div css={css`flex: 1 !important;`}>
864
+ <h5 css={idvBannerTitle}>{walletTextUtils.getWalletText('Recommended: Verify Your Wallet', serviceId)}</h5>
865
+ <p css={idvBannerText}>
866
+ {walletTextUtils.getWalletText('While not required, verifying your wallet with Berify.me provides additional security and trust for your AI agent interactions.', serviceId)}
867
+ </p>
868
+ <button
869
+ onClick={() => onVerifyWallet('berifyme')}
870
+ css={idvVerifyButton}
871
+ >
872
+ {walletTextUtils.getWalletText('Verify Wallet', serviceId)}
873
+ </button>
874
+ </div>
875
+ <button
876
+ onClick={handleDismissIDV}
877
+ css={idvDismissButton}
878
+ title={`Hide for ${Math.floor((props.idvBannerDismissSeconds || 86400) / 3600)} hours`}
879
+ >
880
+ ×
881
+ </button>
882
+ </div>
883
+ )}
884
+
885
+ {!showIDVSuggestion && dismissUntil && timeRemaining && (
886
+ <div css={css`
887
+ background-color: #f8f9fa !important;
888
+ border: 1px solid #e9ecef !important;
889
+ margin: 16px !important;
890
+ padding: 12px !important;
891
+ border-radius: 8px !important;
892
+ text-align: center !important;
893
+ font-size: 13px !important;
894
+ color: #6c757d !important;
895
+ `}>
896
+ <span>💡 IDV suggestion hidden. Will show again in {timeRemaining}</span>
897
+ <button
898
+ onClick={() => {
899
+ localStorage.removeItem('idv-suggestion-dismissed');
900
+ setShowIDVSuggestion(true);
901
+ setDismissUntil(null);
902
+ setTimeRemaining('');
903
+ }}
904
+ css={css`
905
+ background: none !important;
906
+ border: none !important;
907
+ color: #007bff !important;
908
+ font-size: 12px !important;
909
+ cursor: pointer !important;
910
+ margin-left: 8px !important;
911
+ text-decoration: underline !important;
912
+ `}
913
+ title="Show now"
914
+ >
915
+ Show now
916
+ </button>
917
+ </div>
918
+ )}
919
+
920
+ <MessageList />
921
+ {!isVoiceMode && <PresetMessages />}
922
+ {isVoiceMode ? <VoiceModePanel /> : <MessageInput />}
923
+ </div>
924
+
925
+ {showPermissionForm && (
926
+ <div
927
+ css={modalOverlay}
928
+ onClick={(e) => {
929
+ // Close modal when clicking on background
930
+ if (e.target === e.currentTarget) {
931
+ setShowPermissionForm(false);
932
+ }
933
+ }}
934
+ >
935
+ <PermissionForm onClose={() => setShowPermissionForm(false)} />
936
+ </div>
937
+ )}
938
+
939
+ {notification.show && (
940
+ <ToastNotification
941
+ type={notification.type}
942
+ message={notification.message}
943
+ onClose={handleCloseNotification}
944
+ isChatOpen={isOpen}
945
+ />
946
+ )}
947
+ </div>
948
+ );
949
+ };