funda-ui 4.5.767 → 4.5.888
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.css +13 -1
- package/Chatbox/index.d.ts +12 -1
- package/Chatbox/index.js +229 -57
- package/lib/cjs/Chatbox/index.d.ts +12 -1
- package/lib/cjs/Chatbox/index.js +229 -57
- package/lib/css/Chatbox/index.css +13 -1
- package/lib/esm/Chatbox/TypingEffect.tsx +61 -8
- package/lib/esm/Chatbox/index.scss +18 -2
- package/lib/esm/Chatbox/index.tsx +140 -28
- package/lib/esm/Chatbox/utils/func.ts +18 -0
- package/lib/esm/Textarea/index.tsx +1 -1
- package/package.json +1 -1
|
@@ -1,30 +1,83 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
interface TypingEffectProps {
|
|
4
|
-
messagesDiv: any;
|
|
5
4
|
content: string; // The content to display
|
|
6
5
|
speed: number; // Speed of typing in milliseconds
|
|
7
6
|
onComplete?: () => void; // Callback when typing is complete
|
|
7
|
+
onUpdate?: () => void; // Callback when typing
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface ImagePlaceholder {
|
|
11
|
+
original: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
type: 'img' | 'svg';
|
|
14
|
+
}
|
|
15
|
+
const TypingEffect: React.FC<TypingEffectProps> = ({ content, speed, onComplete, onUpdate }) => {
|
|
11
16
|
const [displayedContent, setDisplayedContent] = useState<string>('');
|
|
12
17
|
const [index, setIndex] = useState<number>(0);
|
|
18
|
+
const [imagePlaceholders, setImagePlaceholders] = useState<ImagePlaceholder[]>([]);
|
|
19
|
+
const [processedContent, setProcessedContent] = useState<string>('');
|
|
20
|
+
|
|
21
|
+
// Extract and replace image tags
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const extractImages = (html: string): { processedHtml: string; placeholders: ImagePlaceholder[] } => {
|
|
24
|
+
const placeholders: ImagePlaceholder[] = [];
|
|
25
|
+
let processedHtml = html;
|
|
26
|
+
|
|
27
|
+
// <img>
|
|
28
|
+
processedHtml = processedHtml.replace(/<img[^>]*>/g, (match) => {
|
|
29
|
+
const placeholder = `[IMG_${placeholders.length}]`;
|
|
30
|
+
placeholders.push({
|
|
31
|
+
original: match,
|
|
32
|
+
placeholder,
|
|
33
|
+
type: 'img'
|
|
34
|
+
});
|
|
35
|
+
return placeholder;
|
|
36
|
+
});
|
|
13
37
|
|
|
38
|
+
// <svg>
|
|
39
|
+
processedHtml = processedHtml.replace(/<svg[^>]*>[\s\S]*?<\/svg>/g, (match) => {
|
|
40
|
+
const placeholder = `[SVG_${placeholders.length}]`;
|
|
41
|
+
placeholders.push({
|
|
42
|
+
original: match,
|
|
43
|
+
placeholder,
|
|
44
|
+
type: 'svg'
|
|
45
|
+
});
|
|
46
|
+
return placeholder;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { processedHtml, placeholders };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const { processedHtml, placeholders } = extractImages(content);
|
|
53
|
+
setProcessedContent(processedHtml);
|
|
54
|
+
setImagePlaceholders(placeholders);
|
|
55
|
+
}, [content]);
|
|
56
|
+
|
|
57
|
+
// Handle typing effects
|
|
14
58
|
useEffect(() => {
|
|
15
59
|
const timer = setInterval(() => {
|
|
16
|
-
if (index <
|
|
17
|
-
|
|
60
|
+
if (index < processedContent.length) {
|
|
61
|
+
let newContent = processedContent.substring(0, index + 1);
|
|
62
|
+
|
|
63
|
+
// Replace the completed placeholder
|
|
64
|
+
imagePlaceholders.forEach(({ original, placeholder }) => {
|
|
65
|
+
if (newContent.includes(placeholder)) {
|
|
66
|
+
newContent = newContent.replace(placeholder, original);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
setDisplayedContent(newContent);
|
|
18
71
|
setIndex((prev) => prev + 1);
|
|
19
|
-
|
|
72
|
+
onUpdate?.();
|
|
20
73
|
} else {
|
|
21
74
|
clearInterval(timer);
|
|
22
|
-
onComplete?.();
|
|
75
|
+
onComplete?.();
|
|
23
76
|
}
|
|
24
77
|
}, speed);
|
|
25
78
|
|
|
26
|
-
return () => clearInterval(timer);
|
|
27
|
-
}, [
|
|
79
|
+
return () => clearInterval(timer);
|
|
80
|
+
}, [processedContent, index, speed, onComplete, onUpdate, imagePlaceholders]);
|
|
28
81
|
|
|
29
82
|
return <span dangerouslySetInnerHTML={{ __html: displayedContent }} />;
|
|
30
83
|
};
|
|
@@ -201,7 +201,10 @@
|
|
|
201
201
|
font-size: 13px;
|
|
202
202
|
margin-right: 0;
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
|
205
|
+
display: inline;
|
|
206
|
+
}
|
|
207
|
+
|
|
205
208
|
&::-webkit-scrollbar {
|
|
206
209
|
width: 3px;
|
|
207
210
|
}
|
|
@@ -344,6 +347,10 @@
|
|
|
344
347
|
z-index: 1;
|
|
345
348
|
width: calc(100% - 40px);
|
|
346
349
|
|
|
350
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
|
351
|
+
display: inline;
|
|
352
|
+
}
|
|
353
|
+
|
|
347
354
|
.messageInput {
|
|
348
355
|
width: 100%;
|
|
349
356
|
border: 1px solid var(--custom-chatbox-msg-border);
|
|
@@ -366,7 +373,7 @@
|
|
|
366
373
|
color: var(--custom-chatbox-default-txt-color);
|
|
367
374
|
resize: none;
|
|
368
375
|
max-height: 50vh;
|
|
369
|
-
border
|
|
376
|
+
border: 1px solid var(--custom-chatbox-gray-color);
|
|
370
377
|
|
|
371
378
|
&::-webkit-scrollbar {
|
|
372
379
|
width: 3px;
|
|
@@ -453,6 +460,10 @@
|
|
|
453
460
|
font-size: 0.8125rem;
|
|
454
461
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
455
462
|
|
|
463
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
|
464
|
+
display: inline;
|
|
465
|
+
}
|
|
466
|
+
|
|
456
467
|
&:hover {
|
|
457
468
|
transform: translateY(-2px);
|
|
458
469
|
}
|
|
@@ -483,6 +494,11 @@
|
|
|
483
494
|
transition: all 0.3s ease;
|
|
484
495
|
font-size: 0.75rem;
|
|
485
496
|
|
|
497
|
+
|
|
498
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
|
499
|
+
display: inline;
|
|
500
|
+
}
|
|
501
|
+
|
|
486
502
|
&:hover {
|
|
487
503
|
background-color: var(--custom-chatbox-toolkit-btn-border-color);
|
|
488
504
|
transform: translateY(-2px);
|
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
isValidJSON,
|
|
20
20
|
formatLatestDisplayContent,
|
|
21
21
|
formatName,
|
|
22
|
-
fixHtmlTags
|
|
22
|
+
fixHtmlTags,
|
|
23
|
+
isStreamResponse
|
|
23
24
|
} from './utils/func';
|
|
24
25
|
|
|
25
26
|
import useStreamController from './useStreamController';
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
export type MessageDetail = {
|
|
28
30
|
sender: string; // Sender's name
|
|
29
31
|
timestamp: string; // Time when the message was sent
|
|
@@ -43,6 +45,23 @@ export interface RequestConfig {
|
|
|
43
45
|
responseExtractor: string; // JSON path to extract response
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
type CustomRequestConfig = {
|
|
49
|
+
requestBody: any;
|
|
50
|
+
apiUrl: string;
|
|
51
|
+
headers: any;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type CustomRequestResponse = {
|
|
55
|
+
content: string | Response | null;
|
|
56
|
+
isStream: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type CustomRequestFunction = (
|
|
60
|
+
message: string,
|
|
61
|
+
config: CustomRequestConfig
|
|
62
|
+
) => Promise<CustomRequestResponse>;
|
|
63
|
+
|
|
64
|
+
|
|
46
65
|
export type ChatboxProps = {
|
|
47
66
|
debug?: boolean;
|
|
48
67
|
prefix?: string;
|
|
@@ -71,8 +90,9 @@ export type ChatboxProps = {
|
|
|
71
90
|
contextData?: Record<string, any>; // Dynamic JSON data
|
|
72
91
|
toolkitButtons?: FloatingButton[];
|
|
73
92
|
newChatButton?: FloatingButton;
|
|
93
|
+
customRequest?: CustomRequestFunction;
|
|
74
94
|
renderParser?: (input: string) => Promise<string>;
|
|
75
|
-
requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => any
|
|
95
|
+
requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => Promise<Record<string, any>>;
|
|
76
96
|
nameFormatter?: (input: string) => string;
|
|
77
97
|
onInputChange?: (controlRef: React.RefObject<any>, val: string) => any;
|
|
78
98
|
onChunk?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
|
|
@@ -118,6 +138,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
118
138
|
const [msgList, setMsgList] = useState<MessageDetail[]>([]);
|
|
119
139
|
const [elapsedTime, setElapsedTime] = useState<number>(0);
|
|
120
140
|
const [tempAnimText, setTempAnimText] = useState<string>('');
|
|
141
|
+
const [enableStreamMode, setEnableStreamMode] = useState<boolean>(true);
|
|
142
|
+
const animatedMessagesRef = useRef<Set<number>>(new Set()); // Add a ref to keep track of messages that have already been animated
|
|
121
143
|
|
|
122
144
|
//
|
|
123
145
|
const timer = useRef<any>(null);
|
|
@@ -154,9 +176,18 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
154
176
|
setVal: (v: string) => {
|
|
155
177
|
if (inputContentRef.current) inputContentRef.current.set(v);
|
|
156
178
|
},
|
|
179
|
+
getContextData: () => {
|
|
180
|
+
return contextDataRef.current;
|
|
181
|
+
},
|
|
157
182
|
setContextData: (v: Record<string, any>) => {
|
|
158
183
|
contextDataRef.current = v;
|
|
159
184
|
},
|
|
185
|
+
getMessages: () => {
|
|
186
|
+
return msgList;
|
|
187
|
+
},
|
|
188
|
+
setMessages: (v: MessageDetail[]) => {
|
|
189
|
+
setMsgList(v);
|
|
190
|
+
}
|
|
160
191
|
|
|
161
192
|
};
|
|
162
193
|
};
|
|
@@ -197,6 +228,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
197
228
|
toolkitButtons,
|
|
198
229
|
newChatButton,
|
|
199
230
|
maxHistoryLength,
|
|
231
|
+
customRequest,
|
|
200
232
|
renderParser,
|
|
201
233
|
requestBodyFormatter,
|
|
202
234
|
nameFormatter,
|
|
@@ -267,6 +299,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
267
299
|
maxHistoryLength,
|
|
268
300
|
toolkitButtons,
|
|
269
301
|
newChatButton,
|
|
302
|
+
customRequest,
|
|
270
303
|
renderParser,
|
|
271
304
|
requestBodyFormatter,
|
|
272
305
|
nameFormatter,
|
|
@@ -640,7 +673,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
640
673
|
|
|
641
674
|
// reply (normal)
|
|
642
675
|
//======================
|
|
643
|
-
if (!
|
|
676
|
+
if (!res.useStreamRender) {
|
|
644
677
|
const reply = res.reply;
|
|
645
678
|
let replyRes = `${reply}`;
|
|
646
679
|
|
|
@@ -658,7 +691,6 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
658
691
|
|
|
659
692
|
//reset SSE
|
|
660
693
|
closeSSE();
|
|
661
|
-
|
|
662
694
|
}
|
|
663
695
|
|
|
664
696
|
|
|
@@ -701,8 +733,12 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
701
733
|
|
|
702
734
|
const mainRequest = async (msg: string) => {
|
|
703
735
|
|
|
704
|
-
|
|
705
|
-
|
|
736
|
+
const currentStreamMode: boolean | undefined = args().isStream;
|
|
737
|
+
|
|
738
|
+
// Update stream mode
|
|
739
|
+
setEnableStreamMode(currentStreamMode as boolean);
|
|
740
|
+
|
|
741
|
+
|
|
706
742
|
try {
|
|
707
743
|
// Parse and interpolate request body template
|
|
708
744
|
let requestBodyRes = JSON.parse(
|
|
@@ -715,7 +751,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
715
751
|
//
|
|
716
752
|
// If a formatter function exists, it is used to process the request body
|
|
717
753
|
if (typeof args().requestBodyFormatter === 'function') {
|
|
718
|
-
requestBodyRes = args().requestBodyFormatter(requestBodyRes, args().latestContextData, conversationHistory.current);
|
|
754
|
+
requestBodyRes = await args().requestBodyFormatter(requestBodyRes, args().latestContextData, conversationHistory.current);
|
|
719
755
|
}
|
|
720
756
|
|
|
721
757
|
// Scroll to the bottom
|
|
@@ -724,8 +760,63 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
724
760
|
scrollToBottom();
|
|
725
761
|
}, 500);
|
|
726
762
|
|
|
763
|
+
{/* ======================================================== */}
|
|
764
|
+
{/* ===================== CUSTOM REQUEST ================== */}
|
|
765
|
+
{/* ======================================================== */}
|
|
766
|
+
// Check if customRequest exists and use it
|
|
767
|
+
if (typeof args().customRequest === 'function') {
|
|
768
|
+
|
|
769
|
+
// Update stream mode
|
|
770
|
+
setEnableStreamMode(false);
|
|
771
|
+
|
|
772
|
+
let customResponse: any = await args().customRequest(msg, {
|
|
773
|
+
requestBody: requestBodyRes,
|
|
774
|
+
apiUrl: args().requestApiUrl || '',
|
|
775
|
+
headers: args().headerConfigRes
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const { content, isStream } = customResponse;
|
|
779
|
+
let contentRes: any = content;
|
|
780
|
+
|
|
781
|
+
// Update stream mode
|
|
782
|
+
setEnableStreamMode(isStream);
|
|
783
|
+
|
|
784
|
+
// NORMAL
|
|
785
|
+
//++++++++++++++++++++++++++++++++++++++++++++++++
|
|
786
|
+
if (!isStream && typeof contentRes === 'string' && contentRes.trim() !== '') {
|
|
787
|
+
// Replace with a valid label
|
|
788
|
+
contentRes = fixHtmlTags(contentRes as string, args().withReasoning, args().reasoningSwitchLabel);
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
reply: formatLatestDisplayContent(contentRes),
|
|
792
|
+
useStreamRender: false
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// STREAM
|
|
797
|
+
//++++++++++++++++++++++++++++++++++++++++++++++++
|
|
798
|
+
if (isStream && isStreamResponse(contentRes as never)) {
|
|
799
|
+
// Start streaming
|
|
800
|
+
await streamController.start(contentRes as never);
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
reply: tempAnimText, // The final content will be in tempAnimText
|
|
804
|
+
useStreamRender: true
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
// DEFAULT
|
|
810
|
+
//++++++++++++++++++++++++++++++++++++++++++++++++
|
|
811
|
+
if (contentRes === null) {
|
|
812
|
+
// Update stream mode
|
|
813
|
+
setEnableStreamMode(currentStreamMode as boolean);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
}
|
|
817
|
+
|
|
727
818
|
|
|
728
|
-
if (
|
|
819
|
+
if (currentStreamMode) {
|
|
729
820
|
{/* ======================================================== */}
|
|
730
821
|
{/* ======================== STREAM ====================== */}
|
|
731
822
|
{/* ======================================================== */}
|
|
@@ -746,7 +837,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
746
837
|
|
|
747
838
|
|
|
748
839
|
return {
|
|
749
|
-
reply: _errInfo
|
|
840
|
+
reply: _errInfo,
|
|
841
|
+
useStreamRender: false
|
|
750
842
|
};
|
|
751
843
|
}
|
|
752
844
|
|
|
@@ -754,7 +846,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
754
846
|
await streamController.start(response);
|
|
755
847
|
|
|
756
848
|
return {
|
|
757
|
-
reply: tempAnimText // The final content will be in tempAnimText
|
|
849
|
+
reply: tempAnimText, // The final content will be in tempAnimText
|
|
850
|
+
useStreamRender: true
|
|
758
851
|
};
|
|
759
852
|
|
|
760
853
|
|
|
@@ -780,7 +873,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
780
873
|
setLoaderDisplay(false);
|
|
781
874
|
|
|
782
875
|
return {
|
|
783
|
-
reply: _errInfo
|
|
876
|
+
reply: _errInfo,
|
|
877
|
+
useStreamRender: false
|
|
784
878
|
};
|
|
785
879
|
}
|
|
786
880
|
|
|
@@ -803,16 +897,14 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
803
897
|
content = fixHtmlTags(content, args().withReasoning, args().reasoningSwitchLabel);
|
|
804
898
|
|
|
805
899
|
return {
|
|
806
|
-
reply: formatLatestDisplayContent(content)
|
|
900
|
+
reply: formatLatestDisplayContent(content),
|
|
901
|
+
useStreamRender: false
|
|
807
902
|
};
|
|
808
903
|
|
|
809
904
|
}
|
|
810
905
|
|
|
811
906
|
|
|
812
907
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
908
|
} catch (error) {
|
|
817
909
|
const _err = `--> Error in mainRequest: ${error}`;
|
|
818
910
|
console.error(_err);
|
|
@@ -821,7 +913,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
821
913
|
closeSSE();
|
|
822
914
|
|
|
823
915
|
return {
|
|
824
|
-
reply: _err
|
|
916
|
+
reply: _err,
|
|
917
|
+
useStreamRender: false
|
|
825
918
|
};
|
|
826
919
|
}
|
|
827
920
|
|
|
@@ -832,7 +925,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
832
925
|
useImperativeHandle(
|
|
833
926
|
propsRef.current.contentRef,
|
|
834
927
|
() => exposedMethods(),
|
|
835
|
-
[propsRef.current.contentRef, inputContentRef, msInput],
|
|
928
|
+
[propsRef.current.contentRef, inputContentRef, msInput, msgList],
|
|
836
929
|
);
|
|
837
930
|
|
|
838
931
|
|
|
@@ -859,6 +952,13 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
859
952
|
}
|
|
860
953
|
}, [props.defaultMessages]);
|
|
861
954
|
|
|
955
|
+
useEffect(() => {
|
|
956
|
+
if (Array.isArray(props.defaultMessages) && props.defaultMessages.length > 0) {
|
|
957
|
+
// Update the default messages
|
|
958
|
+
setMsgList(props.defaultMessages);
|
|
959
|
+
}
|
|
960
|
+
}, [props.defaultMessages]);
|
|
961
|
+
|
|
862
962
|
|
|
863
963
|
|
|
864
964
|
return (
|
|
@@ -914,7 +1014,10 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
914
1014
|
{msgList.map((msg, index) => {
|
|
915
1015
|
|
|
916
1016
|
const isAnimProgress = tempAnimText !== '' && msg.sender !== args().questionNameRes && index === msgList.length - 1 && loading;
|
|
917
|
-
|
|
1017
|
+
const hasAnimated = animatedMessagesRef.current.has(index);
|
|
1018
|
+
|
|
1019
|
+
// Mark the message as animated;
|
|
1020
|
+
animatedMessagesRef.current.add(index);
|
|
918
1021
|
|
|
919
1022
|
return <div key={index} className={msg.tag?.indexOf('[reply]') < 0 ? 'request' : 'reply'} style={{ display: isAnimProgress ? 'none' : '' }}>
|
|
920
1023
|
<div className="qa-name" dangerouslySetInnerHTML={{ __html: `${msg.sender}` }}></div>
|
|
@@ -923,15 +1026,22 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
923
1026
|
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
924
1027
|
</> : <>
|
|
925
1028
|
|
|
926
|
-
{
|
|
1029
|
+
{enableStreamMode ? <>
|
|
927
1030
|
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
928
1031
|
</> : <>
|
|
929
1032
|
<div className="qa-content">
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1033
|
+
{hasAnimated ? (
|
|
1034
|
+
<div dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
1035
|
+
) : (
|
|
1036
|
+
<TypingEffect
|
|
1037
|
+
onUpdate={() => {
|
|
1038
|
+
scrollToBottom();
|
|
1039
|
+
}}
|
|
1040
|
+
content={`${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>`}
|
|
1041
|
+
speed={10}
|
|
1042
|
+
/>
|
|
1043
|
+
)}
|
|
1044
|
+
|
|
935
1045
|
</div>
|
|
936
1046
|
</>}
|
|
937
1047
|
</>}
|
|
@@ -945,7 +1055,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
945
1055
|
{/* ======================================================== */}
|
|
946
1056
|
{/* ====================== STREAM begin ==================== */}
|
|
947
1057
|
{/* ======================================================== */}
|
|
948
|
-
{
|
|
1058
|
+
{enableStreamMode ? <>
|
|
949
1059
|
{args().verbose ? <>
|
|
950
1060
|
{/* +++++++++++++++ With reasoning ++++++++++++++++++++ */}
|
|
951
1061
|
|
|
@@ -1010,7 +1120,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1010
1120
|
{/* ======================================================== */}
|
|
1011
1121
|
{/* ====================== NORMAL begin ==================== */}
|
|
1012
1122
|
{/* ======================================================== */}
|
|
1013
|
-
{!
|
|
1123
|
+
{!enableStreamMode ? <>
|
|
1014
1124
|
{/** ANIM TEXT (has loading) */}
|
|
1015
1125
|
{loading ? <>
|
|
1016
1126
|
<div className="reply reply-waiting">
|
|
@@ -1042,6 +1152,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1042
1152
|
{args().newChatButton && msgList.length > 0 && (
|
|
1043
1153
|
<div className="newchat-btn">
|
|
1044
1154
|
<button
|
|
1155
|
+
id={`${args().prefix || 'custom-'}chatbox-btn-new-${chatId}`}
|
|
1045
1156
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(args().newChatButton.onClick, `${args().prefix || 'custom-'}chatbox-btn-new-${chatId}`, e.currentTarget)}
|
|
1046
1157
|
>
|
|
1047
1158
|
<span dangerouslySetInnerHTML={{ __html: args().newChatButton?.label || '' }}></span>
|
|
@@ -1090,7 +1201,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1090
1201
|
e.preventDefault();
|
|
1091
1202
|
e.stopPropagation();
|
|
1092
1203
|
|
|
1093
|
-
if (!
|
|
1204
|
+
if (!enableStreamMode) {
|
|
1094
1205
|
// normal request
|
|
1095
1206
|
abortNormalRequest();
|
|
1096
1207
|
} else {
|
|
@@ -1111,7 +1222,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1111
1222
|
e.stopPropagation();
|
|
1112
1223
|
|
|
1113
1224
|
// normal request
|
|
1114
|
-
if (!
|
|
1225
|
+
if (!enableStreamMode) {
|
|
1115
1226
|
if (abortController.current.signal.aborted) {
|
|
1116
1227
|
reconnectNormalRequest();
|
|
1117
1228
|
}
|
|
@@ -1142,6 +1253,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1142
1253
|
const isActive = activeButtons[_id];
|
|
1143
1254
|
return <button
|
|
1144
1255
|
key={index}
|
|
1256
|
+
id={_id}
|
|
1145
1257
|
className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
|
|
1146
1258
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(btn.onClick, _id, e.currentTarget)}
|
|
1147
1259
|
>
|
|
@@ -105,3 +105,21 @@ export function fixHtmlTags(html: string, withReasoning: boolean, reasoningSwitc
|
|
|
105
105
|
.replace('</think>', '</div></details> ');
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function isStreamResponse(response: Response): boolean {
|
|
109
|
+
// Method 1: Check Content-Type
|
|
110
|
+
const contentType = response.headers.get('Content-Type');
|
|
111
|
+
if (contentType) {
|
|
112
|
+
return contentType.includes('text/event-stream') ||
|
|
113
|
+
contentType.includes('application/x-ndjson') ||
|
|
114
|
+
contentType.includes('application/stream+json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Method 2: Check Transfer-Encoding
|
|
118
|
+
const transferEncoding = response.headers.get('Transfer-Encoding');
|
|
119
|
+
if (transferEncoding) {
|
|
120
|
+
return transferEncoding.includes('chunked');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Method 3: Check if response.body is ReadableStream
|
|
124
|
+
return response.body instanceof ReadableStream;
|
|
125
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, forwardRef, ChangeEvent, KeyboardEvent, useImperativeHandle } from 'react';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
5
6
|
import useAutosizeTextArea from 'funda-utils/dist/cjs/useAutosizeTextArea';
|
|
6
7
|
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
@@ -8,7 +9,6 @@ import { actualPropertyValue, getTextTop } from 'funda-utils/dist/cjs/inputsCalc
|
|
|
8
9
|
import useDebounce from 'funda-utils/dist/cjs/useDebounce';
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
|
|
12
12
|
export type TextareaProps = {
|
|
13
13
|
contentRef?: React.ForwardedRef<any>; // could use "Array" on contentRef.current, such as contentRef.current[0], contentRef.current[1]
|
|
14
14
|
wrapperClassName?: string;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "UIUX Lab",
|
|
3
3
|
"email": "uiuxlab@gmail.com",
|
|
4
4
|
"name": "funda-ui",
|
|
5
|
-
"version": "4.5.
|
|
5
|
+
"version": "4.5.888",
|
|
6
6
|
"description": "React components using pure Bootstrap 5+ which does not contain any external style and script libraries.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|