@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.3 → 0.1.5

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 (91) hide show
  1. package/dist/ChatBot.d.ts +5 -0
  2. package/dist/ChatBot.d.ts.map +1 -0
  3. package/dist/ChatBot.js +35 -0
  4. package/dist/assets/images/adiSideItalicDataUri.d.ts +2 -0
  5. package/dist/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
  6. package/dist/assets/images/adiSideItalicDataUri.js +1 -0
  7. package/dist/context/ChatBotContext.d.ts +5 -0
  8. package/dist/context/ChatBotContext.d.ts.map +1 -0
  9. package/dist/context/ChatBotContext.js +2908 -0
  10. package/dist/index.d.ts +5 -13
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -11
  13. package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -1
  14. package/dist/legacy/chatbot/context/ChatBotContext.js +14 -0
  15. package/dist/types/ChatBotTypes.d.ts +166 -0
  16. package/dist/types/ChatBotTypes.d.ts.map +1 -0
  17. package/dist/types/ChatBotTypes.js +1 -0
  18. package/dist/ui/BerifyMeModal.d.ts +17 -0
  19. package/dist/ui/BerifyMeModal.d.ts.map +1 -0
  20. package/dist/ui/BerifyMeModal.js +110 -0
  21. package/dist/ui/ChatBotHeader.d.ts +15 -0
  22. package/dist/ui/ChatBotHeader.d.ts.map +1 -0
  23. package/dist/ui/ChatBotHeader.js +62 -0
  24. package/dist/ui/ChatBotUI.d.ts +3 -0
  25. package/dist/ui/ChatBotUI.d.ts.map +1 -0
  26. package/dist/ui/ChatBotUI.js +557 -0
  27. package/dist/ui/MessageInput.d.ts +3 -0
  28. package/dist/ui/MessageInput.d.ts.map +1 -0
  29. package/dist/ui/MessageInput.js +321 -0
  30. package/dist/ui/MessageList.d.ts +4 -0
  31. package/dist/ui/MessageList.d.ts.map +1 -0
  32. package/dist/ui/MessageList.js +455 -0
  33. package/dist/ui/ModelSelector.d.ts +4 -0
  34. package/dist/ui/ModelSelector.d.ts.map +1 -0
  35. package/dist/ui/ModelSelector.js +122 -0
  36. package/dist/ui/NotificationModal.d.ts +15 -0
  37. package/dist/ui/NotificationModal.d.ts.map +1 -0
  38. package/dist/ui/NotificationModal.js +53 -0
  39. package/dist/ui/PermissionForm.d.ts +8 -0
  40. package/dist/ui/PermissionForm.d.ts.map +1 -0
  41. package/dist/ui/PermissionForm.js +465 -0
  42. package/dist/ui/PresetMessages.d.ts +4 -0
  43. package/dist/ui/PresetMessages.d.ts.map +1 -0
  44. package/dist/ui/PresetMessages.js +33 -0
  45. package/dist/ui/VoiceModePanel.d.ts +3 -0
  46. package/dist/ui/VoiceModePanel.d.ts.map +1 -0
  47. package/dist/ui/VoiceModePanel.js +95 -0
  48. package/dist/ui/chatBotHeaderParts.d.ts +15 -0
  49. package/dist/ui/chatBotHeaderParts.d.ts.map +1 -0
  50. package/dist/ui/chatBotHeaderParts.js +50 -0
  51. package/dist/ui/index.d.ts +9 -0
  52. package/dist/ui/index.d.ts.map +1 -0
  53. package/dist/ui/index.js +8 -0
  54. package/dist/ui/styles/isolatedStyles.d.ts +73 -0
  55. package/dist/ui/styles/isolatedStyles.d.ts.map +1 -0
  56. package/dist/ui/styles/isolatedStyles.js +985 -0
  57. package/package.json +3 -3
  58. package/src/{legacy/chatbot/context → context}/ChatBotContext.tsx +0 -1
  59. package/src/index.ts +17 -40
  60. package/src/{legacy/chatbot/ui → ui}/ModelSelector.tsx +1 -1
  61. package/src/{legacy/chatbot/ui → ui}/VoiceModePanel.tsx +1 -1
  62. package/src/ui/index.ts +8 -0
  63. package/src/NxtlinqAgentChat.tsx +0 -79
  64. package/src/components/AgentAssistantShell.tsx +0 -104
  65. package/src/components/AgentComposer.tsx +0 -134
  66. package/src/components/AgentMessageList.tsx +0 -78
  67. package/src/components/AgentRemoteAudio.tsx +0 -34
  68. package/src/components/AgentVoiceBar.tsx +0 -173
  69. package/src/components/PresetMessageChips.tsx +0 -41
  70. package/src/context/AgentAssistantContext.tsx +0 -294
  71. package/src/legacy/index.ts +0 -26
  72. package/src/theme/defaultTheme.ts +0 -22
  73. package/src/types.ts +0 -65
  74. package/src/voice/useVoiceConnectOrchestration.ts +0 -117
  75. package/src/voice/useVoiceMicState.ts +0 -117
  76. package/src/voice/useVoiceTranscriptMessages.ts +0 -188
  77. package/src/voice/voiceMicConstants.ts +0 -13
  78. package/src/voice/voiceUserBubble.ts +0 -71
  79. /package/src/{legacy/chatbot/ChatBot.tsx → ChatBot.tsx} +0 -0
  80. /package/src/{legacy/assets → assets}/images/adiSideItalicDataUri.ts +0 -0
  81. /package/src/{legacy/chatbot/types → types}/ChatBotTypes.ts +0 -0
  82. /package/src/{legacy/chatbot/ui → ui}/BerifyMeModal.tsx +0 -0
  83. /package/src/{legacy/chatbot/ui → ui}/ChatBotHeader.tsx +0 -0
  84. /package/src/{legacy/chatbot/ui → ui}/ChatBotUI.tsx +0 -0
  85. /package/src/{legacy/chatbot/ui → ui}/MessageInput.tsx +0 -0
  86. /package/src/{legacy/chatbot/ui → ui}/MessageList.tsx +0 -0
  87. /package/src/{legacy/chatbot/ui → ui}/NotificationModal.tsx +0 -0
  88. /package/src/{legacy/chatbot/ui → ui}/PermissionForm.tsx +0 -0
  89. /package/src/{legacy/chatbot/ui → ui}/PresetMessages.tsx +0 -0
  90. /package/src/{legacy/chatbot/ui → ui}/chatBotHeaderParts.tsx +0 -0
  91. /package/src/{legacy/chatbot/ui → ui}/styles/isolatedStyles.ts +0 -0
