@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.
- package/dist/api/nxtlinq-api.d.ts.map +1 -1
- package/dist/api/nxtlinq-api.js +76 -1
- package/dist/assets/images/adilasDogHeadTiltDataUri.d.ts +2 -0
- package/dist/assets/images/adilasDogHeadTiltDataUri.d.ts.map +1 -0
- package/dist/assets/images/adilasDogHeadTiltDataUri.js +1 -0
- package/dist/components/context/ChatBotContext.d.ts.map +1 -1
- package/dist/components/context/ChatBotContext.js +79 -25
- package/dist/components/types/ChatBotTypes.d.ts +7 -2
- package/dist/components/types/ChatBotTypes.d.ts.map +1 -1
- package/dist/components/ui/ChatBotUI.d.ts +0 -1
- package/dist/components/ui/ChatBotUI.d.ts.map +1 -1
- package/dist/components/ui/ChatBotUI.js +97 -48
- package/dist/components/ui/MessageInput.d.ts.map +1 -1
- package/dist/components/ui/MessageInput.js +228 -8
- package/dist/components/ui/MessageList.d.ts.map +1 -1
- package/dist/components/ui/MessageList.js +44 -1
- package/dist/core/lib/useLocalStorage.d.ts +8 -0
- package/dist/core/lib/useLocalStorage.d.ts.map +1 -1
- package/dist/core/lib/useLocalStorage.js +71 -6
- package/dist/types/ait-api.d.ts +44 -0
- package/dist/types/ait-api.d.ts.map +1 -1
- package/package.json +3 -2
- package/umd/nxtlinq-ai-agent.umd.js +177 -177
- package/umd/nxtlinq-ai-agent.umd.js.LICENSE.txt +4 -9
|
@@ -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 {
|
|
11
|
+
import { MessageList } from './MessageList';
|
|
10
12
|
import { ModelSelector } from './ModelSelector';
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
|
|
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 () =>
|
|
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":"
|
|
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,
|
|
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
|
|
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
|
-
|
|
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." })),
|
|
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(
|
|
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:
|
|
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;
|
|
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.
|
|
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,
|
|
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"}
|