funda-ui 4.5.713 → 4.5.766
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/Chatbox/index.d.ts +54 -0
- package/Chatbox/index.js +4863 -0
- package/README.md +1 -1
- package/Utils/useStreamController.d.ts +71 -0
- package/Utils/useStreamController.js +494 -0
- package/all.d.ts +1 -0
- package/all.js +1 -0
- package/lib/cjs/Chatbox/index.d.ts +54 -0
- package/lib/cjs/Chatbox/index.js +4863 -0
- package/lib/cjs/Utils/useStreamController.d.ts +71 -0
- package/lib/cjs/Utils/useStreamController.js +494 -0
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/esm/Chatbox/PureLoader.tsx +45 -0
- package/lib/esm/Chatbox/TypingEffect.tsx +32 -0
- package/lib/esm/Chatbox/index.scss +502 -0
- package/lib/esm/Chatbox/index.tsx +1171 -0
- package/lib/esm/Chatbox/useStreamController.tsx +277 -0
- package/lib/esm/Chatbox/utils/func.ts +107 -0
- package/lib/esm/Textarea/index.tsx +1 -0
- package/lib/esm/Utils/hooks/useStreamController.tsx +277 -0
- package/lib/esm/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useEffect, useState, useRef, useImperativeHandle } from "react";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import Textarea from 'funda-textarea';
|
|
6
|
+
import RootPortal from 'funda-root-portal';
|
|
7
|
+
|
|
8
|
+
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
9
|
+
import useDebounce from 'funda-utils/dist/cjs/useDebounce';
|
|
10
|
+
import useThrottle from 'funda-utils/dist/cjs/useThrottle';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// loader
|
|
15
|
+
import PureLoader from './PureLoader';
|
|
16
|
+
import TypingEffect from "./TypingEffect";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
isValidJSON,
|
|
20
|
+
formatLatestDisplayContent,
|
|
21
|
+
formatName,
|
|
22
|
+
fixHtmlTags
|
|
23
|
+
} from './utils/func';
|
|
24
|
+
|
|
25
|
+
import useStreamController from './useStreamController';
|
|
26
|
+
|
|
27
|
+
export type MessageDetail = {
|
|
28
|
+
sender: string; // Sender's name
|
|
29
|
+
timestamp: string; // Time when the message was sent
|
|
30
|
+
content: string; // The message content
|
|
31
|
+
tag: string; // such as '[reply]'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface FloatingButton {
|
|
35
|
+
label: string; // HTML string
|
|
36
|
+
value: string;
|
|
37
|
+
onClick: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RequestConfig {
|
|
41
|
+
apiUrl: string;
|
|
42
|
+
requestBody: string; // JSON string for request body template
|
|
43
|
+
responseExtractor: string; // JSON path to extract response
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ChatboxProps = {
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
prefix?: string;
|
|
49
|
+
contentRef?: React.RefObject<any>;
|
|
50
|
+
model?: string;
|
|
51
|
+
baseUrl?: string;
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
defaultMessages?: MessageDetail[];
|
|
54
|
+
verbose?: boolean;
|
|
55
|
+
reasoningSwitchLabel?: string;
|
|
56
|
+
stopLabel?: React.ReactNode;
|
|
57
|
+
questionName?: React.ReactNode;
|
|
58
|
+
answerName?: React.ReactNode;
|
|
59
|
+
questionNameIcon?: string;
|
|
60
|
+
answerNameIcon?: string;
|
|
61
|
+
bubble?: boolean;
|
|
62
|
+
bubbleLabel?: string;
|
|
63
|
+
sendLabel?: string;
|
|
64
|
+
sendLoading?: boolean;
|
|
65
|
+
sendLoadingLabel?: string;
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
noDataPlaceholder?: string;
|
|
68
|
+
requestConfig: RequestConfig;
|
|
69
|
+
headerConfig?: any;
|
|
70
|
+
maxHistoryLength?: number;
|
|
71
|
+
contextData?: Record<string, any>; // Dynamic JSON data
|
|
72
|
+
toolkitButtons?: FloatingButton[];
|
|
73
|
+
newChatButton?: FloatingButton;
|
|
74
|
+
renderParser?: (input: string) => Promise<string>;
|
|
75
|
+
requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => any;
|
|
76
|
+
nameFormatter?: (input: string) => string;
|
|
77
|
+
onInputChange?: (controlRef: React.RefObject<any>, val: string) => any;
|
|
78
|
+
onChunk?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
|
|
79
|
+
onComplete?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
const Chatbox = (props: ChatboxProps) => {
|
|
84
|
+
|
|
85
|
+
const chatId = useComId().replace(/\-/g, '_');
|
|
86
|
+
|
|
87
|
+
// Store latest props in refs
|
|
88
|
+
const propsRef = useRef<any>(props);
|
|
89
|
+
|
|
90
|
+
// Store context data in ref to get latest values
|
|
91
|
+
const contextDataRef = useRef<Record<string, any> | undefined>(props.contextData);
|
|
92
|
+
|
|
93
|
+
// Store config in ref to get latest values
|
|
94
|
+
const configRef = useRef<RequestConfig>({
|
|
95
|
+
apiUrl: "{baseUrl}/v1/chat/completions",
|
|
96
|
+
requestBody: `{
|
|
97
|
+
"model": "{model}",
|
|
98
|
+
"messages": [{
|
|
99
|
+
"role": "user",
|
|
100
|
+
"content": "{message}"
|
|
101
|
+
}],
|
|
102
|
+
"stream": true
|
|
103
|
+
}`,
|
|
104
|
+
responseExtractor: "data.choices.0.delta.content"
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
//
|
|
109
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
110
|
+
const msgContainerRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const msInput = useRef<HTMLTextAreaElement>(null);
|
|
112
|
+
const inputContentRef = useRef<any>(null);
|
|
113
|
+
|
|
114
|
+
const [loaderDisplay, setLoaderDisplay] = useState<boolean>(false);
|
|
115
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
116
|
+
const [thinking, setThinking] = useState<boolean>(false);
|
|
117
|
+
const [show, setShow] = useState<boolean>(false);
|
|
118
|
+
const [msgList, setMsgList] = useState<MessageDetail[]>([]);
|
|
119
|
+
const [elapsedTime, setElapsedTime] = useState<number>(0);
|
|
120
|
+
const [tempAnimText, setTempAnimText] = useState<string>('');
|
|
121
|
+
|
|
122
|
+
//
|
|
123
|
+
const timer = useRef<any>(null);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
//================================================================
|
|
127
|
+
// helper
|
|
128
|
+
//================================================================
|
|
129
|
+
const exposedMethods = () => {
|
|
130
|
+
return {
|
|
131
|
+
chatOpen: () => {
|
|
132
|
+
setShow(true);
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (msInput.current) msInput.current.focus();
|
|
135
|
+
}, 0);
|
|
136
|
+
},
|
|
137
|
+
chatClose: () => {
|
|
138
|
+
setShow(false);
|
|
139
|
+
},
|
|
140
|
+
clearData: () => {
|
|
141
|
+
setMsgList([]);
|
|
142
|
+
conversationHistory.current = [];
|
|
143
|
+
},
|
|
144
|
+
sendMsg: () => {
|
|
145
|
+
handleClickSafe();
|
|
146
|
+
},
|
|
147
|
+
getHistory: () => conversationHistory.current,
|
|
148
|
+
trimHistory: (length?: number) => {
|
|
149
|
+
const maxLength = length || args().maxHistoryLength || 20;
|
|
150
|
+
if (conversationHistory.current.length > maxLength) {
|
|
151
|
+
conversationHistory.current = conversationHistory.current.slice(-maxLength);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
setVal: (v: string) => {
|
|
155
|
+
if (inputContentRef.current) inputContentRef.current.set(v);
|
|
156
|
+
},
|
|
157
|
+
setContextData: (v: Record<string, any>) => {
|
|
158
|
+
contextDataRef.current = v;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const scrollToBottom = useThrottle(() => {
|
|
165
|
+
if (msgContainerRef.current) {
|
|
166
|
+
msgContainerRef.current.scrollTop = msgContainerRef.current.scrollHeight;
|
|
167
|
+
}
|
|
168
|
+
}, 300, []);
|
|
169
|
+
|
|
170
|
+
const args = () => {
|
|
171
|
+
const currentProps = propsRef.current;
|
|
172
|
+
if (typeof currentProps.headerConfig === 'undefined' || typeof configRef.current.apiUrl === 'undefined') {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const {
|
|
177
|
+
debug,
|
|
178
|
+
prefix,
|
|
179
|
+
contentRef,
|
|
180
|
+
model,
|
|
181
|
+
baseUrl,
|
|
182
|
+
apiKey,
|
|
183
|
+
verbose,
|
|
184
|
+
reasoningSwitchLabel,
|
|
185
|
+
stopLabel,
|
|
186
|
+
questionName,
|
|
187
|
+
answerName,
|
|
188
|
+
bubble,
|
|
189
|
+
bubbleLabel,
|
|
190
|
+
sendLabel,
|
|
191
|
+
sendLoading,
|
|
192
|
+
sendLoadingLabel,
|
|
193
|
+
placeholder,
|
|
194
|
+
noDataPlaceholder,
|
|
195
|
+
requestConfig,
|
|
196
|
+
headerConfig,
|
|
197
|
+
toolkitButtons,
|
|
198
|
+
newChatButton,
|
|
199
|
+
maxHistoryLength,
|
|
200
|
+
renderParser,
|
|
201
|
+
requestBodyFormatter,
|
|
202
|
+
nameFormatter,
|
|
203
|
+
onInputChange,
|
|
204
|
+
onChunk,
|
|
205
|
+
onComplete,
|
|
206
|
+
} = currentProps;
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
const {
|
|
210
|
+
apiUrl,
|
|
211
|
+
requestBody,
|
|
212
|
+
responseExtractor
|
|
213
|
+
} = configRef.current;
|
|
214
|
+
|
|
215
|
+
const latestContextData = contextDataRef.current ? contextDataRef.current : undefined;
|
|
216
|
+
|
|
217
|
+
let _requestBodyTmpl = requestBody.replace(/\'/g, '"'); // !!! REQUIRED !!!
|
|
218
|
+
let _isStream: boolean = true;
|
|
219
|
+
|
|
220
|
+
// request API
|
|
221
|
+
const requestApiUrl = apiUrl.replace(/\{baseUrl\}/g, baseUrl);
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// header config
|
|
225
|
+
const _headerConfig = headerConfig.replace(/\{apiKey\}/g, apiKey)
|
|
226
|
+
.replace(/\'/g, '"'); // !!! REQUIRED !!!
|
|
227
|
+
const headerConfigRes = typeof _headerConfig !== 'undefined' ? (isValidJSON(_headerConfig) ? JSON.parse(_headerConfig) : undefined) : {'Content-Type':'application/json'};
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
// Determine whether it is in JSON format
|
|
231
|
+
if (!isValidJSON(_requestBodyTmpl)) {
|
|
232
|
+
console.log('--> [ERROR] Wrong JSON format');
|
|
233
|
+
_requestBodyTmpl = '{}';
|
|
234
|
+
return {};
|
|
235
|
+
} else {
|
|
236
|
+
_isStream = JSON.parse(_requestBodyTmpl).hasOwnProperty('stream') && JSON.parse(_requestBodyTmpl).stream === true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Whether or not to show reasoning
|
|
240
|
+
const withReasoning = typeof verbose === 'undefined' ? true : verbose;
|
|
241
|
+
|
|
242
|
+
// Get latest name values
|
|
243
|
+
const _answerName: string = formatName(answerName, true, currentProps);
|
|
244
|
+
const _questionName: string = formatName(questionName, false, currentProps);
|
|
245
|
+
|
|
246
|
+
// Responder deconstruction
|
|
247
|
+
const responseExtractPath = responseExtractor.split('.');
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
debug,
|
|
251
|
+
prefix,
|
|
252
|
+
contentRef,
|
|
253
|
+
model,
|
|
254
|
+
baseUrl,
|
|
255
|
+
apiKey,
|
|
256
|
+
verbose,
|
|
257
|
+
reasoningSwitchLabel,
|
|
258
|
+
stopLabel,
|
|
259
|
+
bubble,
|
|
260
|
+
bubbleLabel,
|
|
261
|
+
sendLabel,
|
|
262
|
+
sendLoading,
|
|
263
|
+
sendLoadingLabel,
|
|
264
|
+
placeholder,
|
|
265
|
+
noDataPlaceholder,
|
|
266
|
+
requestConfig,
|
|
267
|
+
maxHistoryLength,
|
|
268
|
+
toolkitButtons,
|
|
269
|
+
newChatButton,
|
|
270
|
+
renderParser,
|
|
271
|
+
requestBodyFormatter,
|
|
272
|
+
nameFormatter,
|
|
273
|
+
onInputChange,
|
|
274
|
+
onChunk,
|
|
275
|
+
onComplete,
|
|
276
|
+
|
|
277
|
+
//
|
|
278
|
+
latestContextData,
|
|
279
|
+
questionNameRes: _questionName,
|
|
280
|
+
answerNameRes: _answerName,
|
|
281
|
+
isStream: _isStream,
|
|
282
|
+
headerConfigRes,
|
|
283
|
+
requestApiUrl,
|
|
284
|
+
requestBodyTmpl: _requestBodyTmpl,
|
|
285
|
+
responseExtractPath,
|
|
286
|
+
withReasoning,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
//================================================================
|
|
293
|
+
// Custom buttons
|
|
294
|
+
//================================================================
|
|
295
|
+
const [activeButtons, setActiveButtons] = useState<Record<string, boolean>>({});
|
|
296
|
+
const executeButtonAction = (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
|
|
297
|
+
try {
|
|
298
|
+
// Create a new function to execute
|
|
299
|
+
const actionFn = new Function('method', 'isActive', 'button', actionStr);
|
|
300
|
+
/*
|
|
301
|
+
function (method, isActive, button) {
|
|
302
|
+
console.log('Clearing chat');
|
|
303
|
+
method.clearData();
|
|
304
|
+
}
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
// Update the button status
|
|
308
|
+
const newState = !activeButtons[buttonId];
|
|
309
|
+
setActiveButtons(prev => ({
|
|
310
|
+
...prev,
|
|
311
|
+
[buttonId]: newState
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
return actionFn(exposedMethods(), newState, buttonElement);
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('Error executing button action:', error);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
//================================================================
|
|
323
|
+
// Conversation History
|
|
324
|
+
//================================================================
|
|
325
|
+
const conversationHistory = useRef<Array<MessageDetail>>([]);
|
|
326
|
+
const updateConversationHistory = (newMessage: MessageDetail) => {
|
|
327
|
+
const maxLength = args().maxHistoryLength || 20;
|
|
328
|
+
|
|
329
|
+
// Add new messages to your history
|
|
330
|
+
conversationHistory.current.push(newMessage);
|
|
331
|
+
|
|
332
|
+
// If the maximum length is exceeded, the oldest record is deleted
|
|
333
|
+
if (conversationHistory.current.length > maxLength) {
|
|
334
|
+
const removeCount = conversationHistory.current.length - maxLength;
|
|
335
|
+
conversationHistory.current = conversationHistory.current.slice(removeCount);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
//================================================================
|
|
342
|
+
// normal request
|
|
343
|
+
//================================================================
|
|
344
|
+
const abortController = useRef<any>(new AbortController()); // DO NOT USE "useState()"
|
|
345
|
+
|
|
346
|
+
const abortNormalRequest = () => {
|
|
347
|
+
console.log('--> Abort stream');
|
|
348
|
+
abortController.current.abort();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const reconnectNormalRequest = () => {
|
|
352
|
+
console.log('--> Reconnect stream');
|
|
353
|
+
abortController.current = new AbortController();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
//================================================================
|
|
358
|
+
// stream controller
|
|
359
|
+
//================================================================
|
|
360
|
+
const abortStream = () => {
|
|
361
|
+
console.log('--> Abort stream');
|
|
362
|
+
streamController.abort();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// parse chunk data
|
|
366
|
+
const parseChunkData = async (chunk: string, index: number, complete: boolean) => {
|
|
367
|
+
|
|
368
|
+
// Store the final content and bind it to loading
|
|
369
|
+
let lastContent: string = '';
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
// Extract response using the path
|
|
375
|
+
const extractPath = args().responseExtractPath?.slice(1);
|
|
376
|
+
|
|
377
|
+
// Streaming data is JSON split by rows
|
|
378
|
+
const lines = chunk.split("\n").filter(line => line.trim() !== "");
|
|
379
|
+
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
|
|
382
|
+
// debug
|
|
383
|
+
if (args().debug && index < 10 && !complete) {
|
|
384
|
+
console.log(`--> (${index}) ${line}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Send the streamed data to the front end
|
|
388
|
+
if (line.indexOf('[DONE]') < 0) {
|
|
389
|
+
|
|
390
|
+
// STEP 1:
|
|
391
|
+
// ------
|
|
392
|
+
// Create a JSON string
|
|
393
|
+
const _content = `${line.replace(/^data:\s*/, '')}`;
|
|
394
|
+
|
|
395
|
+
// Determine whether it is in JSON format
|
|
396
|
+
if (!isValidJSON(_content)) {
|
|
397
|
+
console.log('--> [ERROR] Wrong JSON format');
|
|
398
|
+
|
|
399
|
+
//reset SSE
|
|
400
|
+
closeSSE();
|
|
401
|
+
break; // Exit the loop
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// STEP 2:
|
|
405
|
+
// ------
|
|
406
|
+
// Response body
|
|
407
|
+
let result = JSON.parse(_content);
|
|
408
|
+
|
|
409
|
+
//*******
|
|
410
|
+
// for Ollama API (STREAM END)
|
|
411
|
+
//*******
|
|
412
|
+
if (typeof result.done !== 'undefined') {
|
|
413
|
+
if (result.done === true) {
|
|
414
|
+
console.log('--> [DONE]');
|
|
415
|
+
|
|
416
|
+
//reset SSE
|
|
417
|
+
closeSSE();
|
|
418
|
+
break; // Exit the loop
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
//*******
|
|
423
|
+
// for OpenAI API
|
|
424
|
+
//*******
|
|
425
|
+
if (extractPath) {
|
|
426
|
+
for (const path of extractPath) {
|
|
427
|
+
result = result[path];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let content = result;
|
|
432
|
+
|
|
433
|
+
// STEP 3:
|
|
434
|
+
// ------
|
|
435
|
+
// 🚀 !! IMPORTANT: Skip the error content
|
|
436
|
+
if (typeof content === 'undefined') {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// STEP 4:
|
|
442
|
+
// ------
|
|
443
|
+
// Update thinking state
|
|
444
|
+
if (content.includes('<think>')) {
|
|
445
|
+
setThinking(true);
|
|
446
|
+
}
|
|
447
|
+
if (content.includes('</think>')) {
|
|
448
|
+
setThinking(false);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
// STEP 5:
|
|
453
|
+
// ------
|
|
454
|
+
// Replace with a valid label
|
|
455
|
+
content = fixHtmlTags(content, args().withReasoning, args().reasoningSwitchLabel);
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
// STEP 6:
|
|
460
|
+
// ------
|
|
461
|
+
// By updating the stream text, you can update the UI
|
|
462
|
+
tempLastContent.current += content;
|
|
463
|
+
lastContent += content;
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
// STEP 7:
|
|
467
|
+
// ------
|
|
468
|
+
let parsedContent = tempLastContent.current;
|
|
469
|
+
|
|
470
|
+
// If a render parser exists, it is used to process the string
|
|
471
|
+
if (typeof args().renderParser === 'function') {
|
|
472
|
+
parsedContent = await args().renderParser(parsedContent);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// STEP 8:
|
|
477
|
+
// ------
|
|
478
|
+
// Real-time output
|
|
479
|
+
if (args().withReasoning) {
|
|
480
|
+
setTempAnimText(formatLatestDisplayContent(parsedContent));
|
|
481
|
+
} else {
|
|
482
|
+
if (!thinking) {
|
|
483
|
+
setTempAnimText(formatLatestDisplayContent(parsedContent));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// STEP 9:
|
|
488
|
+
// ------
|
|
489
|
+
// Scroll to the bottom
|
|
490
|
+
scrollToBottom();
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
} else {
|
|
494
|
+
console.log('--> [DONE]');
|
|
495
|
+
|
|
496
|
+
//reset SSE
|
|
497
|
+
closeSSE();
|
|
498
|
+
|
|
499
|
+
break; // Exit the loop
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error('--> Error processing chunk:', error);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
let latestRes = complete ? lastContent : tempLastContent.current;
|
|
510
|
+
|
|
511
|
+
// If a render parser exists, it is used to process the string
|
|
512
|
+
if (typeof args().renderParser === 'function') {
|
|
513
|
+
latestRes = await args().renderParser(latestRes);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return formatLatestDisplayContent(latestRes);
|
|
517
|
+
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Store the final content and bind it to loading
|
|
521
|
+
const tempLastContent = useRef<string>('');
|
|
522
|
+
const streamController = useStreamController({
|
|
523
|
+
onChunk: async (chunk: string, index: number) => {
|
|
524
|
+
|
|
525
|
+
// start (Execute it only once)
|
|
526
|
+
if (index === 0) {
|
|
527
|
+
// hide loader
|
|
528
|
+
setLoaderDisplay(false);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
//
|
|
532
|
+
const res = await parseChunkData(chunk, index, false);
|
|
533
|
+
|
|
534
|
+
//
|
|
535
|
+
args().onChunk?.(inputContentRef.current, res, conversationHistory.current);
|
|
536
|
+
},
|
|
537
|
+
onComplete: async (lastContent: string) => {
|
|
538
|
+
console.log('--> Stream complete');
|
|
539
|
+
|
|
540
|
+
const res = await parseChunkData(lastContent, 0 , true);
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
// Display AI reply
|
|
544
|
+
displayMessage(args().answerNameRes, res);
|
|
545
|
+
|
|
546
|
+
//
|
|
547
|
+
args().onComplete?.(inputContentRef.current, res, conversationHistory.current);
|
|
548
|
+
|
|
549
|
+
//
|
|
550
|
+
closeSSE();
|
|
551
|
+
},
|
|
552
|
+
onError: (error) => {
|
|
553
|
+
console.error('--> Stream error:', error);
|
|
554
|
+
closeSSE();
|
|
555
|
+
},
|
|
556
|
+
onAbort: () => {
|
|
557
|
+
console.log('--> Stream aborted');
|
|
558
|
+
closeSSE();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
//================================================================
|
|
564
|
+
// Core
|
|
565
|
+
//================================================================
|
|
566
|
+
const closeSSE = () => {
|
|
567
|
+
|
|
568
|
+
// reset
|
|
569
|
+
setTempAnimText('');
|
|
570
|
+
tempLastContent.current = '';
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
// Stop the timer
|
|
574
|
+
clearInterval(timer.current);
|
|
575
|
+
timer.current = null;
|
|
576
|
+
|
|
577
|
+
// loading
|
|
578
|
+
setLoading(false);
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
const displayMessage = (sender: string | undefined, content: string) => {
|
|
583
|
+
const timestamp = new Date().toLocaleTimeString(); // Get the current time
|
|
584
|
+
const tag = sender === args().answerNameRes ? '[reply]' : '';
|
|
585
|
+
|
|
586
|
+
const newMessage: MessageDetail = {
|
|
587
|
+
sender: sender || '',
|
|
588
|
+
timestamp,
|
|
589
|
+
content,
|
|
590
|
+
tag
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// update messages history
|
|
594
|
+
updateConversationHistory(newMessage);
|
|
595
|
+
|
|
596
|
+
// Update the message list state
|
|
597
|
+
setMsgList((prevMessages) => [...prevMessages, newMessage]);
|
|
598
|
+
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const sendMessage = async () => {
|
|
602
|
+
if (rootRef.current === null || msgContainerRef.current === null || msInput.current === null) return;
|
|
603
|
+
|
|
604
|
+
const messageInput: any = msInput.current;
|
|
605
|
+
const message = messageInput.value;
|
|
606
|
+
|
|
607
|
+
if (message.trim() === '') {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
// Start the timer
|
|
613
|
+
setElapsedTime(0); // Reset elapsed time
|
|
614
|
+
timer.current = setInterval(() => {
|
|
615
|
+
setElapsedTime((prev) => prev + 1); // Increment elapsed time every second
|
|
616
|
+
}, 1000);
|
|
617
|
+
|
|
618
|
+
// user message
|
|
619
|
+
|
|
620
|
+
let inputMsg = `${message}`;
|
|
621
|
+
// If a render parser exists, it is used to process the string
|
|
622
|
+
if (typeof args().renderParser === 'function') {
|
|
623
|
+
inputMsg = await args().renderParser(inputMsg);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
displayMessage(args().questionNameRes, inputMsg); // Display user message
|
|
627
|
+
|
|
628
|
+
// loading
|
|
629
|
+
setLoading(true);
|
|
630
|
+
|
|
631
|
+
// show loader
|
|
632
|
+
setLoaderDisplay(true);
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
// clear
|
|
636
|
+
if (inputContentRef.current) inputContentRef.current.clear();
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
const res: any = await mainRequest(message);
|
|
640
|
+
|
|
641
|
+
// reply (normal)
|
|
642
|
+
//======================
|
|
643
|
+
if (!args().isStream) {
|
|
644
|
+
const reply = res.reply;
|
|
645
|
+
let replyRes = `${reply}`;
|
|
646
|
+
|
|
647
|
+
// If a render parser exists, it is used to process the string
|
|
648
|
+
if (typeof args().renderParser === 'function') {
|
|
649
|
+
replyRes = await args().renderParser(replyRes);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
displayMessage(args().answerNameRes, replyRes); // Display AI reply
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
//
|
|
656
|
+
args().onChunk?.(inputContentRef.current, replyRes, conversationHistory.current);
|
|
657
|
+
args().onComplete?.(inputContentRef.current, replyRes, conversationHistory.current);
|
|
658
|
+
|
|
659
|
+
//reset SSE
|
|
660
|
+
closeSSE();
|
|
661
|
+
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
} catch (error) {
|
|
666
|
+
|
|
667
|
+
// loading
|
|
668
|
+
setLoading(false);
|
|
669
|
+
|
|
670
|
+
// Stop the timer
|
|
671
|
+
clearInterval(timer.current);
|
|
672
|
+
timer.current = null;
|
|
673
|
+
|
|
674
|
+
console.error('--> Error sending message:', error);
|
|
675
|
+
displayMessage(args().answerNameRes, `Error: Unable to send message: ${String(error)}`); // Display AI reply
|
|
676
|
+
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// clear
|
|
680
|
+
messageInput.value = '';
|
|
681
|
+
|
|
682
|
+
// reset textarea height
|
|
683
|
+
if (inputContentRef.current) inputContentRef.current.resetHeight();
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
// Scroll to the bottom
|
|
687
|
+
scrollToBottom();
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const handleClickSafe = useDebounce(() => {
|
|
691
|
+
sendMessage();
|
|
692
|
+
}, 300, []);
|
|
693
|
+
|
|
694
|
+
const handleClose = (e: React.MouseEvent) => {
|
|
695
|
+
e.preventDefault();
|
|
696
|
+
e.stopPropagation();
|
|
697
|
+
setShow(false);
|
|
698
|
+
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
const mainRequest = async (msg: string) => {
|
|
703
|
+
|
|
704
|
+
// Use vLLM's API
|
|
705
|
+
//======================
|
|
706
|
+
try {
|
|
707
|
+
// Parse and interpolate request body template
|
|
708
|
+
let requestBodyRes = JSON.parse(
|
|
709
|
+
(args().requestBodyTmpl || '{}')
|
|
710
|
+
.replace(/\{model\}/g, args().model)
|
|
711
|
+
.replace(/\{message\}/g, msg)
|
|
712
|
+
.replace(/\{token\}/g, chatId)
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
//
|
|
716
|
+
// If a formatter function exists, it is used to process the request body
|
|
717
|
+
if (typeof args().requestBodyFormatter === 'function') {
|
|
718
|
+
requestBodyRes = args().requestBodyFormatter(requestBodyRes, args().latestContextData, conversationHistory.current);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Scroll to the bottom
|
|
722
|
+
setTimeout(() => {
|
|
723
|
+
// Scroll to the bottom
|
|
724
|
+
scrollToBottom();
|
|
725
|
+
}, 500);
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
if (args().isStream) {
|
|
729
|
+
{/* ======================================================== */}
|
|
730
|
+
{/* ======================== STREAM ====================== */}
|
|
731
|
+
{/* ======================================================== */}
|
|
732
|
+
|
|
733
|
+
const response: any = await fetch((args().requestApiUrl || ''), {
|
|
734
|
+
method: "POST",
|
|
735
|
+
body: JSON.stringify(requestBodyRes),
|
|
736
|
+
headers: args().headerConfigRes
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (!response.ok) {
|
|
740
|
+
const _errInfo = `[ERROR] HTTP Error ${response.status}: ${response.statusText}`;
|
|
741
|
+
|
|
742
|
+
setTempAnimText(_errInfo);
|
|
743
|
+
|
|
744
|
+
// hide loader
|
|
745
|
+
setLoaderDisplay(false);
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
reply: _errInfo
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Start streaming
|
|
754
|
+
await streamController.start(response);
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
reply: tempAnimText // The final content will be in tempAnimText
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
} else {
|
|
762
|
+
{/* ======================================================== */}
|
|
763
|
+
{/* ======================== NORMAL ====================== */}
|
|
764
|
+
{/* ======================================================== */}
|
|
765
|
+
|
|
766
|
+
// Extract response using the path
|
|
767
|
+
const extractPath = args().responseExtractPath?.slice(1);
|
|
768
|
+
|
|
769
|
+
const response = await fetch((args().requestApiUrl || ''), {
|
|
770
|
+
method: "POST",
|
|
771
|
+
headers: args().headerConfigRes,
|
|
772
|
+
body: JSON.stringify(requestBodyRes),
|
|
773
|
+
signal: abortController.current.signal
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
if (!response.ok) {
|
|
777
|
+
const _errInfo = `[ERROR] HTTP Error ${response.status}: ${response.statusText}`;
|
|
778
|
+
|
|
779
|
+
// hide loader
|
|
780
|
+
setLoaderDisplay(false);
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
reply: _errInfo
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const jsonResponse = await response.json();
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
// hide loader
|
|
791
|
+
setLoaderDisplay(false);
|
|
792
|
+
|
|
793
|
+
let result: any = jsonResponse;
|
|
794
|
+
if (extractPath) {
|
|
795
|
+
for (const path of extractPath) {
|
|
796
|
+
result = result[path];
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let content = result;
|
|
801
|
+
|
|
802
|
+
// Replace with a valid label
|
|
803
|
+
content = fixHtmlTags(content, args().withReasoning, args().reasoningSwitchLabel);
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
reply: formatLatestDisplayContent(content)
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const _err = `--> Error in mainRequest: ${error}`;
|
|
818
|
+
console.error(_err);
|
|
819
|
+
|
|
820
|
+
//reset SSE
|
|
821
|
+
closeSSE();
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
reply: _err
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
// exposes the following methods
|
|
832
|
+
useImperativeHandle(
|
|
833
|
+
propsRef.current.contentRef,
|
|
834
|
+
() => exposedMethods(),
|
|
835
|
+
[propsRef.current.contentRef, inputContentRef, msInput],
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
// Update ref when props change
|
|
841
|
+
useEffect(() => {
|
|
842
|
+
propsRef.current = props;
|
|
843
|
+
}, [props]);
|
|
844
|
+
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
if (props.requestConfig) {
|
|
847
|
+
configRef.current = props.requestConfig;
|
|
848
|
+
}
|
|
849
|
+
}, [props.requestConfig]);
|
|
850
|
+
|
|
851
|
+
useEffect(() => {
|
|
852
|
+
contextDataRef.current = props.contextData;
|
|
853
|
+
}, [props.contextData]);
|
|
854
|
+
|
|
855
|
+
useEffect(() => {
|
|
856
|
+
if (Array.isArray(props.defaultMessages) && props.defaultMessages.length > 0) {
|
|
857
|
+
// Update the default messages
|
|
858
|
+
setMsgList(props.defaultMessages);
|
|
859
|
+
}
|
|
860
|
+
}, [props.defaultMessages]);
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
return (
|
|
865
|
+
<>
|
|
866
|
+
|
|
867
|
+
<RootPortal show={true} containerClassName="Chatbox">
|
|
868
|
+
|
|
869
|
+
{/**------------- BUBBLE -------------*/}
|
|
870
|
+
{args().bubble ? <>
|
|
871
|
+
<div className={`${args().prefix || 'custom-'}chatbox-circle`} onClick={(e: React.MouseEvent) => {
|
|
872
|
+
e.preventDefault();
|
|
873
|
+
e.stopPropagation();
|
|
874
|
+
setShow(true);
|
|
875
|
+
}}
|
|
876
|
+
>
|
|
877
|
+
<span dangerouslySetInnerHTML={{ __html: `${args().bubbleLabel}` }}></span>
|
|
878
|
+
</div>
|
|
879
|
+
</> : null}
|
|
880
|
+
{/**------------- BUBBLE -------------*/}
|
|
881
|
+
|
|
882
|
+
{/**------------- CLOSE BUTTON -------------*/}
|
|
883
|
+
<button style={{ display: show ? 'block' : 'none' }} className={`${args().prefix || 'custom-'}chatbox-close`} tabIndex={-1} onClick={handleClose}>
|
|
884
|
+
<svg width="30px" height="30px" viewBox="0 0 1024 1024" fill="#000000"><path d="M707.872 329.392L348.096 689.16l-31.68-31.68 359.776-359.768z" fill="#000" /><path d="M328 340.8l32-31.2 348 348-32 32z" fill="#000" /></svg>
|
|
885
|
+
|
|
886
|
+
</button>
|
|
887
|
+
{/**------------- CLOSE BUTTON------------- */}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
<div style={{ display: show ? 'block' : 'none' }} className={`${args().prefix || 'custom-'}chatbox-container`} ref={rootRef}>
|
|
891
|
+
|
|
892
|
+
{/**------------- NO DATA -------------*/}
|
|
893
|
+
{msgList.length === 0 ? <>
|
|
894
|
+
|
|
895
|
+
<div className="d-flex flex-column align-items-center justify-content-center h-50">
|
|
896
|
+
<p>
|
|
897
|
+
<svg width="70px" height="70px" viewBox="0 0 24 24" fill="none">
|
|
898
|
+
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.5997 2.37562 15.1116 3.04346 16.4525C3.22094 16.8088 3.28001 17.2161 3.17712 17.6006L2.58151 19.8267C2.32295 20.793 3.20701 21.677 4.17335 21.4185L6.39939 20.8229C6.78393 20.72 7.19121 20.7791 7.54753 20.9565C8.88837 21.6244 10.4003 22 12 22Z" stroke="#858297" strokeWidth="1.5" />
|
|
899
|
+
<path opacity="0.5" d="M8 10.5H16" stroke="#333" strokeWidth="1.5" strokeLinecap="round" />
|
|
900
|
+
<path opacity="0.5" d="M8 14H13.5" stroke="#333" strokeWidth="1.5" strokeLinecap="round" />
|
|
901
|
+
</svg>
|
|
902
|
+
|
|
903
|
+
</p>
|
|
904
|
+
<p className="text-primary" dangerouslySetInnerHTML={{ __html: `${args().noDataPlaceholder}` }}></p>
|
|
905
|
+
</div>
|
|
906
|
+
</> : null}
|
|
907
|
+
{/**------------- /NO DATA -------------*/}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
{/**------------- MESSAGES LIST -------------*/}
|
|
912
|
+
<div className="messages" ref={msgContainerRef}>
|
|
913
|
+
|
|
914
|
+
{msgList.map((msg, index) => {
|
|
915
|
+
|
|
916
|
+
const isAnimProgress = tempAnimText !== '' && msg.sender !== args().questionNameRes && index === msgList.length - 1 && loading;
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
return <div key={index} className={msg.tag?.indexOf('[reply]') < 0 ? 'request' : 'reply'} style={{ display: isAnimProgress ? 'none' : '' }}>
|
|
920
|
+
<div className="qa-name" dangerouslySetInnerHTML={{ __html: `${msg.sender}` }}></div>
|
|
921
|
+
|
|
922
|
+
{msg.sender === args().questionNameRes ? <>
|
|
923
|
+
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
924
|
+
</> : <>
|
|
925
|
+
|
|
926
|
+
{args().isStream ? <>
|
|
927
|
+
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
928
|
+
</> : <>
|
|
929
|
+
<div className="qa-content">
|
|
930
|
+
<TypingEffect
|
|
931
|
+
messagesDiv={msgContainerRef.current}
|
|
932
|
+
content={`${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>`}
|
|
933
|
+
speed={10}
|
|
934
|
+
/>
|
|
935
|
+
</div>
|
|
936
|
+
</>}
|
|
937
|
+
</>}
|
|
938
|
+
|
|
939
|
+
</div>
|
|
940
|
+
}
|
|
941
|
+
)}
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
{/* ======================================================== */}
|
|
946
|
+
{/* ====================== STREAM begin ==================== */}
|
|
947
|
+
{/* ======================================================== */}
|
|
948
|
+
{args().isStream ? <>
|
|
949
|
+
{args().verbose ? <>
|
|
950
|
+
{/* +++++++++++++++ With reasoning ++++++++++++++++++++ */}
|
|
951
|
+
|
|
952
|
+
{/** ANIM TEXT (has thinking) */}
|
|
953
|
+
{tempAnimText !== '' && loading ? <>
|
|
954
|
+
<div className="reply reply-waiting">
|
|
955
|
+
<div className="qa-name">
|
|
956
|
+
<span dangerouslySetInnerHTML={{ __html: `${args().answerNameRes}` }} />
|
|
957
|
+
{loaderDisplay ? <>
|
|
958
|
+
<div className="msg-dotted-loader-container">
|
|
959
|
+
<span className="msg-dotted-loader"></span>
|
|
960
|
+
<span className="msg-dotted-loader-text">{args().sendLoadingLabel} ({elapsedTime}s)</span>
|
|
961
|
+
</div>
|
|
962
|
+
</> : null}
|
|
963
|
+
|
|
964
|
+
</div>
|
|
965
|
+
|
|
966
|
+
<div className="qa-content">
|
|
967
|
+
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${tempAnimText}` }}></div>
|
|
968
|
+
</div>
|
|
969
|
+
</div>
|
|
970
|
+
</> : null}
|
|
971
|
+
{/** /ANIM TEXT (has thinking) */}
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
</> : <>
|
|
975
|
+
{/* +++++++++++++++ Without reasoning ++++++++++++++++++++ */}
|
|
976
|
+
{/** ANIM TEXT (has loading) */}
|
|
977
|
+
{loading ? <>
|
|
978
|
+
<div className="reply reply-waiting">
|
|
979
|
+
<div className="qa-name">
|
|
980
|
+
<span dangerouslySetInnerHTML={{ __html: `${args().answerNameRes}` }} />
|
|
981
|
+
{thinking ? <>
|
|
982
|
+
<div className="msg-dotted-loader-container">
|
|
983
|
+
<span className="msg-dotted-loader"></span>
|
|
984
|
+
<span className="msg-dotted-loader-text">{args().sendLoadingLabel} ({elapsedTime}s)</span>
|
|
985
|
+
</div>
|
|
986
|
+
</> : null}
|
|
987
|
+
|
|
988
|
+
</div>
|
|
989
|
+
|
|
990
|
+
{tempAnimText !== '' ? <>
|
|
991
|
+
<div className="qa-content">
|
|
992
|
+
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${tempAnimText}` }}></div>
|
|
993
|
+
</div>
|
|
994
|
+
</> : null}
|
|
995
|
+
|
|
996
|
+
</div>
|
|
997
|
+
</> : null}
|
|
998
|
+
{/** /ANIM TEXT (has loading) */}
|
|
999
|
+
|
|
1000
|
+
</>}
|
|
1001
|
+
|
|
1002
|
+
</> : null}
|
|
1003
|
+
{/* ======================================================== */}
|
|
1004
|
+
{/* ====================== STREAM end ===================== */}
|
|
1005
|
+
{/* ======================================================== */}
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
{/* ======================================================== */}
|
|
1011
|
+
{/* ====================== NORMAL begin ==================== */}
|
|
1012
|
+
{/* ======================================================== */}
|
|
1013
|
+
{!args().isStream ? <>
|
|
1014
|
+
{/** ANIM TEXT (has loading) */}
|
|
1015
|
+
{loading ? <>
|
|
1016
|
+
<div className="reply reply-waiting">
|
|
1017
|
+
<div className="qa-name">
|
|
1018
|
+
<span dangerouslySetInnerHTML={{ __html: `${args().answerNameRes}` }} />
|
|
1019
|
+
<div className="msg-dotted-loader-container">
|
|
1020
|
+
<span className="msg-dotted-loader"></span>
|
|
1021
|
+
<span className="msg-dotted-loader-text">{args().sendLoadingLabel} ({elapsedTime}s)</span>
|
|
1022
|
+
</div>
|
|
1023
|
+
|
|
1024
|
+
</div>
|
|
1025
|
+
|
|
1026
|
+
{tempAnimText !== '' ? <>
|
|
1027
|
+
<div className="qa-content">
|
|
1028
|
+
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${tempAnimText}` }}></div>
|
|
1029
|
+
</div>
|
|
1030
|
+
</> : null}
|
|
1031
|
+
|
|
1032
|
+
</div>
|
|
1033
|
+
</> : null}
|
|
1034
|
+
{/** /ANIM TEXT (has loading) */}
|
|
1035
|
+
</> : null}
|
|
1036
|
+
{/* ======================================================== */}
|
|
1037
|
+
{/* ====================== NORMAL end ===================== */}
|
|
1038
|
+
{/* ======================================================== */}
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
{/**------------- NEW CHAT BUTTON -------------*/}
|
|
1042
|
+
{args().newChatButton && msgList.length > 0 && (
|
|
1043
|
+
<div className="newchat-btn">
|
|
1044
|
+
<button
|
|
1045
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(args().newChatButton.onClick, `${args().prefix || 'custom-'}chatbox-btn-new-${chatId}`, e.currentTarget)}
|
|
1046
|
+
>
|
|
1047
|
+
<span dangerouslySetInnerHTML={{ __html: args().newChatButton?.label || '' }}></span>
|
|
1048
|
+
</button>
|
|
1049
|
+
</div>
|
|
1050
|
+
)}
|
|
1051
|
+
{/**------------- /NEW CHAT BUTTON -------------*/}
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
</div>
|
|
1056
|
+
{/**------------- /MESSAGES LIST -------------*/}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
{/**------------- CONTROL AREA -------------*/}
|
|
1062
|
+
<div className="msgcontrol">
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
<Textarea
|
|
1066
|
+
ref={msInput}
|
|
1067
|
+
contentRef={inputContentRef}
|
|
1068
|
+
controlClassName="messageInput-control"
|
|
1069
|
+
wrapperClassName="messageInput"
|
|
1070
|
+
placeholder={args().placeholder}
|
|
1071
|
+
disabled={loading ? true : false}
|
|
1072
|
+
onKeyDown={(event: React.KeyboardEvent) => {
|
|
1073
|
+
if (event.key === 'Enter') {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
handleClickSafe();
|
|
1076
|
+
}
|
|
1077
|
+
}}
|
|
1078
|
+
onChange={(e) => {
|
|
1079
|
+
args().onInputChange?.(inputContentRef.current, e.target.value);
|
|
1080
|
+
}}
|
|
1081
|
+
rows={3}
|
|
1082
|
+
autoSize
|
|
1083
|
+
autoSizeMaxHeight={200}
|
|
1084
|
+
/>
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
{loading ? <>
|
|
1088
|
+
<button
|
|
1089
|
+
onClick={(e: React.MouseEvent) => {
|
|
1090
|
+
e.preventDefault();
|
|
1091
|
+
e.stopPropagation();
|
|
1092
|
+
|
|
1093
|
+
if (!args().isStream) {
|
|
1094
|
+
// normal request
|
|
1095
|
+
abortNormalRequest();
|
|
1096
|
+
} else {
|
|
1097
|
+
// stop stream
|
|
1098
|
+
abortStream();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
//reset SSE
|
|
1102
|
+
closeSSE();
|
|
1103
|
+
}}
|
|
1104
|
+
className="is-suspended"
|
|
1105
|
+
dangerouslySetInnerHTML={{ __html: `${args().stopLabel || '<svg width="15px" height="15px" viewBox="0 0 24 24" fill="none"><path d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z" fill="#1C274C"/></svg>'}` }}
|
|
1106
|
+
></button>
|
|
1107
|
+
</> : <>
|
|
1108
|
+
<button
|
|
1109
|
+
onClick={(e: React.MouseEvent) => {
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
e.stopPropagation();
|
|
1112
|
+
|
|
1113
|
+
// normal request
|
|
1114
|
+
if (!args().isStream) {
|
|
1115
|
+
if (abortController.current.signal.aborted) {
|
|
1116
|
+
reconnectNormalRequest();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
handleClickSafe();
|
|
1121
|
+
}}
|
|
1122
|
+
dangerouslySetInnerHTML={{ __html: `${args().sendLabel}` }}
|
|
1123
|
+
></button>
|
|
1124
|
+
</>}
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
</div>
|
|
1128
|
+
{/**------------- /CONTROL AREA -------------*/}
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
{/**------------- SEND LOADING -------------*/}
|
|
1133
|
+
{args().sendLoading ? <div className="loading"><div style={{ display: loading ? 'block' : 'none' }}><PureLoader customClassName="w-100" txt="" /></div></div> : null}
|
|
1134
|
+
{/**------------- /SEND LOADING -------------*/}
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
{/**------------- TOOLKIT BUTTONS -------------*/}
|
|
1138
|
+
{args().toolkitButtons && args().toolkitButtons.length > 0 && (
|
|
1139
|
+
<div className="toolkit-btns">
|
|
1140
|
+
{args().toolkitButtons.map((btn: FloatingButton, index: number) => {
|
|
1141
|
+
const _id = `${args().prefix || 'custom-'}chatbox-btn-tools-${chatId}${index}`;
|
|
1142
|
+
const isActive = activeButtons[_id];
|
|
1143
|
+
return <button
|
|
1144
|
+
key={index}
|
|
1145
|
+
className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
|
|
1146
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(btn.onClick, _id, e.currentTarget)}
|
|
1147
|
+
>
|
|
1148
|
+
<span dangerouslySetInnerHTML={{ __html: btn.label }}></span>
|
|
1149
|
+
</button>
|
|
1150
|
+
})}
|
|
1151
|
+
</div>
|
|
1152
|
+
)}
|
|
1153
|
+
{/**------------- /TOOLKIT BUTTONS -------------*/}
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
</div>
|
|
1158
|
+
|
|
1159
|
+
</RootPortal>
|
|
1160
|
+
|
|
1161
|
+
</>
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
export default Chatbox;
|
|
1170
|
+
|
|
1171
|
+
|