funda-ui 4.5.713 → 4.5.767

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,277 @@
1
+ /**
2
+ * Stream Controller
3
+ *
4
+ * @usage:
5
+
6
+ // Use in component
7
+ const streamController = useStreamController({
8
+ onChunk: async (chunk: string, index: number) => {
9
+ // start (Execute it only once)
10
+ if (index === 0) {
11
+
12
+ }
13
+
14
+ // Streaming data is JSON split by rows
15
+ const lines = chunk.split("\n").filter(line => line.trim() !== "");
16
+
17
+ // Process each data chunk
18
+ console.log('Received chunk:', chunk);
19
+ },
20
+ onComplete: async (lastContent: string) => {
21
+ // Process when stream is completed
22
+ console.log('Stream completed');
23
+
24
+ // Display AI reply
25
+ console.log('AI reply:', lastContent);
26
+
27
+ },
28
+ onError: (error) => {
29
+ // Error handling
30
+ console.error('Stream error:', error);
31
+ },
32
+ onAbort: () => {
33
+ // Abort processing
34
+ console.log('Stream aborted');
35
+ }
36
+ });
37
+
38
+ // Start stream
39
+ const response = await fetch(url);
40
+ await streamController.start(response);
41
+
42
+ // Pause stream
43
+ streamController.pause();
44
+
45
+ // Resume stream
46
+ streamController.resume();
47
+
48
+ // Abort stream
49
+ streamController.abort();
50
+
51
+ // Check status
52
+ const isActive = streamController.isActive();
53
+ const isPaused = streamController.isPaused();
54
+
55
+ */
56
+
57
+
58
+ import { useRef, useCallback, useEffect } from 'react';
59
+
60
+ export interface StreamControllerOptions {
61
+ onChunk?: (chunk: string, index: number) => void;
62
+ onComplete?: (lastContent: string) => void;
63
+ onError?: (error: any) => void;
64
+ onAbort?: () => void;
65
+ }
66
+
67
+ export interface StreamController {
68
+ start: (response: Response) => Promise<void>;
69
+ pause: () => void;
70
+ resume: () => void;
71
+ abort: () => void;
72
+ isActive: () => boolean;
73
+ isPaused: () => boolean;
74
+ }
75
+
76
+ export const useStreamController = (options: StreamControllerOptions = {}): StreamController => {
77
+ const streamController = useRef<ReadableStreamDefaultController | null>(null);
78
+ const reader = useRef<ReadableStreamDefaultReader | null>(null);
79
+ const activeStream = useRef<ReadableStream | null>(null);
80
+ const responseReader = useRef<ReadableStreamDefaultReader | null>(null);
81
+ const paused = useRef<boolean>(false);
82
+ const active = useRef<boolean>(false);
83
+ const abortController = useRef<AbortController>(new AbortController());
84
+ const textDecoder = useRef<TextDecoder>(new TextDecoder("utf-8")); // Get the decoding of UTF8
85
+
86
+ // To avoid the "Uncaught (in promise) TypeError: Failed to execute 'cancel' on 'ReadableStream': Cannot cancel a locked stream" error,
87
+ // (1) you need to safely release the reader.
88
+ // (2) cleanup() also requires asynchronous state
89
+ const releaseReader = useCallback(async (readerRef: React.MutableRefObject<ReadableStreamDefaultReader | null>) => {
90
+ if (readerRef.current) {
91
+ try {
92
+ await readerRef.current.cancel();
93
+ } catch (e) {
94
+ console.warn('Error cancelling reader:', e);
95
+ }
96
+
97
+ try {
98
+ readerRef.current.releaseLock();
99
+ } catch (e) {
100
+ console.warn('Error releasing reader lock:', e);
101
+ }
102
+ readerRef.current = null;
103
+ }
104
+ }, []);
105
+
106
+ const cleanup = useCallback(async () => {
107
+ // First release all readers
108
+ await releaseReader(reader);
109
+ await releaseReader(responseReader);
110
+
111
+ // Then try to cancel the stream
112
+ if (activeStream.current) {
113
+ try {
114
+ await activeStream.current.cancel();
115
+ } catch (e) {
116
+ console.warn('Error cancelling stream:', e);
117
+ }
118
+ activeStream.current = null;
119
+ }
120
+
121
+ streamController.current = null;
122
+ active.current = false;
123
+ paused.current = false;
124
+ }, [releaseReader]);
125
+
126
+ // Process chunks of data
127
+ const processChunk = useCallback(async (chunk: string, index: number) => {
128
+ try {
129
+ options.onChunk?.(chunk, index);
130
+ } catch (error) {
131
+ options.onError?.(error);
132
+ }
133
+ }, [options]);
134
+
135
+ // Start processing the stream
136
+ const startProcessing = useCallback(async () => {
137
+ if (!reader.current || !active.current) return;
138
+
139
+ //
140
+ let counter = 0;
141
+
142
+
143
+ // Store the final content and bind it to loading
144
+ let lastContent: string = '';
145
+
146
+ while (active.current) {
147
+ try {
148
+
149
+ if (paused.current) {
150
+ await new Promise(resolve => setTimeout(resolve, 100));
151
+ continue;
152
+ }
153
+
154
+ const { done, value } = await reader.current.read();
155
+
156
+ if (done) {
157
+ options.onComplete?.(lastContent);
158
+ await cleanup();
159
+ break;
160
+ }
161
+
162
+ // Decode the content
163
+ const chunkStr = textDecoder.current.decode(value as Uint8Array, { stream: true });
164
+ lastContent += chunkStr;
165
+
166
+ await processChunk(chunkStr, counter);
167
+ counter++;
168
+
169
+ } catch (error: any) {
170
+ if (error.name === 'AbortError') {
171
+ options.onAbort?.();
172
+ } else {
173
+ options.onError?.(error);
174
+ }
175
+ await cleanup();
176
+ break;
177
+ }
178
+
179
+ }
180
+ }, [options, cleanup, processChunk]);
181
+
182
+
183
+ // Start streaming
184
+ const start = useCallback(async (response: Response) => {
185
+ await cleanup();
186
+
187
+ // Get Reader
188
+ reader.current = response.body!.getReader();
189
+
190
+ try {
191
+
192
+ const stream = new ReadableStream({
193
+ start(controller) {
194
+ streamController.current = controller;
195
+ },
196
+ async pull(controller) {
197
+ try {
198
+ const { done, value } = await reader.current!.read();
199
+
200
+ if (done) {
201
+ controller.close();
202
+ return;
203
+ }
204
+
205
+ // Decode the content
206
+ const chunkStr = textDecoder.current.decode(value as Uint8Array, { stream: true });
207
+
208
+ controller.enqueue(chunkStr);
209
+ } catch (error) {
210
+ controller.error(error);
211
+ }
212
+ },
213
+ cancel() {
214
+ response.body?.cancel();
215
+ }
216
+ });
217
+
218
+ activeStream.current = stream;
219
+ active.current = true;
220
+ paused.current = false;
221
+
222
+ // Start processing immediately
223
+ await startProcessing();
224
+ } catch (error) {
225
+ options.onError?.(error);
226
+ cleanup();
227
+ }
228
+
229
+
230
+ }, [options, cleanup, startProcessing]);
231
+
232
+
233
+
234
+ // Pause streaming
235
+ const pause = useCallback(() => {
236
+ paused.current = true;
237
+ }, []);
238
+
239
+ // Resume streaming
240
+ const resume = useCallback(() => {
241
+ paused.current = false;
242
+ }, []);
243
+
244
+ // Abort streaming
245
+ const abort = useCallback(async () => {
246
+ abortController.current.abort();
247
+ await cleanup();
248
+ }, [cleanup]);
249
+
250
+ // Check if stream is active
251
+ const isActive = useCallback(() => {
252
+ return active.current;
253
+ }, []);
254
+
255
+ // Check if stream is paused
256
+ const isPaused = useCallback(() => {
257
+ return paused.current;
258
+ }, []);
259
+
260
+ // Cleanup on unmount
261
+ useEffect(() => {
262
+ return () => {
263
+ cleanup().catch(console.error);
264
+ };
265
+ }, [cleanup]);
266
+
267
+ return {
268
+ start,
269
+ pause,
270
+ resume,
271
+ abort,
272
+ isActive,
273
+ isPaused
274
+ };
275
+ };
276
+
277
+ export default useStreamController;
@@ -0,0 +1,107 @@
1
+ // app.ts
2
+ import type { ChatboxProps } from '../index';
3
+
4
+
5
+ export function isValidJSON(str: string){
6
+ try {
7
+ JSON.parse(str);
8
+ return true;
9
+ } catch (error) {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export function formatLatestDisplayContent(str: string) {
15
+ // Regular expression to match <details> tags and their content
16
+ const output = str.replace(/<details class="think"[^>]*>([\s\S]*?)<\/details>/g, (match, content) => {
17
+ // Use regex to match the content inside the "div.think-content"
18
+ const thinkContentMatch = content.match(/<div class="think-content">([\s\S]*?)<\/div>/);
19
+
20
+ if (thinkContentMatch) {
21
+ const thinkContent = thinkContentMatch[1].trim(); // Get the content inside "div.think-content" and trim whitespace
22
+
23
+ // Check if "div.think-content" is empty
24
+ if (thinkContent === '') {
25
+ return ''; // If empty, return an empty string to replace the entire <details> tag
26
+ }
27
+ }
28
+
29
+ return match; // If not empty, return the original matched content
30
+ });
31
+
32
+ return output;
33
+ }
34
+
35
+ export function formatName(str: any, isAnswer: boolean, props: ChatboxProps) {
36
+ if (typeof str !== "string") return str;
37
+
38
+ const {
39
+ questionNameIcon,
40
+ answerNameIcon,
41
+ nameFormatter
42
+ } = props;
43
+
44
+ if (typeof nameFormatter === 'function') {
45
+ return nameFormatter(str.replace(/\{icon\}/g, `${isAnswer ? answerNameIcon : questionNameIcon}`));
46
+ } else {
47
+ return str.replace(/\{icon\}/g, `${isAnswer ? answerNameIcon : questionNameIcon}`);
48
+ }
49
+ }
50
+
51
+
52
+ export function typewriterEffect(messagesDiv: HTMLElement, element: HTMLElement, str: string, speed: number = 50) {
53
+ if (!element) return;
54
+
55
+ const originalHTML = str;
56
+ element.innerHTML = '';
57
+
58
+ let cursor = 0;
59
+ let tempHTML = '';
60
+ const tagStack: string[] = [];
61
+
62
+ function type() {
63
+ if (cursor >= originalHTML.length) {
64
+ // Clear the cursor after typing is complete
65
+ element.innerHTML = tempHTML; // Set the final content without the cursor
66
+ return;
67
+ }
68
+
69
+ const currentChar = originalHTML[cursor];
70
+
71
+ if (currentChar === '<') {
72
+ const closeTagIndex = originalHTML.indexOf('>', cursor);
73
+ const tagContent = originalHTML.slice(cursor, closeTagIndex + 1);
74
+ tempHTML += tagContent;
75
+
76
+ // Handle opening and closing tags
77
+ if (/^<\/?\w+/.test(tagContent)) {
78
+ if (!/^<\//.test(tagContent)) {
79
+ // Opening tag
80
+ tagStack.push(tagContent as never);
81
+ } else {
82
+ // Closing tag
83
+ tagStack.pop();
84
+ }
85
+ }
86
+
87
+ cursor = closeTagIndex + 1;
88
+ } else {
89
+ tempHTML += currentChar;
90
+ cursor++;
91
+ }
92
+
93
+ messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll to the bottom
94
+ element.innerHTML = tempHTML + '<span class="cursor">|</span>'; // Show cursor
95
+ setTimeout(type, speed);
96
+ }
97
+
98
+ type();
99
+ }
100
+
101
+
102
+ export function fixHtmlTags(html: string, withReasoning: boolean, reasoningSwitchLabel: string) {
103
+ // Replace with a valid label
104
+ return html.replace('<think>', `<details class="think" ${withReasoning ? 'open' : ''}><summary>${reasoningSwitchLabel}</summary><div class="think-content">`)
105
+ .replace('</think>', '</div></details> ');
106
+ }
107
+
@@ -8,6 +8,7 @@ import { actualPropertyValue, getTextTop } from 'funda-utils/dist/cjs/inputsCalc
8
8
  import useDebounce from 'funda-utils/dist/cjs/useDebounce';
9
9
 
10
10
 
11
+
11
12
  export type TextareaProps = {
12
13
  contentRef?: React.ForwardedRef<any>; // could use "Array" on contentRef.current, such as contentRef.current[0], contentRef.current[1]
13
14
  wrapperClassName?: string;
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Stream Controller
3
+ *
4
+ * @usage:
5
+
6
+ // Use in component
7
+ const streamController = useStreamController({
8
+ onChunk: async (chunk: string, index: number) => {
9
+ // start (Execute it only once)
10
+ if (index === 0) {
11
+
12
+ }
13
+
14
+ // Streaming data is JSON split by rows
15
+ const lines = chunk.split("\n").filter(line => line.trim() !== "");
16
+
17
+ // Process each data chunk
18
+ console.log('Received chunk:', chunk);
19
+ },
20
+ onComplete: async (lastContent: string) => {
21
+ // Process when stream is completed
22
+ console.log('Stream completed');
23
+
24
+ // Display AI reply
25
+ console.log('AI reply:', lastContent);
26
+
27
+ },
28
+ onError: (error) => {
29
+ // Error handling
30
+ console.error('Stream error:', error);
31
+ },
32
+ onAbort: () => {
33
+ // Abort processing
34
+ console.log('Stream aborted');
35
+ }
36
+ });
37
+
38
+ // Start stream
39
+ const response = await fetch(url);
40
+ await streamController.start(response);
41
+
42
+ // Pause stream
43
+ streamController.pause();
44
+
45
+ // Resume stream
46
+ streamController.resume();
47
+
48
+ // Abort stream
49
+ streamController.abort();
50
+
51
+ // Check status
52
+ const isActive = streamController.isActive();
53
+ const isPaused = streamController.isPaused();
54
+
55
+ */
56
+
57
+
58
+ import { useRef, useCallback, useEffect } from 'react';
59
+
60
+ export interface StreamControllerOptions {
61
+ onChunk?: (chunk: string, index: number) => void;
62
+ onComplete?: (lastContent: string) => void;
63
+ onError?: (error: any) => void;
64
+ onAbort?: () => void;
65
+ }
66
+
67
+ export interface StreamController {
68
+ start: (response: Response) => Promise<void>;
69
+ pause: () => void;
70
+ resume: () => void;
71
+ abort: () => void;
72
+ isActive: () => boolean;
73
+ isPaused: () => boolean;
74
+ }
75
+
76
+ export const useStreamController = (options: StreamControllerOptions = {}): StreamController => {
77
+ const streamController = useRef<ReadableStreamDefaultController | null>(null);
78
+ const reader = useRef<ReadableStreamDefaultReader | null>(null);
79
+ const activeStream = useRef<ReadableStream | null>(null);
80
+ const responseReader = useRef<ReadableStreamDefaultReader | null>(null);
81
+ const paused = useRef<boolean>(false);
82
+ const active = useRef<boolean>(false);
83
+ const abortController = useRef<AbortController>(new AbortController());
84
+ const textDecoder = useRef<TextDecoder>(new TextDecoder("utf-8")); // Get the decoding of UTF8
85
+
86
+ // To avoid the "Uncaught (in promise) TypeError: Failed to execute 'cancel' on 'ReadableStream': Cannot cancel a locked stream" error,
87
+ // (1) you need to safely release the reader.
88
+ // (2) cleanup() also requires asynchronous state
89
+ const releaseReader = useCallback(async (readerRef: React.MutableRefObject<ReadableStreamDefaultReader | null>) => {
90
+ if (readerRef.current) {
91
+ try {
92
+ await readerRef.current.cancel();
93
+ } catch (e) {
94
+ console.warn('Error cancelling reader:', e);
95
+ }
96
+
97
+ try {
98
+ readerRef.current.releaseLock();
99
+ } catch (e) {
100
+ console.warn('Error releasing reader lock:', e);
101
+ }
102
+ readerRef.current = null;
103
+ }
104
+ }, []);
105
+
106
+ const cleanup = useCallback(async () => {
107
+ // First release all readers
108
+ await releaseReader(reader);
109
+ await releaseReader(responseReader);
110
+
111
+ // Then try to cancel the stream
112
+ if (activeStream.current) {
113
+ try {
114
+ await activeStream.current.cancel();
115
+ } catch (e) {
116
+ console.warn('Error cancelling stream:', e);
117
+ }
118
+ activeStream.current = null;
119
+ }
120
+
121
+ streamController.current = null;
122
+ active.current = false;
123
+ paused.current = false;
124
+ }, [releaseReader]);
125
+
126
+ // Process chunks of data
127
+ const processChunk = useCallback(async (chunk: string, index: number) => {
128
+ try {
129
+ options.onChunk?.(chunk, index);
130
+ } catch (error) {
131
+ options.onError?.(error);
132
+ }
133
+ }, [options]);
134
+
135
+ // Start processing the stream
136
+ const startProcessing = useCallback(async () => {
137
+ if (!reader.current || !active.current) return;
138
+
139
+ //
140
+ let counter = 0;
141
+
142
+
143
+ // Store the final content and bind it to loading
144
+ let lastContent: string = '';
145
+
146
+ while (active.current) {
147
+ try {
148
+
149
+ if (paused.current) {
150
+ await new Promise(resolve => setTimeout(resolve, 100));
151
+ continue;
152
+ }
153
+
154
+ const { done, value } = await reader.current.read();
155
+
156
+ if (done) {
157
+ options.onComplete?.(lastContent);
158
+ await cleanup();
159
+ break;
160
+ }
161
+
162
+ // Decode the content
163
+ const chunkStr = textDecoder.current.decode(value as Uint8Array, { stream: true });
164
+ lastContent += chunkStr;
165
+
166
+ await processChunk(chunkStr, counter);
167
+ counter++;
168
+
169
+ } catch (error: any) {
170
+ if (error.name === 'AbortError') {
171
+ options.onAbort?.();
172
+ } else {
173
+ options.onError?.(error);
174
+ }
175
+ await cleanup();
176
+ break;
177
+ }
178
+
179
+ }
180
+ }, [options, cleanup, processChunk]);
181
+
182
+
183
+ // Start streaming
184
+ const start = useCallback(async (response: Response) => {
185
+ await cleanup();
186
+
187
+ // Get Reader
188
+ reader.current = response.body!.getReader();
189
+
190
+ try {
191
+
192
+ const stream = new ReadableStream({
193
+ start(controller) {
194
+ streamController.current = controller;
195
+ },
196
+ async pull(controller) {
197
+ try {
198
+ const { done, value } = await reader.current!.read();
199
+
200
+ if (done) {
201
+ controller.close();
202
+ return;
203
+ }
204
+
205
+ // Decode the content
206
+ const chunkStr = textDecoder.current.decode(value as Uint8Array, { stream: true });
207
+
208
+ controller.enqueue(chunkStr);
209
+ } catch (error) {
210
+ controller.error(error);
211
+ }
212
+ },
213
+ cancel() {
214
+ response.body?.cancel();
215
+ }
216
+ });
217
+
218
+ activeStream.current = stream;
219
+ active.current = true;
220
+ paused.current = false;
221
+
222
+ // Start processing immediately
223
+ await startProcessing();
224
+ } catch (error) {
225
+ options.onError?.(error);
226
+ cleanup();
227
+ }
228
+
229
+
230
+ }, [options, cleanup, startProcessing]);
231
+
232
+
233
+
234
+ // Pause streaming
235
+ const pause = useCallback(() => {
236
+ paused.current = true;
237
+ }, []);
238
+
239
+ // Resume streaming
240
+ const resume = useCallback(() => {
241
+ paused.current = false;
242
+ }, []);
243
+
244
+ // Abort streaming
245
+ const abort = useCallback(async () => {
246
+ abortController.current.abort();
247
+ await cleanup();
248
+ }, [cleanup]);
249
+
250
+ // Check if stream is active
251
+ const isActive = useCallback(() => {
252
+ return active.current;
253
+ }, []);
254
+
255
+ // Check if stream is paused
256
+ const isPaused = useCallback(() => {
257
+ return paused.current;
258
+ }, []);
259
+
260
+ // Cleanup on unmount
261
+ useEffect(() => {
262
+ return () => {
263
+ cleanup().catch(console.error);
264
+ };
265
+ }, [cleanup]);
266
+
267
+ return {
268
+ start,
269
+ pause,
270
+ resume,
271
+ abort,
272
+ isActive,
273
+ isPaused
274
+ };
275
+ };
276
+
277
+ export default useStreamController;
package/lib/esm/index.js CHANGED
@@ -2,6 +2,7 @@ export { default as Accordion } from './Accordion';
2
2
  export { default as BackToTop } from './BackToTop';
3
3
  export { default as CascadingSelect } from './CascadingSelect';
4
4
  export { default as CascadingSelectE2E } from './CascadingSelectE2E';
5
+ export { default as Chatbox } from './Chatbox';
5
6
  export { default as Checkbox } from './Checkbox';
6
7
  export { default as ColorPicker } from './ColorPicker';
7
8
  export { default as Date } from './Date';
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.713",
5
+ "version": "4.5.767",
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",