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.
@@ -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
- const TypingEffect: React.FC<TypingEffectProps> = ({ messagesDiv, content, speed, onComplete }) => {
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 < content.length) {
17
- setDisplayedContent((prev) => prev + content[index]);
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
- if (messagesDiv !== null) messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll to the bottom
72
+ onUpdate?.();
20
73
  } else {
21
74
  clearInterval(timer);
22
- onComplete?.(); // Call the onComplete callback if provided
75
+ onComplete?.();
23
76
  }
24
77
  }, speed);
25
78
 
26
- return () => clearInterval(timer); // Cleanup on unmount
27
- }, [content, index, speed, onComplete]);
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-color: var(--custom-chatbox-gray-color);
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 (!args().isStream) {
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
- // Use vLLM's API
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 (args().isStream) {
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
- {args().isStream ? <>
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
- <TypingEffect
931
- messagesDiv={msgContainerRef.current}
932
- content={`${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>`}
933
- speed={10}
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
- {args().isStream ? <>
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
- {!args().isStream ? <>
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 (!args().isStream) {
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 (!args().isStream) {
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.767",
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",