@bytexbyte/nxtlinq-ai-agent-sdk 1.6.31 → 1.6.33

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.
@@ -1,19 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
2
  /** @jsxImportSource @emotion/react */
3
- import * as React from 'react';
4
3
  import { css } from '@emotion/react';
4
+ import * as React from 'react';
5
+ import { useDraggable } from '../../core/lib/useDraggable';
6
+ import useLocalStorage from '../../core/lib/useLocalStorage';
7
+ import { useResizable } from '../../core/lib/useResizable';
8
+ import * as walletTextUtils from '../../core/utils/walletTextUtils';
5
9
  import { useChatBot } from '../context/ChatBotContext';
6
- import { PermissionForm } from './PermissionForm';
7
- import { MessageList } from './MessageList';
8
10
  import { MessageInput } from './MessageInput';
9
- import { PresetMessages } from './PresetMessages';
11
+ import { MessageList } from './MessageList';
10
12
  import { ModelSelector } from './ModelSelector';
11
- import * as walletTextUtils from '../../core/utils/walletTextUtils';
12
- import { useResizable } from '../../core/lib/useResizable';
13
- import { useDraggable } from '../../core/lib/useDraggable';
14
- import useLocalStorage from '../../core/lib/useLocalStorage';
15
- import { sdkContainer, floatingButton, chatWindow, chatHeader, headerTitle, headerButton, closeButton, modalOverlay, idvBanner, idvBannerTitle, idvBannerText, idvVerifyButton, idvDismissButton, loadingSpinner, successToast, errorToast, warningToast, infoToast, toastCloseButton } from './styles/isolatedStyles';
16
- import { resizeHandle } from './styles/isolatedStyles';
13
+ import { PermissionForm } from './PermissionForm';
14
+ import { PresetMessages } from './PresetMessages';
15
+ import { chatHeader, chatWindow, closeButton, errorToast, floatingButton, headerButton, headerTitle, idvBanner, idvBannerText, idvBannerTitle, idvDismissButton, idvVerifyButton, infoToast, loadingSpinner, modalOverlay, resizeHandle, sdkContainer, successToast, toastCloseButton, warningToast } from './styles/isolatedStyles';
17
16
  // Toast Notification Component
