funda-ui 4.6.151 → 4.6.333

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.
@@ -2,49 +2,69 @@
2
2
 
3
3
  /*=================== Chatbox (Loading) =================*/
4
4
  .custom-chatbox-loader-container {
5
+ --custom-chatbox-loader-color: #b9caf7;
6
+ --custom-chatbox-loader-color2: #0d6efd;
7
+
8
+
5
9
  width: 130px;
6
10
  text-align: start;
7
11
 
8
12
  .custom-chatbox-loader {
9
13
  height: 4px;
10
14
  width: 100%;
11
- --c: no-repeat linear-gradient(var(--bs-primary) 0 0);
12
- background: var(--c), var(--c), #b9caf7;
15
+ --c: no-repeat linear-gradient(var(--custom-chatbox-loader-color2) 0 0);
16
+ background: var(--c), var(--c), var(--custom-chatbox-loader-color);
13
17
  background-size: 60% 100%;
14
- animation: cssAnim--loadermove 3s infinite;
18
+ animation: loader-move 3s infinite;
15
19
  }
16
- }
17
20
 
18
21
 
19
- .dark-mode,
20
- [data-bs-theme=dark] {
21
-
22
- .custom-chatbox-loader-container {
23
- .custom-chatbox-loader {
24
- --c: no-repeat linear-gradient(var(--bs-primary) 0 0);
25
- background: var(--c), var(--c), #8692b5;
22
+ @keyframes loader-move {
23
+ 0% {
24
+ background-position: -150% 0, -150% 0
25
+ }
26
+
27
+ 66% {
28
+ background-position: 250% 0, -150% 0
29
+ }
30
+
31
+ 100% {
32
+ background-position: 250% 0, 250% 0
26
33
  }
27
34
  }
28
-
29
35
  }
30
36
 
31
37
 
32
- @keyframes cssAnim--loadermove {
33
- 0% {
34
- background-position: -150% 0, -150% 0
35
- }
36
38
 
37
- 66% {
38
- background-position: 250% 0, -150% 0
39
- }
39
+ .custom-chatbox-mini-loader {
40
+ --custom-chatbox-miniloader-color: rgba(0,0,0,.5);
41
+
42
+ width: 15px;
43
+ height: 15px;
44
+ margin: .5rem;
45
+ margin-bottom: 0;
46
+ border: 3px dotted var(--custom-chatbox-miniloader-color);
47
+ border-radius: 50%;
48
+ display: inline-block;
49
+ position: relative;
50
+ box-sizing: border-box;
51
+ animation: mini-loader-spin 1s linear infinite;
52
+
53
+ @keyframes mini-loader-spin {
54
+ 0% {
55
+ transform: rotate(0deg);
56
+ }
57
+
58
+ 100% {
59
+ transform: rotate(360deg);
60
+ }
40
61
 
41
- 100% {
42
- background-position: 250% 0, 250% 0
43
62
  }
44
63
  }
45
64
 
46
65
 
47
66
 
67
+
48
68
  /*=================== Chatbox (Core) =================*/
49
69
 
50
70
  .custom-chatbox-circle {
@@ -144,6 +164,9 @@
144
164
  --custom-chatbox-toolkit-btn-radius: 20px;
145
165
  --custom-chatbox-questions-bg: #f5f5f5;
146
166
  --custom-chatbox-questions-hover-bg: #e9e9e9;
167
+ --custom-chatbox-content-html-elem-border-color: #ddd;
168
+ --custom-chatbox-content-html-elem-bg: rgba(0,0,0,.05);
169
+
147
170
 
148
171
 
149
172
  min-width: var(--custom-chatbox-w);
@@ -165,6 +188,8 @@
165
188
  padding: 0;
166
189
  font-size: 0.75rem;
167
190
  margin-bottom: .5rem;
191
+ background: var(--custom-chatbox-msg-bg);
192
+ padding: .5rem;
168
193
  }
169
194
 
170
195
  summary {
@@ -201,7 +226,7 @@
201
226
 
202
227
  /* message list */
203
228
  .messages {
204
- height: calc(100% - 80px);
229
+ height: calc(100% - 110px);
205
230
  overflow-y: auto;
206
231
  margin-bottom: 10px;
207
232
  font-size: 13px;
@@ -220,10 +245,11 @@
220
245
  }
221
246
 
222
247
 
223
- > div {
248
+ > div:not(.newchat-btn) {
224
249
  margin: 5px 0;
225
250
  padding: 3px 5px;
226
251
  border-radius: 0.35rem;
252
+ position: relative;
227
253
  }
228
254
 
229
255
  p {
@@ -254,6 +280,45 @@
254
280
  margin-top: .3rem;
255
281
  display: inline-block;
256
282
  text-align: start;
283
+
284
+
285
+ /* Custom HTML Styles */
286
+ .table-container {
287
+ overflow-x: auto;
288
+ margin-bottom: .5rem;
289
+
290
+ &::-webkit-scrollbar {
291
+ height: 10px;
292
+ }
293
+
294
+ &::-webkit-scrollbar-thumb {
295
+ background: rgba(0, 0, 0, 0.2);
296
+ }
297
+
298
+ table {
299
+ width: 100%;
300
+ border-collapse: collapse;
301
+ border-radius: 0.35rem;
302
+
303
+
304
+
305
+ thead {
306
+ background: var(--custom-chatbox-content-html-elem-bg);
307
+
308
+ tr {
309
+ white-space: nowrap;
310
+ }
311
+ }
312
+
313
+ th, td {
314
+ padding: .25rem;
315
+ text-align: left;
316
+ border: 1px solid var(--custom-chatbox-content-html-elem-border-color);
317
+ }
318
+ }
319
+ }
320
+
321
+
257
322
  }
258
323
 
259
324
 
@@ -275,6 +340,8 @@
275
340
 
276
341
  .qa-content {
277
342
  width: var(--custom-chatbox-content-w);
343
+ background: transparent;
344
+ padding-top: 0;
278
345
  }
279
346
  }
280
347
 
@@ -283,6 +350,26 @@
283
350
 
284
351
  }
285
352
  }
353
+
354
+
355
+ /* copy button */
356
+ .copy-btn {
357
+ position: absolute;
358
+ left: calc(var(--custom-chatbox-content-w) - .7rem);
359
+ bottom: 0.5rem;
360
+ z-index: 1;
361
+ background: transparent;
362
+ border: none;
363
+ padding: 4px;
364
+ cursor: pointer;
365
+ opacity: 0.6;
366
+ transition: opacity 0.2s;
367
+
368
+ &:hover {
369
+ opacity: 1;
370
+ }
371
+ }
372
+
286
373
  }
287
374
 
288
375
  /* dot loading */
@@ -456,7 +543,12 @@
456
543
  /* new chat button */
457
544
  .newchat-btn {
458
545
  text-align: center;
459
-
546
+ position: absolute;
547
+ bottom: 95px;
548
+ left: 50%;
549
+ transform: translateX(-50%);
550
+ z-index: 1;
551
+
460
552
  > button {
461
553
  padding: 3px 6px;
462
554
  background-color: var(--custom-chatbox-newchat-btn-color);
@@ -572,6 +664,7 @@
572
664
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
573
665
  margin-bottom: 10px;
574
666
  max-height: 300px;
667
+ min-width: 120px;
575
668
  overflow-y: auto;
576
669
  animation: dropupAnimation 0.2s ease;
577
670
 
@@ -606,6 +699,7 @@
606
699
  color: var(--custom-chatbox-gray-color);
607
700
  }
608
701
 
702
+ &.selected:not(.cancel),
609
703
  &:hover {
610
704
  background-color: var(--custom-chatbox-toolkit-opt-active-color);
611
705
  }
@@ -614,8 +708,6 @@
614
708
  }
615
709
 
616
710
 
617
-
618
-
619
711
  /* default questions */
620
712
  .default-questions-title {
621
713
  margin-bottom: .5rem;
@@ -643,8 +735,6 @@
643
735
  }
644
736
 
645
737
  }
646
-
647
-
648
738
 
649
739
 
650
740
  }
