funda-ui 4.5.777 → 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.d.ts +12 -1
- package/Chatbox/index.js +226 -57
- package/lib/cjs/Chatbox/index.d.ts +12 -1
- package/lib/cjs/Chatbox/index.js +226 -57
- package/lib/esm/Chatbox/TypingEffect.tsx +61 -8
- package/lib/esm/Chatbox/index.tsx +137 -28
- package/lib/esm/Chatbox/utils/func.ts +18 -0
- package/lib/esm/Textarea/index.tsx +1 -1
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -160,6 +182,12 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
160
182
|
setContextData: (v: Record<string, any>) => {
|
|
161
183
|
contextDataRef.current = v;
|
|
162
184
|
},
|
|
185
|
+
getMessages: () => {
|
|
186
|
+
return msgList;
|
|
187
|
+
},
|
|
188
|
+
setMessages: (v: MessageDetail[]) => {
|
|
189
|
+
setMsgList(v);
|
|
190
|
+
}
|
|
163
191
|
|
|
164
192
|
};
|
|
165
193
|
};
|
|
@@ -200,6 +228,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
200
228
|
toolkitButtons,
|
|
201
229
|
newChatButton,
|
|
202
230
|
maxHistoryLength,
|
|
231
|
+
customRequest,
|
|
203
232
|
renderParser,
|
|
204
233
|
requestBodyFormatter,
|
|
205
234
|
nameFormatter,
|
|
@@ -270,6 +299,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
270
299
|
maxHistoryLength,
|
|
271
300
|
toolkitButtons,
|
|
272
301
|
newChatButton,
|
|
302
|
+
customRequest,
|
|
273
303
|
renderParser,
|
|
274
304
|
requestBodyFormatter,
|
|
275
305
|
nameFormatter,
|
|
@@ -643,7 +673,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
643
673
|
|
|
644
674
|
// reply (normal)
|
|
645
675
|
//======================
|
|
646
|
-
if (!
|
|
676
|
+
if (!res.useStreamRender) {
|
|
647
677
|
const reply = res.reply;
|
|
648
678
|
let replyRes = `${reply}`;
|
|
649
679
|
|
|
@@ -661,7 +691,6 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
661
691
|
|
|
662
692
|
//reset SSE
|
|
663
693
|
closeSSE();
|
|
664
|
-
|
|
665
694
|
}
|
|
666
695
|
|
|
667
696
|
|
|
@@ -704,8 +733,12 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
704
733
|
|
|
705
734
|
const mainRequest = async (msg: string) => {
|
|
706
735
|
|
|
707
|
-
|
|
708
|
-
|
|
736
|
+
const currentStreamMode: boolean | undefined = args().isStream;
|
|
737
|
+
|
|
738
|
+
// Update stream mode
|
|
739
|
+
setEnableStreamMode(currentStreamMode as boolean);
|
|
740
|
+
|
|
741
|
+
|
|
709
742
|
try {
|
|
710
743
|
// Parse and interpolate request body template
|
|
711
744
|
let requestBodyRes = JSON.parse(
|
|
@@ -718,7 +751,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
718
751
|
//
|
|
719
752
|
// If a formatter function exists, it is used to process the request body
|
|
720
753
|
if (typeof args().requestBodyFormatter === 'function') {
|
|
721
|
-
requestBodyRes = args().requestBodyFormatter(requestBodyRes, args().latestContextData, conversationHistory.current);
|
|
754
|
+
requestBodyRes = await args().requestBodyFormatter(requestBodyRes, args().latestContextData, conversationHistory.current);
|
|
722
755
|
}
|
|
723
756
|
|
|
724
757
|
// Scroll to the bottom
|
|
@@ -727,8 +760,63 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
727
760
|
scrollToBottom();
|
|
728
761
|
}, 500);
|
|
729
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);
|
|
730
789
|
|
|
731
|
-
|
|
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
|
+
|
|
818
|
+
|
|
819
|
+
if (currentStreamMode) {
|
|
732
820
|
{/* ======================================================== */}
|
|
733
821
|
{/* ======================== STREAM ====================== */}
|
|
734
822
|
{/* ======================================================== */}
|
|
@@ -749,7 +837,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
749
837
|
|
|
750
838
|
|
|
751
839
|
return {
|
|
752
|
-
reply: _errInfo
|
|
840
|
+
reply: _errInfo,
|
|
841
|
+
useStreamRender: false
|
|
753
842
|
};
|
|
754
843
|
}
|
|
755
844
|
|
|
@@ -757,7 +846,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
757
846
|
await streamController.start(response);
|
|
758
847
|
|
|
759
848
|
return {
|
|
760
|
-
reply: tempAnimText // The final content will be in tempAnimText
|
|
849
|
+
reply: tempAnimText, // The final content will be in tempAnimText
|
|
850
|
+
useStreamRender: true
|
|
761
851
|
};
|
|
762
852
|
|
|
763
853
|
|
|
@@ -783,7 +873,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
783
873
|
setLoaderDisplay(false);
|
|
784
874
|
|
|
785
875
|
return {
|
|
786
|
-
reply: _errInfo
|
|
876
|
+
reply: _errInfo,
|
|
877
|
+
useStreamRender: false
|
|
787
878
|
};
|
|
788
879
|
}
|
|
789
880
|
|
|
@@ -806,16 +897,14 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
806
897
|
content = fixHtmlTags(content, args().withReasoning, args().reasoningSwitchLabel);
|
|
807
898
|
|
|
808
899
|
return {
|
|
809
|
-
reply: formatLatestDisplayContent(content)
|
|
900
|
+
reply: formatLatestDisplayContent(content),
|
|
901
|
+
useStreamRender: false
|
|
810
902
|
};
|
|
811
903
|
|
|
812
904
|
}
|
|
813
905
|
|
|
814
906
|
|
|
815
907
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
908
|
} catch (error) {
|
|
820
909
|
const _err = `--> Error in mainRequest: ${error}`;
|
|
821
910
|
console.error(_err);
|
|
@@ -824,7 +913,8 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
824
913
|
closeSSE();
|
|
825
914
|
|
|
826
915
|
return {
|
|
827
|
-
reply: _err
|
|
916
|
+
reply: _err,
|
|
917
|
+
useStreamRender: false
|
|
828
918
|
};
|
|
829
919
|
}
|
|
830
920
|
|
|
@@ -835,7 +925,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
835
925
|
useImperativeHandle(
|
|
836
926
|
propsRef.current.contentRef,
|
|
837
927
|
() => exposedMethods(),
|
|
838
|
-
[propsRef.current.contentRef, inputContentRef, msInput],
|
|
928
|
+
[propsRef.current.contentRef, inputContentRef, msInput, msgList],
|
|
839
929
|
);
|
|
840
930
|
|
|
841
931
|
|
|
@@ -862,6 +952,13 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
862
952
|
}
|
|
863
953
|
}, [props.defaultMessages]);
|
|
864
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
|
+
|
|
865
962
|
|
|
866
963
|
|
|
867
964
|
return (
|
|
@@ -917,7 +1014,10 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
917
1014
|
{msgList.map((msg, index) => {
|
|
918
1015
|
|
|
919
1016
|
const isAnimProgress = tempAnimText !== '' && msg.sender !== args().questionNameRes && index === msgList.length - 1 && loading;
|
|
920
|
-
|
|
1017
|
+
const hasAnimated = animatedMessagesRef.current.has(index);
|
|
1018
|
+
|
|
1019
|
+
// Mark the message as animated;
|
|
1020
|
+
animatedMessagesRef.current.add(index);
|
|
921
1021
|
|
|
922
1022
|
return <div key={index} className={msg.tag?.indexOf('[reply]') < 0 ? 'request' : 'reply'} style={{ display: isAnimProgress ? 'none' : '' }}>
|
|
923
1023
|
<div className="qa-name" dangerouslySetInnerHTML={{ __html: `${msg.sender}` }}></div>
|
|
@@ -926,15 +1026,22 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
926
1026
|
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
927
1027
|
</> : <>
|
|
928
1028
|
|
|
929
|
-
{
|
|
1029
|
+
{enableStreamMode ? <>
|
|
930
1030
|
<div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
|
|
931
1031
|
</> : <>
|
|
932
1032
|
<div className="qa-content">
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
+
|
|
938
1045
|
</div>
|
|
939
1046
|
</>}
|
|
940
1047
|
</>}
|
|
@@ -948,7 +1055,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
948
1055
|
{/* ======================================================== */}
|
|
949
1056
|
{/* ====================== STREAM begin ==================== */}
|
|
950
1057
|
{/* ======================================================== */}
|
|
951
|
-
{
|
|
1058
|
+
{enableStreamMode ? <>
|
|
952
1059
|
{args().verbose ? <>
|
|
953
1060
|
{/* +++++++++++++++ With reasoning ++++++++++++++++++++ */}
|
|
954
1061
|
|
|
@@ -1013,7 +1120,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1013
1120
|
{/* ======================================================== */}
|
|
1014
1121
|
{/* ====================== NORMAL begin ==================== */}
|
|
1015
1122
|
{/* ======================================================== */}
|
|
1016
|
-
{!
|
|
1123
|
+
{!enableStreamMode ? <>
|
|
1017
1124
|
{/** ANIM TEXT (has loading) */}
|
|
1018
1125
|
{loading ? <>
|
|
1019
1126
|
<div className="reply reply-waiting">
|
|
@@ -1045,6 +1152,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1045
1152
|
{args().newChatButton && msgList.length > 0 && (
|
|
1046
1153
|
<div className="newchat-btn">
|
|
1047
1154
|
<button
|
|
1155
|
+
id={`${args().prefix || 'custom-'}chatbox-btn-new-${chatId}`}
|
|
1048
1156
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(args().newChatButton.onClick, `${args().prefix || 'custom-'}chatbox-btn-new-${chatId}`, e.currentTarget)}
|
|
1049
1157
|
>
|
|
1050
1158
|
<span dangerouslySetInnerHTML={{ __html: args().newChatButton?.label || '' }}></span>
|
|
@@ -1093,7 +1201,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1093
1201
|
e.preventDefault();
|
|
1094
1202
|
e.stopPropagation();
|
|
1095
1203
|
|
|
1096
|
-
if (!
|
|
1204
|
+
if (!enableStreamMode) {
|
|
1097
1205
|
// normal request
|
|
1098
1206
|
abortNormalRequest();
|
|
1099
1207
|
} else {
|
|
@@ -1114,7 +1222,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1114
1222
|
e.stopPropagation();
|
|
1115
1223
|
|
|
1116
1224
|
// normal request
|
|
1117
|
-
if (!
|
|
1225
|
+
if (!enableStreamMode) {
|
|
1118
1226
|
if (abortController.current.signal.aborted) {
|
|
1119
1227
|
reconnectNormalRequest();
|
|
1120
1228
|
}
|
|
@@ -1145,6 +1253,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1145
1253
|
const isActive = activeButtons[_id];
|
|
1146
1254
|
return <button
|
|
1147
1255
|
key={index}
|
|
1256
|
+
id={_id}
|
|
1148
1257
|
className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
|
|
1149
1258
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(btn.onClick, _id, e.currentTarget)}
|
|
1150
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",
|