18
17
  const ToastNotification = ({ type, message, onClose, isChatOpen = false }) => {
19
18
  const getToastStyles = () => {
@@ -71,10 +70,62 @@ export const ChatBotUI = () => {
71
70
  const [showIDVSuggestion, setShowIDVSuggestion] = React.useState(true);
72
71
  const [dismissUntil, setDismissUntil] = React.useState(null);
73
72
  const [timeRemaining, setTimeRemaining] = React.useState('');
73
+ const countdownIntervalRef = React.useRef(null);
74
74
  // Check if there's a berifyme token in URL (indicating recent berifyme verification)
75
75
  const urlParams = new URLSearchParams(window.location.search);
76
76
  const hasBerifymeToken = urlParams.get('token') && urlParams.get('method') === 'berifyme';
77
77
  const isWalletVerifiedWithBerifyme = walletInfo?.id && walletInfo?.method === 'berifyme';
78
+ // Helper function to update IDV suggestion state based on storage value
79
+ const updateIDVSuggestionState = React.useCallback((dismissedValue) => {
80
+ // Clear any existing countdown interval
81
+ if (countdownIntervalRef.current) {
82
+ clearInterval(countdownIntervalRef.current);
83
+ countdownIntervalRef.current = null;
84
+ }
85
+ if (dismissedValue) {
86
+ const dismissTime = parseInt(dismissedValue);
87
+ const now = Date.now();
88
+ const timeLeft = dismissTime - now;
89
+ if (timeLeft > 0) {
90
+ setShowIDVSuggestion(false);
91
+ setDismissUntil(dismissTime);
92
+ // Calculate initial time remaining immediately
93
+ const hours = Math.floor(timeLeft / (1000 * 60 * 60));
94
+ const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
95
+ setTimeRemaining(`${hours}h ${minutes}m`);
96
+ // Set up countdown interval to update time remaining every minute
97
+ countdownIntervalRef.current = setInterval(() => {
98
+ const remaining = dismissTime - Date.now();
99
+ if (remaining > 0) {
100
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
101
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
102
+ setTimeRemaining(`${hours}h ${minutes}m`);
103
+ }
104
+ else {
105
+ setShowIDVSuggestion(true);
106
+ setDismissUntil(null);
107
+ setTimeRemaining('');
108
+ localStorage.removeItem('idv-suggestion-dismissed');
109
+ if (countdownIntervalRef.current) {
110
+ clearInterval(countdownIntervalRef.current);
111
+ countdownIntervalRef.current = null;
112
+ }
113
+ }
114
+ }, 60000);
115
+ }
116
+ else {
117
+ localStorage.removeItem('idv-suggestion-dismissed');
118
+ setShowIDVSuggestion(true);
119
+ setDismissUntil(null);
120
+ setTimeRemaining('');
121
+ }
122
+ }
123
+ else {
124
+ setShowIDVSuggestion(true);
125
+ setDismissUntil(null);
126
+ setTimeRemaining('');
127
+ }
128
+ }, []);
78
129
  // Check if IDV suggestion should be shown
79
130
  React.useEffect(() => {
80
131
  const shouldShowBanner = hitAddress &&
@@ -91,42 +142,7 @@ export const ChatBotUI = () => {
91
142
  !isNeedSignInWithWallet;
92
143
  if (shouldShowBannerAfterDelay) {
93
144
  const dismissed = localStorage.getItem('idv-suggestion-dismissed');
94
- if (dismissed) {
95
- const dismissTime = parseInt(dismissed);
96
- const now = Date.now();
97
- const timeLeft = dismissTime - now;
98
- if (timeLeft > 0) {
99
- setShowIDVSuggestion(false);
100
- setDismissUntil(dismissTime);
101
- const countdownTimer = setInterval(() => {
102
- const remaining = dismissTime - Date.now();
103
- if (remaining > 0) {
104
- const hours = Math.floor(remaining / (1000 * 60 * 60));
105
- const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
106
- setTimeRemaining(`${hours}h ${minutes}m`);
107
- }
108
- else {
109
- setShowIDVSuggestion(true);
110
- setDismissUntil(null);
111
- setTimeRemaining('');
112
- localStorage.removeItem('idv-suggestion-dismissed');
113
- clearInterval(countdownTimer);
114
- }
115
- }, 60000);
116
- return () => clearInterval(countdownTimer);
117
- }
118
- else {
119
- localStorage.removeItem('idv-suggestion-dismissed');
120
- setShowIDVSuggestion(true);
121
- setDismissUntil(null);
122
- setTimeRemaining('');
123
- }
124
- }
125
- else {
126
- setShowIDVSuggestion(true);
127
- setDismissUntil(null);
128
- setTimeRemaining('');
129
- }
145
+ updateIDVSuggestionState(dismissed);
130
146
  }
131
147
  else {
132
148
  setShowIDVSuggestion(false);
@@ -134,7 +150,14 @@ export const ChatBotUI = () => {
134
150
  setTimeRemaining('');
135
151
  }
136
152
  }, 1000);
137
- return () => clearTimeout(timer);
153
+ return () => {
154
+ clearTimeout(timer);
155
+ // Clear countdown interval on cleanup
156
+ if (countdownIntervalRef.current) {
157
+ clearInterval(countdownIntervalRef.current);
158
+ countdownIntervalRef.current = null;
159
+ }
160
+ };
138
161
  }
139
162
  else {
140
163
  // Don't show when:
@@ -144,13 +167,39 @@ export const ChatBotUI = () => {
144
167
  setDismissUntil(null);
145
168
  setTimeRemaining('');
146
169
  }
147
- }, [hitAddress, walletInfo, isNeedSignInWithWallet, props.requireWalletIDVVerification]);
170
+ }, [hitAddress, walletInfo, isNeedSignInWithWallet, props.requireWalletIDVVerification, updateIDVSuggestionState]);
148
171
  const handleDismissIDV = () => {
149
172
  const dismissSeconds = props.idvBannerDismissSeconds || 86400;
150
173
  const dismissTime = Date.now() + (dismissSeconds * 1000);
151
174
  localStorage.setItem('idv-suggestion-dismissed', dismissTime.toString());
152
175
  setShowIDVSuggestion(false);
153
176
  setDismissUntil(dismissTime);
177
+ // Calculate and set initial time remaining
178
+ const initialHours = Math.floor(dismissSeconds / 3600);
179
+ const initialMinutes = Math.floor((dismissSeconds % 3600) / 60);
180
+ setTimeRemaining(`${initialHours}h ${initialMinutes}m`);
181
+ // Set up countdown interval
182
+ if (countdownIntervalRef.current) {
183
+ clearInterval(countdownIntervalRef.current);
184
+ }
185
+ countdownIntervalRef.current = setInterval(() => {
186
+ const remaining = dismissTime - Date.now();
187
+ if (remaining > 0) {
188
+ const remainingHours = Math.floor(remaining / (1000 * 60 * 60));
189
+ const remainingMinutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
190
+ setTimeRemaining(`${remainingHours}h ${remainingMinutes}m`);
191
+ }
192
+ else {
193
+ setShowIDVSuggestion(true);
194
+ setDismissUntil(null);
195
+ setTimeRemaining('');
196
+ localStorage.removeItem('idv-suggestion-dismissed');
197
+ if (countdownIntervalRef.current) {
198
+ clearInterval(countdownIntervalRef.current);
199
+ countdownIntervalRef.current = null;
200
+ }
201
+ }
202
+ }, 60000);
154
203
  const hours = Math.floor(dismissSeconds / 3600);
155
204
  const minutes = Math.floor((dismissSeconds % 3600) / 60);
156
205
  const timeText = hours > 0
@@ -1 +1 @@
1
- {"version":3,"file":"MessageInput.d.ts","sourceRoot":"","sources":["../../../src/components/ui/MessageInput.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EA2LhC,CAAC"}
1
+ {"version":3,"file":"MessageInput.d.ts","sourceRoot":"","sources":["../../../src/components/ui/MessageInput.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EA6ehC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
2
2
  /** @jsxImportSource @emotion/react */
3
3
  import { css } from '@emotion/react';
4
4
  import MicIcon from '@mui/icons-material/Mic';
@@ -6,19 +6,178 @@ import MicOffIcon from '@mui/icons-material/MicOff';
6
6
  import SendIcon from '@mui/icons-material/Send';
7
7
  import VolumeUpIcon from '@mui/icons-material/VolumeUp';
8
8
  import VolumeOffIcon from '@mui/icons-material/VolumeOff';
9
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
10
+ import CloseIcon from '@mui/icons-material/Close';
9
11
  import { IconButton, InputBase, Tooltip } from '@mui/material';
12
+ import * as React from 'react';
10
13
  import { useChatBot } from '../context/ChatBotContext';
11
14
  import * as walletTextUtils from '../../core/utils/walletTextUtils';
12
15
  import { actionButton } from './styles/isolatedStyles';
13
16
  export const MessageInput = () => {
14
- const { inputValue, setInputValue, isLoading, isAITLoading, handleSubmit, isMicEnabled, isAwaitingMicGesture, startRecording, stopRecording, textInputRef, autoSendEnabled, setAutoSendEnabled, textToSpeechEnabled, setTextToSpeechEnabled, serviceId, props: { placeholder = 'Type a message...' } } = useChatBot();
15
- const isDisabled = isLoading || isAITLoading;
17
+ const { inputValue, setInputValue, isLoading, isAITLoading, handleSubmit, uploadAttachment, showError, isMicEnabled, isAwaitingMicGesture, startRecording, stopRecording, textInputRef, autoSendEnabled, setAutoSendEnabled, textToSpeechEnabled, setTextToSpeechEnabled, serviceId, getCurrentModel, props: { placeholder = 'Type a message...' } } = useChatBot();
18
+ const [pendingAttachments, setPendingAttachments] = React.useState([]);
19
+ const [isUploading, setIsUploading] = React.useState(false);
20
+ const fileInputRef = React.useRef(null);
21
+ // Check if current model is llama (upload not supported)
22
+ const currentModel = getCurrentModel();
23
+ const isLlamaModel = currentModel.value === 'llama' || currentModel.value.includes('llama');
24
+ // Clear pending attachments when switching to llama model
25
+ React.useEffect(() => {
26
+ if (isLlamaModel && pendingAttachments.length > 0) {
27
+ setPendingAttachments([]);
28
+ // Also clear the file input
29
+ if (fileInputRef.current) {
30
+ fileInputRef.current.value = '';
31
+ }
32
+ }
33
+ }, [isLlamaModel, pendingAttachments.length]);
34
+ const isDisabled = isLoading || isAITLoading || isUploading;
16
35
  const inputPlaceholder = isAITLoading ? walletTextUtils.getWalletText('Loading wallet configuration...', serviceId) : placeholder;
36
+ const hasContent = React.useCallback(() => (textInputRef.current?.value ?? inputValue).trim().length > 0 || pendingAttachments.length > 0, [inputValue, pendingAttachments.length]);
37
+ const doSubmit = React.useCallback(async (e) => {
38
+ if (!hasContent() || isDisabled)
39
+ return;
40
+ let attachmentsToSend = [];
41
+ if (pendingAttachments.length > 0) {
42
+ setIsUploading(true);
43
+ const uploadErrors = [];
44
+ try {
45
+ for (const { attachment, file } of pendingAttachments) {
46
+ try {
47
+ const res = await uploadAttachment(file);
48
+ if ('error' in res) {
49
+ uploadErrors.push(`${attachment.name}: ${res.error || '上傳失敗'}`);
50
+ continue; // Continue with other files instead of returning
51
+ }
52
+ attachmentsToSend.push({
53
+ type: attachment.type,
54
+ url: res.url,
55
+ name: attachment.name,
56
+ mimeType: attachment.mimeType,
57
+ size: attachment.size,
58
+ });
59
+ }
60
+ catch (err) {
61
+ const errorMessage = err instanceof Error ? err.message : '上傳失敗';
62
+ uploadErrors.push(`${attachment.name}: ${errorMessage}`);
63
+ console.error(`Failed to upload ${attachment.name}:`, err);
64
+ }
65
+ }
66
+ // Show errors if any files failed, but continue if some succeeded
67
+ if (uploadErrors.length > 0) {
68
+ if (attachmentsToSend.length === 0) {
69
+ // All files failed
70
+ showError(`所有檔案上傳失敗:\n${uploadErrors.join('\n')}`);
71
+ return;
72
+ }
73
+ else {
74
+ // Some files succeeded, some failed
75
+ showError(`部分檔案上傳失敗:\n${uploadErrors.join('\n')}\n\n已成功上傳 ${attachmentsToSend.length} 個檔案,將繼續發送。`);
76
+ }
77
+ }
78
+ }
79
+ finally {
80
+ setIsUploading(false);
81
+ }
82
+ }
83
+ setPendingAttachments([]);
84
+ await handleSubmit(e, attachmentsToSend);
85
+ }, [
86
+ hasContent,
87
+ pendingAttachments,
88
+ isDisabled,
89
+ uploadAttachment,
90
+ showError,
91
+ handleSubmit,
92
+ ]);
17
93
  const handleKeyPress = (e) => {
18
94
  if (e.key === 'Enter' && !e.shiftKey) {
19
95
  e.preventDefault();
20
- handleSubmit(e);
96
+ const syntheticEvent = {
97
+ preventDefault: () => e.preventDefault(),
98
+ stopPropagation: () => e.stopPropagation(),
99
+ nativeEvent: e.nativeEvent,
100
+ currentTarget: e.currentTarget,
101
+ target: e.target,
102
+ bubbles: e.bubbles,
103
+ cancelable: e.cancelable,
104
+ defaultPrevented: e.defaultPrevented,
105
+ eventPhase: e.eventPhase,
106
+ isTrusted: e.isTrusted,
107
+ timeStamp: e.timeStamp,
108
+ type: 'submit',
109
+ };
110
+ void doSubmit(syntheticEvent);
111
+ }
112
+ };
113
+ const fileToDataURL = (file) => {
114
+ return new Promise((resolve, reject) => {
115
+ const reader = new FileReader();
116
+ reader.onload = () => resolve(reader.result);
117
+ reader.onerror = reject;
118
+ reader.readAsDataURL(file);
119
+ });
120
+ };
121
+ const getFileType = (mimeType) => {
122
+ if (mimeType.startsWith('image/'))
123
+ return 'image';
124
+ return 'file';
125
+ };
126
+ const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB - matches backend limit
127
+ const handleFileSelect = async (e) => {
128
+ const files = e.target.files;
129
+ if (!files || files.length === 0)
130
+ return;
131
+ const newPending = [];
132
+ const errors = [];
133
+ for (let i = 0; i < files.length; i++) {
134
+ const file = files[i];
135
+ // Check file size before processing
136
+ if (file.size > MAX_FILE_SIZE) {
137
+ errors.push(`${file.name}: 檔案大小超過限制(最大 20MB)`);
138
+ continue;
139
+ }
140
+ // Check if file size is exactly at limit (should be < not <=)
141
+ if (file.size >= MAX_FILE_SIZE) {
142
+ errors.push(`${file.name}: 檔案大小超過限制(最大 20MB)`);
143
+ continue;
144
+ }
145
+ try {
146
+ const dataUrl = await fileToDataURL(file);
147
+ const type = getFileType(file.type);
148
+ newPending.push({
149
+ attachment: {
150
+ type,
151
+ url: dataUrl,
152
+ name: file.name,
153
+ mimeType: file.type,
154
+ size: file.size,
155
+ },
156
+ file,
157
+ });
158
+ }
159
+ catch (err) {
160
+ const errorMessage = err instanceof Error ? err.message : '讀取檔案失敗';
161
+ errors.push(`${file.name}: ${errorMessage}`);
162
+ console.error(`Error reading file ${file.name}:`, err);
163
+ }
164
+ }
165
+ // Show errors if any, but still add successfully processed files
166
+ if (errors.length > 0) {
167
+ showError(`部分檔案無法上傳:\n${errors.join('\n')}`);
21
168
  }
169
+ // Add successfully processed files
170
+ if (newPending.length > 0) {
171
+ setPendingAttachments(prev => [...prev, ...newPending]);
172
+ }
173
+ if (fileInputRef.current)
174
+ fileInputRef.current.value = '';
175
+ };
176
+ const removeAttachment = (index) => {
177
+ setPendingAttachments(prev => prev.filter((_, i) => i !== index));
178
+ };
179
+ const handleSubmitWithAttachments = (e) => {
180
+ void doSubmit(e);
22
181
  };
23
182
  return (_jsxs(_Fragment, { children: [isAwaitingMicGesture && (_jsx("div", { role: "status", "aria-live": "polite", css: css `
24
183
  margin: 12px 15px 0 !important;
@@ -34,13 +193,74 @@ export const MessageInput = () => {
34
193
  display: block !important;
35
194
  visibility: visible !important;
36
195
  opacity: 1 !important;
37
- `, children: "\u26A0\uFE0F The microphone needs a user interaction to re-enable. Please click on the page or press any key." })), _jsxs("div", { css: css `
196
+ `, children: "\u26A0\uFE0F The microphone needs a user interaction to re-enable. Please click on the page or press any key." })), pendingAttachments.length > 0 && (_jsx("div", { css: css `
197
+ padding: 10px 15px 0 !important;
198
+ display: flex !important;
199
+ flex-wrap: wrap !important;
200
+ gap: 8px !important;
201
+ border-top: 1px solid #eee !important;
202
+ `, children: pendingAttachments.map(({ attachment }, index) => (_jsxs("div", { css: css `
203
+ position: relative !important;
204
+ display: inline-block !important;
205
+ border: 1px solid #ddd !important;
206
+ border-radius: 8px !important;
207
+ overflow: hidden !important;
208
+ background: #f5f5f5 !important;
209
+ `, children: [attachment.type === 'image' && (_jsx("img", { src: attachment.url, alt: attachment.name, css: css `
210
+ max-width: 100px !important;
211
+ max-height: 100px !important;
212
+ object-fit: cover !important;
213
+ display: block !important;
214
+ ` })), attachment.type === 'file' && (_jsxs("div", { css: css `
215
+ padding: 20px 10px !important;
216
+ text-align: center !important;
217
+ min-width: 100px !important;
218
+ min-height: 100px !important;
219
+ display: flex !important;
220
+ align-items: center !important;
221
+ justify-content: center !important;
222
+ flex-direction: column !important;
223
+ `, children: [_jsx(AttachFileIcon, { css: css `font-size: 32px !important; color: #666 !important;` }), _jsx("span", { css: css `
224
+ font-size: 11px !important;
225
+ color: #666 !important;
226
+ margin-top: 4px !important;
227
+ word-break: break-word !important;
228
+ max-width: 80px !important;
229
+ `, children: attachment.name })] })), _jsx("button", { onClick: () => removeAttachment(index), css: css `
230
+ position: absolute !important;
231
+ top: 4px !important;
232
+ right: 4px !important;
233
+ background: rgba(0, 0, 0, 0.6) !important;
234
+ border: none !important;
235
+ border-radius: 50% !important;
236
+ width: 20px !important;
237
+ height: 20px !important;
238
+ display: flex !important;
239
+ align-items: center !important;
240
+ justify-content: center !important;
241
+ cursor: pointer !important;
242
+ padding: 0 !important;
243
+ color: white !important;
244
+ font-size: 12px !important;
245
+
246
+ &:hover {
247
+ background: rgba(0, 0, 0, 0.8) !important;
248
+ }
249
+ `, children: _jsx(CloseIcon, { fontSize: "small" }) })] }, index))) })), _jsxs("div", { css: css `
38
250
  padding: 15px !important;
39
251
  display: flex !important;
40
252
  align-items: center !important;
41
253
  gap: 10px !important;
42
254
  border-top: 1px solid #eee !important;
43
- `, children: [_jsx(InputBase, { value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyPress: handleKeyPress, placeholder: inputPlaceholder, fullWidth: true, inputProps: {
255
+ `, children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: "image/*,.pdf,.doc,.docx,.txt,.csv,.xlsx,.xls", onChange: handleFileSelect, css: css `
256
+ display: none !important;
257
+ ` }), _jsx(Tooltip, { title: isLlamaModel
258
+ ? 'Llama model does not support file upload. Please switch to another model to use this feature.'
259
+ : 'Upload file', children: _jsx("span", { children: _jsx(IconButton, { onClick: () => !isLlamaModel && fileInputRef.current?.click(), disabled: isDisabled || isLlamaModel, css: css `
260
+ padding: 8px !important;
261
+ color: ${isDisabled || isLlamaModel ? '#ccc' : '#666'} !important;
262
+ cursor: ${isLlamaModel ? 'not-allowed' : 'pointer'} !important;
263
+ `, children: _jsx(AttachFileIcon, {}) }) }) }), _jsx(InputBase, { value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyPress: handleKeyPress, placeholder: inputPlaceholder, fullWidth: true, inputProps: {
44
264
  ref: textInputRef
45
265
  }, endAdornment: _jsxs(_Fragment, { children: [_jsx(Tooltip, { title: textToSpeechEnabled ? 'Text-to-speech enabled' : 'Text-to-speech disabled', children: _jsx(IconButton, { size: "small", onClick: (e) => {
46
266
  e.stopPropagation();
@@ -66,7 +286,7 @@ export const MessageInput = () => {
66
286
  background-color: #fff !important;
67
287
  height: 40px !important;
68
288
  box-sizing: border-box !important;
69
- ` }), _jsxs("button", { onClick: (e) => handleSubmit(e), disabled: isDisabled || !inputValue.trim(), css: css `
289
+ ` }), _jsxs("button", { onClick: handleSubmitWithAttachments, disabled: isDisabled || !hasContent(), css: css `
70
290
  ${actionButton}
71
291
  padding: 10px 20px !important;
72
292
  border-radius: 20px !important;
@@ -86,7 +306,7 @@ export const MessageInput = () => {
86
306
  &:hover:not(:disabled) {
87
307
  background-color: #0056b3 !important;
88
308
  }
89
- `, children: ["Send", (isLoading || isAITLoading) && (_jsx("span", { css: css `
309
+ `, children: ["Send", (isLoading || isAITLoading || isUploading) && (_jsx("span", { css: css `
90
310
  margin-left: 8px !important;
91
311
  display: flex !important;
92
312
  align-items: center !important;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/ui/MessageList.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAkC/B,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAsO/B,CAAC"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/ui/MessageList.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAmC/B,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EA0T/B,CAAC"}
@@ -5,6 +5,7 @@ import { css } from '@emotion/react';
5
5
  import { convertUrlsToLinks } from '../../core/utils/urlUtils';
6
6
  import * as walletTextUtils from '../../core/utils/walletTextUtils';
7
7
  import { useChatBot } from '../context/ChatBotContext';
8
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
8
9
  import { messageListContainer, messageBubble, userMessage, messageContent, userMessageContent, retryMessageContent, chatbotButton, connectedButton, loadingIndicator, modelIndicator, modelBadge, modelDot, streamingCaret, streamingContainer, streamingHeader, streamingIcon, streamingToolName, streamingProgressPercent, streamingProgressContainer, streamingProgressBar, streamingPartialText, streamingStatus, streamingStepsContainer, streamingStepItem, streamingStepCheck } from './styles/isolatedStyles';
9
10
  export const MessageList = () => {
10
11
  const { messages, isLoading, isTtsProcessing, requiresGesture, retryTtsWithGesture, connectWallet, signInWallet, hitAddress, isAutoConnecting, isNeedSignInWithWallet, enableAIT, isAITLoading, isAITEnabling, sendMessage, permissions, availableModels, serviceId } = useChatBot();
@@ -66,7 +67,49 @@ export const MessageList = () => {
66
67
  ? userMessageContent
67
68
  : message.metadata?.isRetry
68
69
  ? retryMessageContent
69
- : messageContent, children: [message.metadata?.isRetry && (_jsx("span", { css: css `margin-right: 8px !important; font-size: 14px !important;`, children: "\uD83D\uDD04" })), message.isStreaming && message.partialContent && (_jsxs("div", { css: streamingPartialText, children: [message.role === 'assistant' ? convertUrlsToLinks(message.partialContent) : message.partialContent, _jsx("span", { css: streamingCaret, children: "\u258A" })] })), message.isStreaming && !message.partialContent && message.role === 'assistant' && (_jsxs("div", { css: streamingContainer, children: [_jsxs("div", { css: streamingHeader, children: [_jsx("span", { css: streamingIcon, children: "\uD83D\uDD27" }), _jsx("span", { css: streamingToolName, children: message.streamingToolName || 'Processing' }), message.streamingProgress !== undefined && (_jsxs("span", { css: streamingProgressPercent, children: [message.streamingProgress, "%"] }))] }), message.streamingProgress !== undefined && (_jsx("div", { css: streamingProgressContainer, children: _jsx("div", { css: [streamingProgressBar, css `width: ${message.streamingProgress}% !important;`] }) })), message.streamingStatus && (_jsx("div", { css: streamingStatus, children: message.streamingStatus })), message.streamingSteps && message.streamingSteps.length > 0 && (_jsx("div", { css: streamingStepsContainer, children: message.streamingSteps.map((step, idx) => (_jsxs("div", { css: streamingStepItem, children: [_jsx("span", { css: streamingStepCheck, children: "\u2713" }), _jsx("span", { children: step })] }, idx))) }))] })), !message.isStreaming && (message.role === 'assistant' ? convertUrlsToLinks(message.content) : message.content), message.button && (_jsx("div", { css: css `margin-top: 10px !important;`, children: _jsx("button", { onClick: () => {
70
+ : messageContent, children: [message.metadata?.isRetry && (_jsx("span", { css: css `margin-right: 8px !important; font-size: 14px !important;`, children: "\uD83D\uDD04" })), message.attachments && message.attachments.length > 0 && (_jsx("div", { css: css `
71
+ display: flex !important;
72
+ flex-wrap: wrap !important;
73
+ gap: 8px !important;
74
+ margin-bottom: ${message.content ? '10px' : '0'} !important;
75
+ `, children: message.attachments.map((attachment, idx) => (_jsxs("div", { css: css `
76
+ position: relative !important;
77
+ display: inline-block !important;
78
+ border: 1px solid #ddd !important;
79
+ border-radius: 8px !important;
80
+ overflow: hidden !important;
81
+ background: #f5f5f5 !important;
82
+ max-width: 200px !important;
83
+ `, children: [attachment.type === 'image' && (_jsx("img", { src: attachment.url, alt: attachment.name, css: css `
84
+ max-width: 200px !important;
85
+ max-height: 200px !important;
86
+ object-fit: cover !important;
87
+ display: block !important;
88
+ cursor: pointer !important;
89
+ `, onClick: () => {
90
+ // Open image in new window on click
91
+ const newWindow = window.open();
92
+ if (newWindow) {
93
+ newWindow.document.write(`<img src="${attachment.url}" style="max-width: 100%; height: auto;" />`);
94
+ }
95
+ } })), attachment.type === 'file' && (_jsxs("div", { css: css `
96
+ padding: 20px 15px !important;
97
+ text-align: center !important;
98
+ min-width: 150px !important;
99
+ display: flex !important;
100
+ align-items: center !important;
101
+ justify-content: center !important;
102
+ flex-direction: column !important;
103
+ gap: 8px !important;
104
+ `, children: [_jsx(AttachFileIcon, { css: css `font-size: 32px !important; color: #666 !important;` }), _jsx("span", { css: css `
105
+ font-size: 12px !important;
106
+ color: #666 !important;
107
+ word-break: break-word !important;
108
+ max-width: 120px !important;
109
+ `, children: attachment.name }), attachment.size && (_jsxs("span", { css: css `
110
+ font-size: 10px !important;
111
+ color: #999 !important;
112
+ `, children: [(attachment.size / 1024).toFixed(1), " KB"] }))] }))] }, idx))) })), message.isStreaming && message.partialContent && (_jsxs("div", { css: streamingPartialText, children: [message.role === 'assistant' ? convertUrlsToLinks(message.partialContent) : message.partialContent, _jsx("span", { css: streamingCaret, children: "\u258A" })] })), message.isStreaming && !message.partialContent && message.role === 'assistant' && (_jsxs("div", { css: streamingContainer, children: [_jsxs("div", { css: streamingHeader, children: [_jsx("span", { css: streamingIcon, children: "\uD83D\uDD27" }), _jsx("span", { css: streamingToolName, children: message.streamingToolName || 'Processing' }), message.streamingProgress !== undefined && (_jsxs("span", { css: streamingProgressPercent, children: [message.streamingProgress, "%"] }))] }), message.streamingProgress !== undefined && (_jsx("div", { css: streamingProgressContainer, children: _jsx("div", { css: [streamingProgressBar, css `width: ${message.streamingProgress}% !important;`] }) })), message.streamingStatus && (_jsx("div", { css: streamingStatus, children: message.streamingStatus })), message.streamingSteps && message.streamingSteps.length > 0 && (_jsx("div", { css: streamingStepsContainer, children: message.streamingSteps.map((step, idx) => (_jsxs("div", { css: streamingStepItem, children: [_jsx("span", { css: streamingStepCheck, children: "\u2713" }), _jsx("span", { children: step })] }, idx))) }))] })), !message.isStreaming && (message.role === 'assistant' ? convertUrlsToLinks(message.content) : message.content), message.button && (_jsx("div", { css: css `margin-top: 10px !important;`, children: _jsx("button", { onClick: () => {
70
113
  if (message.button && message.button.trim()) {
71
114
  handleButtonClick(message.button, message);
72
115
  }
@@ -1,3 +1,11 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
+ /**
3
+ * Custom hook for managing localStorage with React state and cross-tab synchronization
4
+ * Automatically syncs changes across browser tabs using StorageEvent
5
+ *
6
+ * @param key - The key to store the value under in localStorage
7
+ * @param defaultValue - The default value to use if no value is stored
8
+ * @returns [storedValue, setStoredValue, isInitialized]
9
+ */
2
10
  export default function useLocalStorage<T>(key: string, defaultValue: T): [T, Dispatch<SetStateAction<T>>, boolean];
3
11
  //# sourceMappingURL=useLocalStorage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLocalStorage.d.ts","sourceRoot":"","sources":["../../../src/core/lib/useLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAuB,MAAM,OAAO,CAAC;AAEtE,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAoBlH"}
1
+ {"version":3,"file":"useLocalStorage.d.ts","sourceRoot":"","sources":["../../../src/core/lib/useLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAA+B,MAAM,OAAO,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CA8ElH"}