@@ -0,0 +1,557 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { css } from '@emotion/react';
4
+ import * as React from 'react';
5
+ import { useDraggable, useLocalStorage, useResizable, walletTextUtils } from '@bytexbyte/nxtlinq-ai-agent-web-development';
6
+ import { useChatBot } from '../context/ChatBotContext';
7
+ import { ChatBotHeader } from './ChatBotHeader';
8
+ import { MessageInput } from './MessageInput';
9
+ import { MessageList } from './MessageList';
10
+ import { PermissionForm } from './PermissionForm';
11
+ import { PresetMessages } from './PresetMessages';
12
+ import { VoiceModePanel } from './VoiceModePanel';
13
+ import { chatWindow, errorToast, floatingButton, idvBanner, idvBannerText, idvBannerTitle, idvDismissButton, idvVerifyButton, infoToast, modalOverlay, resizeHandleNE, resizeHandleNW, resizeHandleSE, resizeHandleSW, sdkContainer, successToast, toastCloseButton, warningToast } from './styles/isolatedStyles';
14
+ const MOBILE_BREAKPOINT = 768;
15
+ const MOBILE_EDGE_MARGIN = 12;
16
+ const MOBILE_FAB_POSITION = { right: MOBILE_EDGE_MARGIN, bottom: MOBILE_EDGE_MARGIN };
17
+ const isMobileViewport = () => typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT;
18
+ // Toast Notification Component
19
+ const ToastNotification = ({ type, message, onClose, isChatOpen = false }) => {
20
+ const getToastStyles = () => {
21
+ const baseStyles = css `
22
+ ${isChatOpen ? css `
23
+ top: 20px !important;
24
+ right: 20px !important;
25
+ max-width: 480px !important;
26
+ ` : css `
27
+ bottom: 20px !important;
28
+ right: 20px !important;
29
+ `}
30
+ `;
31
+ switch (type) {
32
+ case 'success':
33
+ return css `${successToast} ${baseStyles}`;
34
+ case 'error':
35
+ return css `${errorToast} ${baseStyles}`;
36
+ case 'warning':
37
+ return css `${warningToast} ${baseStyles}`;
38
+ default:
39
+ return css `${infoToast} ${baseStyles}`;
40
+ }
41
+ };
42
+ const getIcon = () => {
43
+ switch (type) {
44
+ case 'success':
45
+ return '✅';
46
+ case 'error':
47
+ return '❌';
48
+ case 'warning':
49
+ return '⚠️';
50
+ default:
51
+ return 'ℹ️';
52
+ }
53
+ };
54
+ return (_jsxs("div", { css: getToastStyles(), children: [_jsx("span", { css: css `font-size: 18px !important;`, children: getIcon() }), _jsx("span", { css: css `flex: 1 !important;`, children: message }), _jsx("button", { onClick: onClose, css: toastCloseButton, children: "\u00D7" })] }));
55
+ };
56
+ export const ChatBotUI = () => {
57
+ const { isOpen, setIsOpen, showPermissionForm, setShowPermissionForm, notification, setNotification, isAITLoading, hitAddress, walletInfo, onVerifyWallet, serviceId, isNeedSignInWithWallet, piiDisplayMode, isVoiceMode, isVoiceConnecting, enterVoiceMode, exitVoiceMode, props } = useChatBot();
58
+ const positionRef = React.useRef({ x: 0, y: 0 });
59
+ const { dimensions, handleResizeStart, positionAdjustment } = useResizable(positionRef);
60
+ const { position, handleDragStart, isDragging, updatePosition } = useDraggable(dimensions);
61
+ const [mobileLayout, setMobileLayout] = React.useState(isMobileViewport);
62
+ React.useEffect(() => {
63
+ if (typeof window === 'undefined')
64
+ return;
65
+ const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
66
+ const sync = () => setMobileLayout(mq.matches);
67
+ sync();
68
+ mq.addEventListener('change', sync);
69
+ return () => mq.removeEventListener('change', sync);
70
+ }, []);
71
+ // Update position ref when position changes
72
+ React.useEffect(() => {
73
+ positionRef.current = position;
74
+ }, [position]);
75
+ // Update position when resizing to keep bottom-right corner fixed
76
+ React.useEffect(() => {
77
+ if (positionAdjustment) {
78
+ updatePosition(positionAdjustment);
79
+ }
80
+ }, [positionAdjustment, updatePosition]);
81
+ // IDV suggestion banner state
82
+ const [showIDVSuggestion, setShowIDVSuggestion] = React.useState(true);
83
+ const [dismissUntil, setDismissUntil] = React.useState(null);
84
+ const [timeRemaining, setTimeRemaining] = React.useState('');
85
+ const countdownIntervalRef = React.useRef(null);
86
+ // Check if there's a berifyme token in URL (indicating recent berifyme verification)
87
+ const urlParams = new URLSearchParams(window.location.search);
88
+ const hasBerifymeToken = urlParams.get('token') && urlParams.get('method') === 'berifyme';
89
+ const isWalletVerifiedWithBerifyme = walletInfo?.id && walletInfo?.method === 'berifyme';
90
+ // Helper function to update IDV suggestion state based on storage value
91
+ const updateIDVSuggestionState = React.useCallback((dismissedValue) => {
92
+ // Clear any existing countdown interval
93
+ if (countdownIntervalRef.current) {
94
+ clearInterval(countdownIntervalRef.current);
95
+ countdownIntervalRef.current = null;
96
+ }
97
+ if (dismissedValue) {
98
+ const dismissTime = parseInt(dismissedValue);
99
+ const now = Date.now();
100
+ const timeLeft = dismissTime - now;
101
+ if (timeLeft > 0) {
102
+ setShowIDVSuggestion(false);
103
+ setDismissUntil(dismissTime);
104
+ // Calculate initial time remaining immediately
105
+ const hours = Math.floor(timeLeft / (1000 * 60 * 60));
106
+ const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
107
+ setTimeRemaining(`${hours}h ${minutes}m`);
108
+ // Set up countdown interval to update time remaining every minute
109
+ countdownIntervalRef.current = setInterval(() => {
110
+ const remaining = dismissTime - Date.now();
111
+ if (remaining > 0) {
112
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
113
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
114
+ setTimeRemaining(`${hours}h ${minutes}m`);
115
+ }
116
+ else {
117
+ setShowIDVSuggestion(true);
118
+ setDismissUntil(null);
119
+ setTimeRemaining('');
120
+ localStorage.removeItem('idv-suggestion-dismissed');
121
+ if (countdownIntervalRef.current) {
122
+ clearInterval(countdownIntervalRef.current);
123
+ countdownIntervalRef.current = null;
124
+ }
125
+ }
126
+ }, 60000);
127
+ }
128
+ else {
129
+ localStorage.removeItem('idv-suggestion-dismissed');
130
+ setShowIDVSuggestion(true);
131
+ setDismissUntil(null);
132
+ setTimeRemaining('');
133
+ }
134
+ }
135
+ else {
136
+ setShowIDVSuggestion(true);
137
+ setDismissUntil(null);
138
+ setTimeRemaining('');
139
+ }
140
+ }, []);
141
+ // Check if IDV suggestion should be shown
142
+ React.useEffect(() => {
143
+ const shouldShowBanner = hitAddress &&
144
+ !props.requireWalletIDVVerification &&
145
+ !hasBerifymeToken &&
146
+ !isWalletVerifiedWithBerifyme &&
147
+ !isNeedSignInWithWallet;
148
+ if (shouldShowBanner) {
149
+ const timer = setTimeout(() => {
150
+ const shouldShowBannerAfterDelay = hitAddress &&
151
+ !props.requireWalletIDVVerification &&
152
+ !hasBerifymeToken &&
153
+ !isWalletVerifiedWithBerifyme &&
154
+ !isNeedSignInWithWallet;
155
+ if (shouldShowBannerAfterDelay) {
156
+ const dismissed = localStorage.getItem('idv-suggestion-dismissed');
157
+ updateIDVSuggestionState(dismissed);
158
+ }
159
+ else {
160
+ setShowIDVSuggestion(false);
161
+ setDismissUntil(null);
162
+ setTimeRemaining('');
163
+ }
164
+ }, 1000);
165
+ return () => {
166
+ clearTimeout(timer);
167
+ // Clear countdown interval on cleanup
168
+ if (countdownIntervalRef.current) {
169
+ clearInterval(countdownIntervalRef.current);
170
+ countdownIntervalRef.current = null;
171
+ }
172
+ };
173
+ }
174
+ else {
175
+ // Don't show when:
176
+ // - No wallet connected (no hitAddress)
177
+ // - Wallet is already verified (walletInfo?.id exists)
178
+ setShowIDVSuggestion(false);
179
+ setDismissUntil(null);
180
+ setTimeRemaining('');
181
+ }
182
+ }, [hitAddress, walletInfo, isNeedSignInWithWallet, props.requireWalletIDVVerification, updateIDVSuggestionState]);
183
+ const handleDismissIDV = () => {
184
+ const dismissSeconds = props.idvBannerDismissSeconds || 86400;
185
+ const dismissTime = Date.now() + (dismissSeconds * 1000);
186
+ localStorage.setItem('idv-suggestion-dismissed', dismissTime.toString());
187
+ setShowIDVSuggestion(false);
188
+ setDismissUntil(dismissTime);
189
+ // Calculate and set initial time remaining
190
+ const initialHours = Math.floor(dismissSeconds / 3600);
191
+ const initialMinutes = Math.floor((dismissSeconds % 3600) / 60);
192
+ setTimeRemaining(`${initialHours}h ${initialMinutes}m`);
193
+ // Set up countdown interval
194
+ if (countdownIntervalRef.current) {
195
+ clearInterval(countdownIntervalRef.current);
196
+ }
197
+ countdownIntervalRef.current = setInterval(() => {
198
+ const remaining = dismissTime - Date.now();
199
+ if (remaining > 0) {
200
+ const remainingHours = Math.floor(remaining / (1000 * 60 * 60));
201
+ const remainingMinutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
202
+ setTimeRemaining(`${remainingHours}h ${remainingMinutes}m`);
203
+ }
204
+ else {
205
+ setShowIDVSuggestion(true);
206
+ setDismissUntil(null);
207
+ setTimeRemaining('');
208
+ localStorage.removeItem('idv-suggestion-dismissed');
209
+ if (countdownIntervalRef.current) {
210
+ clearInterval(countdownIntervalRef.current);
211
+ countdownIntervalRef.current = null;
212
+ }
213
+ }
214
+ }, 60000);
215
+ const hours = Math.floor(dismissSeconds / 3600);
216
+ const minutes = Math.floor((dismissSeconds % 3600) / 60);
217
+ const timeText = hours > 0
218
+ ? (minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`)
219
+ : `${minutes}m`;
220
+ setNotification({
221
+ show: true,
222
+ type: 'info',
223
+ message: walletTextUtils.getWalletText(`IDV suggestion hidden for ${timeText}. You can still verify your wallet anytime.`, serviceId),
224
+ autoHide: true,
225
+ duration: 3000
226
+ });
227
+ };
228
+ // Add CSS animation for loading spinner
229
+ React.useEffect(() => {
230
+ const style = document.createElement('style');
231
+ style.textContent = `
232
+ @keyframes spin {
233
+ 0% { transform: rotate(0deg); }
234
+ 100% { transform: rotate(360deg); }
235
+ }
236
+ `;
237
+ document.head.appendChild(style);
238
+ return () => {
239
+ document.head.removeChild(style);
240
+ };
241
+ }, []);
242
+ const handleClose = () => {
243
+ if (typeof window !== 'undefined') {
244
+ if (mobileLayout) {
245
+ setButtonPosition(MOBILE_FAB_POSITION);
246
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
247
+ }
248
+ else {
249
+ const windowRight = window.innerWidth - (position.x + dimensions.width);
250
+ const windowBottom = window.innerHeight - (position.y + dimensions.height);
251
+ const newButtonPosition = clampButtonPosition({ right: windowRight, bottom: windowBottom });
252
+ setButtonPosition(newButtonPosition);
253
+ if (hasLoadedButtonPosition.current) {
254
+ setSavedButtonPosition(newButtonPosition);
255
+ }
256
+ }
257
+ }
258
+ setIsOpen(false);
259
+ };
260
+ const handleOpen = () => {
261
+ if (typeof window !== 'undefined') {
262
+ if (mobileLayout) {
263
+ updatePosition({ x: MOBILE_EDGE_MARGIN, y: MOBILE_EDGE_MARGIN });
264
+ }
265
+ else if (buttonRef.current) {
266
+ // Window's bottom-right corner aligns with the FAB
267
+ const buttonRect = buttonRef.current.getBoundingClientRect();
268
+ const buttonRight = window.innerWidth - buttonRect.right;
269
+ const buttonBottom = window.innerHeight - buttonRect.bottom;
270
+ const windowLeft = window.innerWidth - dimensions.width - buttonRight;
271
+ const windowTop = window.innerHeight - dimensions.height - buttonBottom;
272
+ const clampedLeft = Math.max(BUTTON_MARGIN, Math.min(windowLeft, window.innerWidth - dimensions.width - BUTTON_MARGIN));
273
+ const clampedTop = Math.max(BUTTON_MARGIN, Math.min(windowTop, window.innerHeight - dimensions.height - BUTTON_MARGIN));
274
+ updatePosition({ x: clampedLeft, y: clampedTop });
275
+ }
276
+ }
277
+ setIsOpen(true);
278
+ };
279
+ const handleCloseNotification = () => {
280
+ setNotification((prev) => ({ ...prev, show: false }));
281
+ };
282
+ // Auto-hide notification when autoHide is true and duration is set
283
+ React.useEffect(() => {
284
+ if (notification.show && notification.autoHide && notification.duration && notification.duration > 0) {
285
+ const timer = setTimeout(() => {
286
+ setNotification((prev) => ({ ...prev, show: false }));
287
+ }, notification.duration);
288
+ return () => clearTimeout(timer);
289
+ }
290
+ }, [notification.show, notification.autoHide, notification.duration, setNotification]);
291
+ const handleSettingsClick = () => {
292
+ setShowPermissionForm(true);
293
+ };
294
+ // Constants for button positioning
295
+ const BUTTON_MARGIN = 20;
296
+ const BUTTON_MIN_MARGIN = 5;
297
+ const DRAG_THRESHOLD = 5; // Minimum distance in pixels to distinguish drag from click
298
+ // Track window size for responsive button positioning and dragging
299
+ const buttonRef = React.useRef(null);
300
+ const buttonDragState = React.useRef(null);
301
+ const [isButtonDragging, setIsButtonDragging] = React.useState(false);
302
+ const [isButtonDragActive, setIsButtonDragActive] = React.useState(false);
303
+ const hasLoadedButtonPosition = React.useRef(false);
304
+ const buttonDragDistance = React.useRef(0);
305
+ // Track latest button position for saving (to avoid stale closure issues)
306
+ const latestButtonPositionRef = React.useRef({ right: BUTTON_MARGIN, bottom: BUTTON_MARGIN });
307
+ // Load saved position synchronously on initialization to avoid position jump
308
+ const getInitialButtonPosition = () => {
309
+ if (typeof window === 'undefined') {
310
+ return { right: BUTTON_MARGIN, bottom: BUTTON_MARGIN };
311
+ }
312
+ if (isMobileViewport()) {
313
+ return { ...MOBILE_FAB_POSITION };
314
+ }
315
+ try {
316
+ const saved = localStorage.getItem('ai-agent-button-position');
317
+ if (saved) {
318
+ const parsed = JSON.parse(saved);
319
+ if (parsed && typeof parsed.right === 'number' && typeof parsed.bottom === 'number') {
320
+ return parsed;
321
+ }
322
+ }
323
+ }
324
+ catch (e) {
325
+ // Ignore parse errors, use default
326
+ }
327
+ return { right: BUTTON_MARGIN, bottom: BUTTON_MARGIN };
328
+ };
329
+ const [, setSavedButtonPosition, isButtonPositionInitialized] = useLocalStorage('ai-agent-button-position', null);
330
+ const [buttonPosition, setButtonPosition] = React.useState(getInitialButtonPosition);
331
+ // Keep ref in sync with button position state and mark as loaded when initialized
332
+ React.useEffect(() => {
333
+ latestButtonPositionRef.current = buttonPosition;
334
+ if (isButtonPositionInitialized && !hasLoadedButtonPosition.current) {
335
+ hasLoadedButtonPosition.current = true;
336
+ }
337
+ }, [buttonPosition, isButtonPositionInitialized]);
338
+ // Fast clamp calculation helper (used in both drag and resize)
339
+ const clampButtonPositionFast = React.useCallback((right, bottom, buttonWidth, buttonHeight) => {
340
+ const viewportWidth = window.innerWidth;
341
+ const viewportHeight = window.innerHeight;
342
+ // Clamp horizontally
343
+ const maxRight = viewportWidth - buttonWidth - BUTTON_MARGIN;
344
+ let clampedRight = right;
345
+ if (clampedRight > maxRight) {
346
+ clampedRight = Math.max(BUTTON_MIN_MARGIN, maxRight);
347
+ }
348
+ else if (clampedRight < BUTTON_MARGIN) {
349
+ clampedRight = BUTTON_MARGIN;
350
+ }
351
+ // Clamp vertically
352
+ const maxBottom = viewportHeight - buttonHeight - BUTTON_MARGIN;
353
+ let clampedBottom = bottom;
354
+ if (clampedBottom > maxBottom) {
355
+ clampedBottom = Math.max(BUTTON_MIN_MARGIN, maxBottom);
356
+ }
357
+ else if (clampedBottom < BUTTON_MARGIN) {
358
+ clampedBottom = BUTTON_MARGIN;
359
+ }
360
+ return { right: clampedRight, bottom: clampedBottom };
361
+ }, []);
362
+ // Clamp button position to keep it within viewport
363
+ const clampButtonPosition = React.useCallback((pos, buttonWidth, buttonHeight) => {
364
+ if (typeof window === 'undefined') {
365
+ return pos;
366
+ }
367
+ // Use provided dimensions or get from DOM (avoid getBoundingClientRect during drag)
368
+ let width = buttonWidth;
369
+ let height = buttonHeight;
370
+ if (width === undefined || height === undefined) {
371
+ if (!buttonRef.current) {
372
+ return pos;
373
+ }
374
+ const rect = buttonRef.current.getBoundingClientRect();
375
+ width = rect.width;
376
+ height = rect.height;
377
+ }
378
+ return clampButtonPositionFast(pos.right, pos.bottom, width, height);
379
+ }, [clampButtonPositionFast]);
380
+ const handleButtonDragStart = React.useCallback((event) => {
381
+ if (mobileLayout) {
382
+ return;
383
+ }
384
+ event.stopPropagation();
385
+ if (buttonRef.current) {
386
+ // Cache button dimensions to avoid getBoundingClientRect during drag
387
+ const rect = buttonRef.current.getBoundingClientRect();
388
+ buttonDragState.current = {
389
+ startX: event.clientX,
390
+ startY: event.clientY,
391
+ startRight: buttonPosition.right,
392
+ startBottom: buttonPosition.bottom,
393
+ buttonWidth: rect.width,
394
+ buttonHeight: rect.height
395
+ };
396
+ buttonDragDistance.current = 0;
397
+ setIsButtonDragging(false);
398
+ setIsButtonDragActive(true);
399
+ document.body.style.userSelect = 'none';
400
+ }
401
+ }, [buttonPosition, mobileLayout]);
402
+ React.useEffect(() => {
403
+ if (!mobileLayout) {
404
+ return;
405
+ }
406
+ setButtonPosition(MOBILE_FAB_POSITION);
407
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
408
+ }, [mobileLayout]);
409
+ // Handle button drag move and end
410
+ React.useEffect(() => {
411
+ if (mobileLayout || !isButtonDragActive) {
412
+ return;
413
+ }
414
+ const handlePointerMove = (event) => {
415
+ if (!buttonDragState.current)
416
+ return;
417
+ const deltaX = event.clientX - buttonDragState.current.startX;
418
+ const deltaY = event.clientY - buttonDragState.current.startY;
419
+ const dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
420
+ buttonDragDistance.current = dragDistance;
421
+ // Only start dragging if moved more than threshold (to distinguish from click)
422
+ if (dragDistance > DRAG_THRESHOLD && !isButtonDragging) {
423
+ setIsButtonDragging(true);
424
+ document.body.style.cursor = 'grabbing';
425
+ }
426
+ if (dragDistance > DRAG_THRESHOLD) {
427
+ // Calculate new position (right decreases when moving right, bottom decreases when moving down)
428
+ const newRight = buttonDragState.current.startRight - deltaX;
429
+ const newBottom = buttonDragState.current.startBottom - deltaY;
430
+ // Use fast clamp calculation with cached dimensions
431
+ const clampedPos = clampButtonPositionFast(newRight, newBottom, buttonDragState.current.buttonWidth, buttonDragState.current.buttonHeight);
432
+ latestButtonPositionRef.current = clampedPos;
433
+ // Update state immediately (same as window drag) for responsive dragging
434
+ setButtonPosition(clampedPos);
435
+ }
436
+ };
437
+ const handlePointerUp = () => {
438
+ const wasDragging = buttonDragDistance.current > DRAG_THRESHOLD;
439
+ // Save position to localStorage if we were dragging
440
+ if (wasDragging && hasLoadedButtonPosition.current) {
441
+ setSavedButtonPosition(latestButtonPositionRef.current);
442
+ }
443
+ setIsButtonDragging(false);
444
+ setIsButtonDragActive(false);
445
+ buttonDragDistance.current = 0;
446
+ document.body.style.userSelect = '';
447
+ document.body.style.cursor = '';
448
+ buttonDragState.current = null;
449
+ };
450
+ document.addEventListener('pointermove', handlePointerMove);
451
+ document.addEventListener('pointerup', handlePointerUp);
452
+ return () => {
453
+ document.removeEventListener('pointermove', handlePointerMove);
454
+ document.removeEventListener('pointerup', handlePointerUp);
455
+ document.body.style.userSelect = '';
456
+ document.body.style.cursor = '';
457
+ };
458
+ }, [isButtonDragActive, isButtonDragging, setSavedButtonPosition, clampButtonPositionFast, mobileLayout]);
459
+ // Update button position on window resize to ensure it stays within viewport
460
+ React.useEffect(() => {
461
+ if (typeof window === 'undefined')
462
+ return;
463
+ const handleResize = () => {
464
+ requestAnimationFrame(() => {
465
+ if (mobileLayout) {
466
+ setButtonPosition(MOBILE_FAB_POSITION);
467
+ latestButtonPositionRef.current = MOBILE_FAB_POSITION;
468
+ return;
469
+ }
470
+ const clampedPos = clampButtonPosition(buttonPosition);
471
+ if (clampedPos.right !== buttonPosition.right || clampedPos.bottom !== buttonPosition.bottom) {
472
+ setButtonPosition(clampedPos);
473
+ }
474
+ });
475
+ };
476
+ window.addEventListener('resize', handleResize);
477
+ return () => window.removeEventListener('resize', handleResize);
478
+ }, [buttonPosition, clampButtonPosition, mobileLayout]);
479
+ const getButtonStyle = React.useMemo(() => {
480
+ if (mobileLayout) {
481
+ return css `
482
+ position: fixed !important;
483
+ bottom: calc(${MOBILE_EDGE_MARGIN}px + env(safe-area-inset-bottom, 0px)) !important;
484
+ right: calc(${MOBILE_EDGE_MARGIN}px + env(safe-area-inset-right, 0px)) !important;
485
+ left: auto !important;
486
+ top: auto !important;
487
+ touch-action: manipulation !important;
488
+ `;
489
+ }
490
+ return css `
491
+ position: fixed !important;
492
+ bottom: ${buttonPosition.bottom}px !important;
493
+ right: ${buttonPosition.right}px !important;
494
+ `;
495
+ }, [buttonPosition, mobileLayout]);
496
+ // Show floating button when chat is closed
497
+ if (!isOpen) {
498
+ return (_jsxs("div", { css: sdkContainer, children: [_jsx("button", { ref: buttonRef, onClick: (e) => {
499
+ // Don't open if we just finished dragging (moved more than threshold)
500
+ if (buttonDragDistance.current > DRAG_THRESHOLD) {
501
+ e.preventDefault();
502
+ e.stopPropagation();
503
+ return;
504
+ }
505
+ handleOpen();
506
+ }, onPointerDown: mobileLayout ? undefined : handleButtonDragStart, css: [floatingButton, getButtonStyle, css `
507
+ cursor: ${mobileLayout ? 'pointer' : isButtonDragging ? 'grabbing' : 'grab'} !important;
508
+ user-select: none !important;
509
+ `], title: mobileLayout ? 'Open AI Agent Chat' : 'Drag to move or click to open AI Agent Chat', children: "AI Agent" }), notification.show && (_jsx(ToastNotification, { type: notification.type, message: notification.message, onClose: handleCloseNotification, isChatOpen: isOpen }))] }));
510
+ }
511
+ return (_jsxs("div", { css: sdkContainer, children: [_jsxs("div", { css: [chatWindow, css `
512
+ width: ${dimensions.width}px !important;
513
+ height: ${dimensions.height}px !important;
514
+ left: ${position.x}px !important;
515
+ top: ${position.y}px !important;
516
+ bottom: auto !important;
517
+ right: auto !important;
518
+ ${mobileLayout ? css `
519
+ width: calc(100vw - ${MOBILE_EDGE_MARGIN * 2}px) !important;
520
+ height: calc(100dvh - ${MOBILE_EDGE_MARGIN * 2}px) !important;
521
+ max-width: calc(100vw - ${MOBILE_EDGE_MARGIN * 2}px) !important;
522
+ max-height: calc(100dvh - ${MOBILE_EDGE_MARGIN * 2}px) !important;
523
+ left: ${MOBILE_EDGE_MARGIN}px !important;
524
+ top: ${MOBILE_EDGE_MARGIN}px !important;
525
+ ` : ''}
526
+ `], children: [!mobileLayout && (_jsxs(_Fragment, { children: [_jsx("div", { css: resizeHandleNW, onPointerDown: handleResizeStart('nw'), title: "Resize", "aria-label": "Resize chat window from top-left" }), _jsx("div", { css: resizeHandleNE, onPointerDown: handleResizeStart('ne'), title: "Resize", "aria-label": "Resize chat window from top-right" }), _jsx("div", { css: resizeHandleSW, onPointerDown: handleResizeStart('sw'), title: "Resize", "aria-label": "Resize chat window from bottom-left" }), _jsx("div", { css: resizeHandleSE, onPointerDown: handleResizeStart('se'), title: "Resize", "aria-label": "Resize chat window from bottom-right" })] })), _jsx(ChatBotHeader, { mobileLayout: mobileLayout, isDragging: isDragging, onDragStart: handleDragStart, isVoiceMode: isVoiceMode, isVoiceConnecting: isVoiceConnecting, onVoiceToggle: () => void ((isVoiceMode || isVoiceConnecting) ? exitVoiceMode() : enterVoiceMode()), piiDisplayMode: piiDisplayMode, isAITLoading: isAITLoading, onSettingsClick: handleSettingsClick, onClose: handleClose }), showIDVSuggestion && hitAddress && !props.requireWalletIDVVerification && !hasBerifymeToken && !isWalletVerifiedWithBerifyme && (_jsxs("div", { "data-idv-banner": true, css: idvBanner, children: [_jsx("div", { css: css `
527
+ font-size: 20px !important;
528
+ color: #f39c12 !important;
529
+ `, children: "\uD83D\uDCA1" }), _jsxs("div", { css: css `flex: 1 !important;`, children: [_jsx("h5", { css: idvBannerTitle, children: walletTextUtils.getWalletText('Recommended: Verify Your Wallet', serviceId) }), _jsx("p", { css: idvBannerText, children: walletTextUtils.getWalletText('While not required, verifying your wallet with Berify.me provides additional security and trust for your AI agent interactions.', serviceId) }), _jsx("button", { onClick: () => onVerifyWallet('berifyme'), css: idvVerifyButton, children: walletTextUtils.getWalletText('Verify Wallet', serviceId) })] }), _jsx("button", { onClick: handleDismissIDV, css: idvDismissButton, title: `Hide for ${Math.floor((props.idvBannerDismissSeconds || 86400) / 3600)} hours`, children: "\u00D7" })] })), !showIDVSuggestion && dismissUntil && timeRemaining && (_jsxs("div", { css: css `
530
+ background-color: #f8f9fa !important;
531
+ border: 1px solid #e9ecef !important;
532
+ margin: 16px !important;
533
+ padding: 12px !important;
534
+ border-radius: 8px !important;
535
+ text-align: center !important;
536
+ font-size: 13px !important;
537
+ color: #6c757d !important;
538
+ `, children: [_jsxs("span", { children: ["\uD83D\uDCA1 IDV suggestion hidden. Will show again in ", timeRemaining] }), _jsx("button", { onClick: () => {
539
+ localStorage.removeItem('idv-suggestion-dismissed');
540
+ setShowIDVSuggestion(true);
541
+ setDismissUntil(null);
542
+ setTimeRemaining('');
543
+ }, css: css `
544
+ background: none !important;
545
+ border: none !important;
546
+ color: #007bff !important;
547
+ font-size: 12px !important;
548
+ cursor: pointer !important;
549
+ margin-left: 8px !important;
550
+ text-decoration: underline !important;
551
+ `, title: "Show now", children: "Show now" })] })), _jsx(MessageList, {}), !isVoiceMode && _jsx(PresetMessages, {}), isVoiceMode ? _jsx(VoiceModePanel, {}) : _jsx(MessageInput, {})] }), showPermissionForm && (_jsx("div", { css: modalOverlay, onClick: (e) => {
552
+ // Close modal when clicking on background
553
+ if (e.target === e.currentTarget) {
554
+ setShowPermissionForm(false);
555
+ }
556
+ }, children: _jsx(PermissionForm, { onClose: () => setShowPermissionForm(false) }) })), notification.show && (_jsx(ToastNotification, { type: notification.type, message: notification.message, onClose: handleCloseNotification, isChatOpen: isOpen }))] }));
557
+ };
@@ -0,0 +1,3 @@
1
+ import * as React from 'react';
2
+ export declare const MessageInput: React.FC;
3
+ //# sourceMappingURL=MessageInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageInput.d.ts","sourceRoot":"","sources":["../../src/ui/MessageInput.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAofhC,CAAC"}