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.
@@ -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
+