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.
@@ -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 (!args().isStream) {
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
- // Use vLLM's API
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
- if (args().isStream) {
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
- {args().isStream ? <>
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
- <TypingEffect
934
- messagesDiv={msgContainerRef.current}
935
- content={`${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>`}
936
- speed={10}
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
- {args().isStream ? <>
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
- {!args().isStream ? <>
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 (!args().isStream) {
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 (!args().isStream) {
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.777",
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",