@@ -51,6 +51,7 @@ export interface FloatingButton {
51
51
  value: string;
52
52
  onClick: string;
53
53
  isSelect?: boolean; // Mark whether it is a drop-down selection button
54
+ dynamicOptions?: boolean; // Mark whether to use dynamic options
54
55
  [key: string]: any; // Allows dynamic `onSelect__<number>` attributes, such as `onSelect__1`, `onSelect__2`, ...
55
56
  }
56
57
 
@@ -114,10 +115,14 @@ export type ChatboxProps = {
114
115
  newChatButton?: FloatingButton;
115
116
  customMethods?: CustomMethod[]; // [{"name": "method1", "func": "() => { console.log('test'); }"}, ...]
116
117
  defaultQuestions?: QuestionData;
118
+ showCopyBtn?: boolean; // Whether to show copy button for each reply
119
+ autoCopyReply?: boolean; // Whether to automatically copy reply to clipboard
117
120
  customRequest?: CustomRequestFunction;
118
121
  renderParser?: (input: string) => Promise<string>;
119
122
  requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => Promise<Record<string, any>>;
123
+ copiedContentFormatter?: (string: string) => string;
120
124
  nameFormatter?: (input: string) => string;
125
+ onCopyCallback?: (res: Record<string, any>) => void;
121
126
  onQuestionClick?: (text: string, methods: Record<string, Function>) => void;
122
127
  onInputChange?: (controlRef: React.RefObject<any>, val: string) => any;
123
128
  onInputCallback?: (input: string) => Promise<string>;
@@ -201,8 +206,10 @@ const Chatbox = (props: ChatboxProps) => {
201
206
  setShow(false);
202
207
  },
203
208
  clearData: () => {
204
- setMsgList([]);
209
+ // Update both the conversation history and displayed messages
205
210
  conversationHistory.current = [];
211
+ setMsgList([]);
212
+
206
213
  },
207
214
  sendMsg: () => {
208
215
  handleClickSafe();
@@ -214,6 +221,11 @@ const Chatbox = (props: ChatboxProps) => {
214
221
  conversationHistory.current = conversationHistory.current.slice(-maxLength);
215
222
  }
216
223
  },
224
+ setHistory: (messages: MessageDetail[]) => {
225
+ // Update both the conversation history and displayed messages
226
+ conversationHistory.current = [...messages];
227
+ setMsgList([...messages]);
228
+ },
217
229
  setVal: (v: string) => {
218
230
  if (inputContentRef.current) inputContentRef.current.set(v);
219
231
  },
@@ -289,9 +301,13 @@ const Chatbox = (props: ChatboxProps) => {
289
301
  maxHistoryLength,
290
302
  customRequest,
291
303
  onQuestionClick,
304
+ onCopyCallback,
292
305
  renderParser,
293
306
  requestBodyFormatter,
307
+ copiedContentFormatter,
294
308
  nameFormatter,
309
+ showCopyBtn,
310
+ autoCopyReply,
295
311
  onInputChange,
296
312
  onInputCallback,
297
313
  onChunk,
@@ -363,9 +379,13 @@ const Chatbox = (props: ChatboxProps) => {
363
379
  newChatButton,
364
380
  customRequest,
365
381
  onQuestionClick,
382
+ onCopyCallback,
366
383
  renderParser,
367
384
  requestBodyFormatter,
385
+ copiedContentFormatter,
368
386
  nameFormatter,
387
+ showCopyBtn,
388
+ autoCopyReply,
369
389
  onInputChange,
370
390
  onInputCallback,
371
391
  onChunk,
@@ -386,6 +406,57 @@ const Chatbox = (props: ChatboxProps) => {
386
406
 
387
407
  }
388
408
 
409
+ //================================================================
410
+ // Clipboard
411
+ //================================================================
412
+ const chatboxCopyToClipboard = async (text: string) => {
413
+
414
+ let _content: string = text;
415
+ if (typeof args().copiedContentFormatter === 'function') {
416
+ _content = args().copiedContentFormatter(text);
417
+ }
418
+
419
+ try {
420
+ // Try using the modern Clipboard API first
421
+ if (navigator.clipboard && window.isSecureContext) {
422
+ await navigator.clipboard.writeText(_content);
423
+ args().onCopyCallback?.({
424
+ success: true,
425
+ message: 'Text copied to clipboard',
426
+ });
427
+ return true;
428
+ }
429
+
430
+ // Fallback for older browsers
431
+ const textArea = document.createElement('textarea');
432
+ textArea.value = _content;
433
+ textArea.style.position = 'fixed';
434
+ textArea.style.left = '-999999px';
435
+ textArea.style.top = '-999999px';
436
+ document.body.appendChild(textArea);
437
+ textArea.focus();
438
+ textArea.select();
439
+
440
+ try {
441
+ document.execCommand('copy');
442
+ textArea.remove();
443
+ args().onCopyCallback?.({
444
+ success: true,
445
+ message: 'Text copied to clipboard',
446
+ });
447
+ return true;
448
+ } catch (err) {
449
+ textArea.remove();
450
+ return false;
451
+ }
452
+ } catch (err) {
453
+ args().onCopyCallback?.({
454
+ success: false,
455
+ message: `Failed to copy text: ${err}`,
456
+ });
457
+ return false;
458
+ }
459
+ };
389
460
 
390
461
 
391
462
  //================================================================
@@ -427,16 +498,28 @@ const Chatbox = (props: ChatboxProps) => {
427
498
  return newState;
428
499
  });
429
500
  };
430
- const executeButtonAction = (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
501
+ const executeButtonAction = async (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
431
502
  try {
432
- // Create a new function to execute
433
503
  const actionFn = new Function('method', 'isActive', 'button', actionStr);
434
- /*
435
- function (method, isActive, button) {
436
- console.log('Clearing chat');
437
- method.clearData();
504
+
505
+ // !!!REQUIRED "await"
506
+ // "customMethods" may be asynchronous
507
+ const result = await actionFn(exposedMethods(), !activeButtons[buttonId], buttonElement);
508
+
509
+ // If the returned result is an array, it is a dynamic option
510
+ if (Array.isArray(result) && Object.keys(dynamicOptions).length === 0) {
511
+ const options: FloatingButtonSelectOption[] = result.map(item => {
512
+ const [key, value] = Object.entries(item)[0];
513
+ const [label, val, onClick] = (value as string).split('{#}').map((s: string) => s.trim());
514
+ return { label, value: val, onClick };
515
+ });
516
+
517
+ // Update dynamic options
518
+ setDynamicOptions(prev => ({
519
+ ...prev,
520
+ [buttonId]: options
521
+ }));
438
522
  }
439
- */
440
523
 
441
524
  // Update the button status
442
525
  const newState = !activeButtons[buttonId];
@@ -445,17 +528,28 @@ const Chatbox = (props: ChatboxProps) => {
445
528
  [buttonId]: newState
446
529
  }));
447
530
 
448
- return actionFn(exposedMethods(), newState, buttonElement);
449
-
450
-
531
+ return result;
451
532
  } catch (error) {
452
533
  console.error('Error executing button action:', error);
453
534
  }
454
535
  };
536
+
537
+
455
538
 
456
539
  // options
457
540
  const [selectedOpt, setSelectedOpt] = useState<Record<string, string | number>>({});
458
- const getButtonOptions = (btn: FloatingButton): FloatingButtonSelectOption[] => {
541
+ // Store dynamic options
542
+ const [dynamicOptions, setDynamicOptions] = useState<Record<string, FloatingButtonSelectOption[]>>({});
543
+
544
+ const getButtonOptions = (btn: FloatingButton, buttonId: string): FloatingButtonSelectOption[] => {
545
+ // If you are using the dynamic option and already have a cache, return the option for caching
546
+ //---------
547
+ if (btn.dynamicOptions && dynamicOptions[buttonId]) {
548
+ return dynamicOptions[buttonId];
549
+ }
550
+
551
+ // Use the static option from "props"
552
+ //---------
459
553
  const options: FloatingButtonSelectOption[] = [];
460
554
  let index = 1;
461
555
 
@@ -790,6 +884,11 @@ const Chatbox = (props: ChatboxProps) => {
790
884
  // Update the message list state
791
885
  setMsgList((prevMessages) => [...prevMessages, newMessage]);
792
886
 
887
+ // Auto copy reply if enabled
888
+ if (args().autoCopyReply && sender === args().answerNameRes) {
889
+ chatboxCopyToClipboard(content);
890
+ }
891
+
793
892
  };
794
893
 
795
894
  const sendMessage = async () => {
@@ -1125,7 +1224,10 @@ const Chatbox = (props: ChatboxProps) => {
1125
1224
  }
1126
1225
  }, [props.defaultMessages]);
1127
1226
 
1128
-
1227
+ useEffect(() => {
1228
+ // Bind chatboxCopyToClipboard to window so it can be called in HTML code
1229
+ (window as any).chatboxCopyToClipboard = chatboxCopyToClipboard;
1230
+ }, []);
1129
1231
 
1130
1232
  return (
1131
1233
  <>
@@ -1190,39 +1292,42 @@ const Chatbox = (props: ChatboxProps) => {
1190
1292
 
1191
1293
  </> : null}
1192
1294
  {/**------------- /NO DATA -------------*/}
1193
-
1194
-
1295
+
1195
1296
 
1196
1297
  {/**------------- MESSAGES LIST -------------*/}
1197
1298
  <div className="messages" ref={msgContainerRef}>
1198
1299
 
1199
1300
  {msgList.map((msg, index) => {
1200
1301
 
1302
+ const copyTargetId = `${args().prefix || 'custom-'}chatbox-content--${chatId}${index}`;
1201
1303
  const isAnimProgress = tempAnimText !== '' && msg.sender !== args().questionNameRes && index === msgList.length - 1 && loading;
1202
1304
  const hasAnimated = animatedMessagesRef.current.has(index);
1203
1305
 
1204
1306
  // Mark the message as animated;
1205
1307
  animatedMessagesRef.current.add(index);
1206
1308
 
1309
+ const timeShow = `<span class="qa-timestamp">${msg.timestamp}</span>${args().showCopyBtn && msg.tag?.indexOf('[reply]') >= 0 ?(`<button class="copy-btn" onclick="window.chatboxCopyToClipboard(document.querySelector('#${copyTargetId} .qa-content-inner').innerHTML)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M8 4v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7.242a2 2 0 0 0-.602-1.43L16.083 2.57A2 2 0 0 0 14.685 2H10a2 2 0 0 0-2 2z"/><path d="M16 18v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h2"/></svg></button>`) : ''}`;
1310
+
1207
1311
  return <div key={index} className={msg.tag?.indexOf('[reply]') < 0 ? 'request' : 'reply'} style={{ display: isAnimProgress ? 'none' : '' }}>
1208
1312
  <div className="qa-name" dangerouslySetInnerHTML={{ __html: `${msg.sender}` }}></div>
1209
1313
 
1314
+
1210
1315
  {msg.sender === args().questionNameRes ? <>
1211
- <div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
1316
+ <div className="qa-content" id={copyTargetId} dangerouslySetInnerHTML={{ __html: `<div class="qa-content-inner">${msg.content}</div> ${timeShow}` }}></div>
1212
1317
  </> : <>
1213
1318
 
1214
1319
  {enableStreamMode ? <>
1215
- <div className="qa-content" dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
1320
+ <div className="qa-content" id={copyTargetId} dangerouslySetInnerHTML={{ __html: `<div class="qa-content-inner">${msg.content}</div> ${timeShow}` }}></div>
1216
1321
  </> : <>
1217
- <div className="qa-content">
1322
+ <div className="qa-content" id={copyTargetId}>
1218
1323
  {hasAnimated ? (
1219
- <div dangerouslySetInnerHTML={{ __html: `${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>` }}></div>
1324
+ <div dangerouslySetInnerHTML={{ __html: `<div class="qa-content-inner">${msg.content}</div> ${timeShow}` }}></div>
1220
1325
  ) : (
1221
1326
  <TypingEffect
1222
1327
  onUpdate={() => {
1223
1328
  scrollToBottom();
1224
1329
  }}
1225
- content={`${msg.content} <span class="qa-timestamp">${msg.timestamp}</span>`}
1330
+ content={`<div class="qa-content-inner">${msg.content}</div> ${timeShow}`}
1226
1331
  speed={10}
1227
1332
  />
1228
1333
  )}
@@ -1431,7 +1536,7 @@ const Chatbox = (props: ChatboxProps) => {
1431
1536
 
1432
1537
 
1433
1538
  {/**------------- SEND LOADING -------------*/}
1434
- {args().sendLoading ? <div className="loading"><div style={{ display: loading ? 'block' : 'none' }}><PureLoader customClassName="w-100" txt="" /></div></div> : null}
1539
+ {args().sendLoading ? <div className="loading"><div style={{ display: loading ? 'block' : 'none' }}><PureLoader prefix={args().prefix} customClassName="w-100" txt="" /></div></div> : null}
1435
1540
  {/**------------- /SEND LOADING -------------*/}
1436
1541
 
1437
1542
 
@@ -1443,8 +1548,8 @@ const Chatbox = (props: ChatboxProps) => {
1443
1548
  const isActive = activeButtons[_id];
1444
1549
 
1445
1550
  if (btn.isSelect) {
1446
- const options = getButtonOptions(btn);
1447
-
1551
+ const options = getButtonOptions(btn, _id);
1552
+
1448
1553
  return (
1449
1554
  <div key={index} className="toolkit-select-wrapper">
1450
1555
  <button
@@ -1456,6 +1561,9 @@ const Chatbox = (props: ChatboxProps) => {
1456
1561
  ...prev,
1457
1562
  [_id]: !prev[_id]
1458
1563
  }));
1564
+
1565
+ //
1566
+ executeButtonAction(btn.onClick, _id, e.currentTarget);
1459
1567
  }}
1460
1568
  >
1461
1569
  <span dangerouslySetInnerHTML={{
@@ -1474,18 +1582,23 @@ const Chatbox = (props: ChatboxProps) => {
1474
1582
  </svg></span>
1475
1583
  </button>
1476
1584
 
1477
-
1478
-
1585
+ {/* OPTIONS */}
1479
1586
  <div className={`toolkit-select-options ${isActive ? 'active' : ''}`}>
1480
- {options.map((option: FloatingButtonSelectOption, optIndex: number) => (
1481
- <div
1482
- key={optIndex}
1483
- className={`toolkit-select-option ${option.value || ''} ${selectedOpt.curIndex === optIndex ? 'selected' : ''}`}
1484
- onClick={() => handleExecuteButtonSelect(_id, option, optIndex, option.value)}
1485
- >
1486
- <span dangerouslySetInnerHTML={{ __html: option.label }}></span>
1487
- </div>
1488
- ))}
1587
+
1588
+ {options.length > 0 ? <>
1589
+ {options.map((option: FloatingButtonSelectOption, optIndex: number) => (
1590
+ <div
1591
+ key={optIndex}
1592
+ className={`toolkit-select-option ${option.value || ''} ${selectedOpt.curIndex === optIndex ? 'selected' : ''}`}
1593
+ onClick={() => handleExecuteButtonSelect(_id, option, optIndex, option.value)}
1594
+ >
1595
+ <span dangerouslySetInnerHTML={{ __html: option.label }}></span>
1596
+ </div>
1597
+ ))}
1598
+ </> : <>
1599
+ <div className={`${args().prefix || 'custom-'}chatbox-mini-loader`}></div>
1600
+ </>}
1601
+
1489
1602
  </div>
1490
1603
  </div>
1491
1604
  );
@@ -1497,8 +1610,7 @@ const Chatbox = (props: ChatboxProps) => {
1497
1610
  key={index}
1498
1611
  id={_id}
1499
1612
  className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
1500
- onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
1501
- executeButtonAction(btn.onClick, _id, e.currentTarget)}
1613
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(btn.onClick, _id, e.currentTarget)}
1502
1614
  >
1503
1615
  <span dangerouslySetInnerHTML={{ __html: btn.label }}></span>
1504
1616
  </button>
@@ -1,6 +1,11 @@
1
1
  // app.ts
2
2
  import type { ChatboxProps } from '../index';
3
3
 
4
+ export interface HtmlTagPlaceholder {
5
+ original: string;
6
+ placeholder: string;
7
+ type: 'table' | 'img' | 'svg';
8
+ }
4
9
 
5
10
  export function isValidJSON(str: string){
6
11
  try {
@@ -13,7 +18,7 @@ export function isValidJSON(str: string){
13
18
 
14
19
  export function formatLatestDisplayContent(str: string) {
15
20
  // Regular expression to match <details> tags and their content
16
- const output = str.replace(/<details class="think"[^>]*>([\s\S]*?)<\/details>/g, (match, content) => {
21
+ let output = str.replace(/<details class="think"[^>]*>([\s\S]*?)<\/details>/g, (match, content) => {
17
22
  // Use regex to match the content inside the "div.think-content"
18
23
  const thinkContentMatch = content.match(/<div class="think-content">([\s\S]*?)<\/div>/);
19
24
 
@@ -28,6 +33,12 @@ export function formatLatestDisplayContent(str: string) {
28
33
 
29
34
  return match; // If not empty, return the original matched content
30
35
  });
36
+
37
+ // Then handle tables without is-init class
38
+ output = output.replace(/<table(?![^>]*\bis-init\b)([^>]*)>([\s\S]*?)<\/table>/g, (match, attributes, content) => {
39
+ // Add is-init class to table and wrap it in container div
40
+ return `<div class="table-container"><table class="is-init"${attributes}>${content}</table></div>`;
41
+ });
31
42
 
32
43
  return output;
33
44
  }
@@ -127,3 +138,43 @@ export function isStreamResponse(response: Response): boolean {
127
138
  return response.body instanceof ReadableStream;
128
139
  };
129
140
 
141
+
142
+ export function extractHtmlTags(html: string): { processedHtml: string; placeholders: HtmlTagPlaceholder[] } {
143
+ const placeholders: HtmlTagPlaceholder[] = [];
144
+ let processedHtml = html;
145
+
146
+ // <table>
147
+ processedHtml = processedHtml.replace(/<table[^>]*>[\s\S]*?<\/table>/g, (match) => {
148
+ const placeholder = `[TABLE_${placeholders.length}]`;
149
+ placeholders.push({
150
+ original: `<div class="table-container">${match?.replace('<table', '<table class="is-init"')}</div>`,
151
+ placeholder,
152
+ type: 'table'
153
+ });
154
+ return placeholder;
155
+ });
156
+
157
+ // <img>
158
+ processedHtml = processedHtml.replace(/<img[^>]*>/g, (match) => {
159
+ const placeholder = `[IMG_${placeholders.length}]`;
160
+ placeholders.push({
161
+ original: match,
162
+ placeholder,
163
+ type: 'img'
164
+ });
165
+ return placeholder;
166
+ });
167
+
168
+ // <svg>
169
+ processedHtml = processedHtml.replace(/<svg[^>]*>[\s\S]*?<\/svg>/g, (match) => {
170
+ const placeholder = `[SVG_${placeholders.length}]`;
171
+ placeholders.push({
172
+ original: match,
173
+ placeholder,
174
+ type: 'svg'
175
+ });
176
+ return placeholder;
177
+ });
178
+
179
+ return { processedHtml, placeholders };
180
+ };