funda-ui 4.6.111 → 4.6.222

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 {
@@ -142,8 +162,10 @@
142
162
  --custom-chatbox-toolkit-opt-border-color: #e9ecef;
143
163
  --custom-chatbox-toolkit-opt-active-color: #c2dfff;
144
164
  --custom-chatbox-toolkit-btn-radius: 20px;
145
-
146
-
165
+ --custom-chatbox-questions-bg: #f5f5f5;
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);
147
169
 
148
170
  min-width: var(--custom-chatbox-w);
149
171
  max-width: var(--custom-chatbox-w);
@@ -164,6 +186,8 @@
164
186
  padding: 0;
165
187
  font-size: 0.75rem;
166
188
  margin-bottom: .5rem;
189
+ background: var(--custom-chatbox-msg-bg);
190
+ padding: .5rem;
167
191
  }
168
192
 
169
193
  summary {
@@ -200,7 +224,7 @@
200
224
 
201
225
  /* message list */
202
226
  .messages {
203
- height: calc(100% - 80px);
227
+ height: calc(100% - 110px);
204
228
  overflow-y: auto;
205
229
  margin-bottom: 10px;
206
230
  font-size: 13px;
@@ -253,6 +277,43 @@
253
277
  margin-top: .3rem;
254
278
  display: inline-block;
255
279
  text-align: start;
280
+
281
+
282
+ /* Custom HTML Styles */
283
+ .table-container {
284
+ overflow-x: auto;
285
+ margin-bottom: .5rem;
286
+
287
+ &::-webkit-scrollbar {
288
+ height: 10px;
289
+ }
290
+
291
+ &::-webkit-scrollbar-thumb {
292
+ background: rgba(0, 0, 0, 0.2);
293
+ }
294
+
295
+ table {
296
+ width: 100%;
297
+ border-collapse: collapse;
298
+ border-radius: 0.35rem;
299
+
300
+
301
+ thead {
302
+ background: var(--custom-chatbox-content-html-elem-bg);
303
+
304
+ tr {
305
+ white-space: nowrap;
306
+ }
307
+ }
308
+
309
+ th, td {
310
+ padding: .25rem;
311
+ text-align: left;
312
+ border: 1px solid var(--custom-chatbox-content-html-elem-border-color);
313
+ }
314
+ }
315
+ }
316
+
256
317
  }
257
318
 
258
319
 
@@ -274,6 +335,8 @@
274
335
 
275
336
  .qa-content {
276
337
  width: var(--custom-chatbox-content-w);
338
+ background: transparent;
339
+ padding-top: 0;
277
340
  }
278
341
  }
279
342
 
@@ -455,6 +518,11 @@
455
518
  /* new chat button */
456
519
  .newchat-btn {
457
520
  text-align: center;
521
+ position: absolute;
522
+ bottom: 95px;
523
+ left: 50%;
524
+ transform: translateX(-50%);
525
+ z-index: 1;
458
526
 
459
527
  > button {
460
528
  padding: 3px 6px;
@@ -542,7 +610,7 @@
542
610
  display: flex;
543
611
  align-items: center;
544
612
  justify-content: space-between;
545
- border: 1px solid #ddd;
613
+ border: 1px solid var(--custom-chatbox-gray-color);
546
614
  border-radius: var(--custom-chatbox-toolkit-btn-radius);
547
615
  cursor: pointer;
548
616
  }
@@ -571,6 +639,7 @@
571
639
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
572
640
  margin-bottom: 10px;
573
641
  max-height: 300px;
642
+ min-width: 120px;
574
643
  overflow-y: auto;
575
644
  animation: dropupAnimation 0.2s ease;
576
645
 
@@ -605,6 +674,7 @@
605
674
  color: var(--custom-chatbox-gray-color);
606
675
  }
607
676
 
677
+ &.selected:not(.cancel),
608
678
  &:hover {
609
679
  background-color: var(--custom-chatbox-toolkit-opt-active-color);
610
680
  }
@@ -615,5 +685,35 @@
615
685
 
616
686
 
617
687
 
688
+ /* default questions */
689
+ .default-questions-title {
690
+ margin-bottom: .5rem;
691
+ }
692
+ .default-questions {
693
+ padding: 15px;
694
+ width: 100%;
695
+ }
696
+
697
+ .default-question-item {
698
+ padding: .3rem 1rem;
699
+ margin-bottom: .2rem;
700
+ background-color: var(--custom-chatbox-questions-bg);
701
+ border-radius: 0.35rem;
702
+ cursor: pointer;
703
+ transition: all 0.3s ease;
704
+
705
+ &:hover {
706
+ background-color: var(--custom-chatbox-questions-hover-bg);
707
+ }
708
+
709
+
710
+ &:last-child {
711
+ margin-bottom: 0;
712
+ }
713
+
714
+ }
715
+
716
+
717
+
618
718
 
619
719
  }
@@ -12,7 +12,6 @@ import useClickOutside from 'funda-utils/dist/cjs/useClickOutside';
12
12
  import { htmlEncode } from 'funda-utils/dist/cjs/sanitize';
13
13
 
14
14
 
15
-
16
15
  // loader
17
16
  import PureLoader from './PureLoader';
18
17
  import TypingEffect from "./TypingEffect";
@@ -39,11 +38,19 @@ export type MessageDetail = {
39
38
  tag: string; // such as '[reply]'
40
39
  };
41
40
 
41
+
42
+ export type QuestionData = {
43
+ title: string;
44
+ list: Array<string>;
45
+ };
46
+
47
+
42
48
  export interface FloatingButton {
43
49
  label: string; // HTML string
44
50
  value: string;
45
51
  onClick: string;
46
52
  isSelect?: boolean; // Mark whether it is a drop-down selection button
53
+ dynamicOptions?: boolean; // Mark whether to use dynamic options
47
54
  [key: string]: any; // Allows dynamic `onSelect__<number>` attributes, such as `onSelect__1`, `onSelect__2`, ...
48
55
  }
49
56
 
@@ -106,10 +113,12 @@ export type ChatboxProps = {
106
113
  toolkitButtons?: FloatingButton[];
107
114
  newChatButton?: FloatingButton;
108
115
  customMethods?: CustomMethod[]; // [{"name": "method1", "func": "() => { console.log('test'); }"}, ...]
116
+ defaultQuestions?: QuestionData;
109
117
  customRequest?: CustomRequestFunction;
110
118
  renderParser?: (input: string) => Promise<string>;
111
119
  requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => Promise<Record<string, any>>;
112
120
  nameFormatter?: (input: string) => string;
121
+ onQuestionClick?: (text: string, methods: Record<string, Function>) => void;
113
122
  onInputChange?: (controlRef: React.RefObject<any>, val: string) => any;
114
123
  onInputCallback?: (input: string) => Promise<string>;
115
124
  onChunk?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
@@ -192,8 +201,10 @@ const Chatbox = (props: ChatboxProps) => {
192
201
  setShow(false);
193
202
  },
194
203
  clearData: () => {
195
- setMsgList([]);
204
+ // Update both the conversation history and displayed messages
196
205
  conversationHistory.current = [];
206
+ setMsgList([]);
207
+
197
208
  },
198
209
  sendMsg: () => {
199
210
  handleClickSafe();
@@ -205,6 +216,11 @@ const Chatbox = (props: ChatboxProps) => {
205
216
  conversationHistory.current = conversationHistory.current.slice(-maxLength);
206
217
  }
207
218
  },
219
+ setHistory: (messages: MessageDetail[]) => {
220
+ // Update both the conversation history and displayed messages
221
+ conversationHistory.current = [...messages];
222
+ setMsgList([...messages]);
223
+ },
208
224
  setVal: (v: string) => {
209
225
  if (inputContentRef.current) inputContentRef.current.set(v);
210
226
  },
@@ -279,6 +295,7 @@ const Chatbox = (props: ChatboxProps) => {
279
295
  newChatButton,
280
296
  maxHistoryLength,
281
297
  customRequest,
298
+ onQuestionClick,
282
299
  renderParser,
283
300
  requestBodyFormatter,
284
301
  nameFormatter,
@@ -352,6 +369,7 @@ const Chatbox = (props: ChatboxProps) => {
352
369
  toolkitButtons,
353
370
  newChatButton,
354
371
  customRequest,
372
+ onQuestionClick,
355
373
  renderParser,
356
374
  requestBodyFormatter,
357
375
  nameFormatter,
@@ -361,6 +379,7 @@ const Chatbox = (props: ChatboxProps) => {
361
379
  onComplete,
362
380
 
363
381
  //
382
+ defaultQuestionsRes: questions,
364
383
  latestContextData,
365
384
  questionNameRes: _questionName,
366
385
  answerNameRes: _answerName,
@@ -375,6 +394,28 @@ const Chatbox = (props: ChatboxProps) => {
375
394
  }
376
395
 
377
396
 
397
+
398
+ //================================================================
399
+ // Custom Questions
400
+ //================================================================
401
+ const [questions, setQuestions] = useState<QuestionData | undefined>(props.defaultQuestions);
402
+ useEffect(() => {
403
+ if (props.defaultQuestions) {
404
+ setQuestions(props.defaultQuestions);
405
+ }
406
+ }, [props.defaultQuestions]);
407
+ const hasQuestion = () => {
408
+ return args().defaultQuestionsRes && (args().defaultQuestionsRes as QuestionData).list.length > 0;
409
+ };
410
+ const handleQuestionClick = (text: string) => {
411
+ if (inputContentRef.current) {
412
+ inputContentRef.current.set(text);
413
+ }
414
+
415
+ args().onQuestionClick?.(text, exposedMethods());
416
+ };
417
+
418
+
378
419
  //================================================================
379
420
  // Custom buttons
380
421
  //================================================================
@@ -393,16 +434,28 @@ const Chatbox = (props: ChatboxProps) => {
393
434
  return newState;
394
435
  });
395
436
  };
396
- const executeButtonAction = (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
437
+ const executeButtonAction = async (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
397
438
  try {
398
- // Create a new function to execute
399
439
  const actionFn = new Function('method', 'isActive', 'button', actionStr);
400
- /*
401
- function (method, isActive, button) {
402
- console.log('Clearing chat');
403
- method.clearData();
440
+
441
+ // !!!REQUIRED "await"
442
+ // "customMethods" may be asynchronous
443
+ const result = await actionFn(exposedMethods(), !activeButtons[buttonId], buttonElement);
444
+
445
+ // If the returned result is an array, it is a dynamic option
446
+ if (Array.isArray(result) && Object.keys(dynamicOptions).length === 0) {
447
+ const options: FloatingButtonSelectOption[] = result.map(item => {
448
+ const [key, value] = Object.entries(item)[0];
449
+ const [label, val, onClick] = (value as string).split('{#}').map((s: string) => s.trim());
450
+ return { label, value: val, onClick };
451
+ });
452
+
453
+ // Update dynamic options
454
+ setDynamicOptions(prev => ({
455
+ ...prev,
456
+ [buttonId]: options
457
+ }));
404
458
  }
405
- */
406
459
 
407
460
  // Update the button status
408
461
  const newState = !activeButtons[buttonId];
@@ -411,17 +464,28 @@ const Chatbox = (props: ChatboxProps) => {
411
464
  [buttonId]: newState
412
465
  }));
413
466
 
414
- return actionFn(exposedMethods(), newState, buttonElement);
415
-
416
-
467
+ return result;
417
468
  } catch (error) {
418
469
  console.error('Error executing button action:', error);
419
470
  }
420
471
  };
472
+
473
+
421
474
 
422
475
  // options
423
476
  const [selectedOpt, setSelectedOpt] = useState<Record<string, string | number>>({});
424
- const getButtonOptions = (btn: FloatingButton): FloatingButtonSelectOption[] => {
477
+ // Store dynamic options
478
+ const [dynamicOptions, setDynamicOptions] = useState<Record<string, FloatingButtonSelectOption[]>>({});
479
+
480
+ const getButtonOptions = (btn: FloatingButton, buttonId: string): FloatingButtonSelectOption[] => {
481
+ // If you are using the dynamic option and already have a cache, return the option for caching
482
+ //---------
483
+ if (btn.dynamicOptions && dynamicOptions[buttonId]) {
484
+ return dynamicOptions[buttonId];
485
+ }
486
+
487
+ // Use the static option from "props"
488
+ //---------
425
489
  const options: FloatingButtonSelectOption[] = [];
426
490
  let index = 1;
427
491
 
@@ -1124,7 +1188,7 @@ const Chatbox = (props: ChatboxProps) => {
1124
1188
  {/**------------- NO DATA -------------*/}
1125
1189
  {msgList.length === 0 ? <>
1126
1190
 
1127
- <div className="d-flex flex-column align-items-center justify-content-center h-50">
1191
+ <div className={`d-flex flex-column align-items-center justify-content-center ${hasQuestion() ? '' : 'h-50'}`}>
1128
1192
  <p>
1129
1193
  <svg width="70px" height="70px" viewBox="0 0 24 24" fill="none">
1130
1194
  <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.5997 2.37562 15.1116 3.04346 16.4525C3.22094 16.8088 3.28001 17.2161 3.17712 17.6006L2.58151 19.8267C2.32295 20.793 3.20701 21.677 4.17335 21.4185L6.39939 20.8229C6.78393 20.72 7.19121 20.7791 7.54753 20.9565C8.88837 21.6244 10.4003 22 12 22Z" stroke="#858297" strokeWidth="1.5" />
@@ -1134,7 +1198,26 @@ const Chatbox = (props: ChatboxProps) => {
1134
1198
 
1135
1199
  </p>
1136
1200
  <p className="text-primary" dangerouslySetInnerHTML={{ __html: `${args().noDataPlaceholder}` }}></p>
1201
+
1202
+ {/** DEFAULT QUESTIONS */}
1203
+ {hasQuestion() && (
1204
+ <div className="default-questions">
1205
+ <div className="default-questions-title" dangerouslySetInnerHTML={{ __html: `${(args().defaultQuestionsRes as QuestionData).title}` }}></div>
1206
+ {(args().defaultQuestionsRes as QuestionData).list?.map((question: string, index: number) => (
1207
+ <div
1208
+ key={index}
1209
+ className="default-question-item"
1210
+ onClick={() => handleQuestionClick(question)}
1211
+ dangerouslySetInnerHTML={{ __html: `${question}` }}
1212
+ />
1213
+ ))}
1214
+ </div>
1215
+ )}
1216
+ {/** /DEFAULT QUESTIONS */}
1217
+
1137
1218
  </div>
1219
+
1220
+
1138
1221
  </> : null}
1139
1222
  {/**------------- /NO DATA -------------*/}
1140
1223
 
@@ -1378,7 +1461,7 @@ const Chatbox = (props: ChatboxProps) => {
1378
1461
 
1379
1462
 
1380
1463
  {/**------------- SEND LOADING -------------*/}
1381
- {args().sendLoading ? <div className="loading"><div style={{ display: loading ? 'block' : 'none' }}><PureLoader customClassName="w-100" txt="" /></div></div> : null}
1464
+ {args().sendLoading ? <div className="loading"><div style={{ display: loading ? 'block' : 'none' }}><PureLoader prefix={args().prefix} customClassName="w-100" txt="" /></div></div> : null}
1382
1465
  {/**------------- /SEND LOADING -------------*/}
1383
1466
 
1384
1467
 
@@ -1390,8 +1473,8 @@ const Chatbox = (props: ChatboxProps) => {
1390
1473
  const isActive = activeButtons[_id];
1391
1474
 
1392
1475
  if (btn.isSelect) {
1393
- const options = getButtonOptions(btn);
1394
-
1476
+ const options = getButtonOptions(btn, _id);
1477
+
1395
1478
  return (
1396
1479
  <div key={index} className="toolkit-select-wrapper">
1397
1480
  <button
@@ -1403,6 +1486,9 @@ const Chatbox = (props: ChatboxProps) => {
1403
1486
  ...prev,
1404
1487
  [_id]: !prev[_id]
1405
1488
  }));
1489
+
1490
+ //
1491
+ executeButtonAction(btn.onClick, _id, e.currentTarget);
1406
1492
  }}
1407
1493
  >
1408
1494
  <span dangerouslySetInnerHTML={{
@@ -1421,18 +1507,23 @@ const Chatbox = (props: ChatboxProps) => {
1421
1507
  </svg></span>
1422
1508
  </button>
1423
1509
 
1424
-
1425
-
1510
+ {/* OPTIONS */}
1426
1511
  <div className={`toolkit-select-options ${isActive ? 'active' : ''}`}>
1427
- {options.map((option: FloatingButtonSelectOption, optIndex: number) => (
1428
- <div
1429
- key={optIndex}
1430
- className={`toolkit-select-option ${option.value || ''} ${selectedOpt.curIndex === optIndex ? 'selected' : ''}`}
1431
- onClick={() => handleExecuteButtonSelect(_id, option, optIndex, option.value)}
1432
- >
1433
- <span dangerouslySetInnerHTML={{ __html: option.label }}></span>
1434
- </div>
1435
- ))}
1512
+
1513
+ {options.length > 0 ? <>
1514
+ {options.map((option: FloatingButtonSelectOption, optIndex: number) => (
1515
+ <div
1516
+ key={optIndex}
1517
+ className={`toolkit-select-option ${option.value || ''} ${selectedOpt.curIndex === optIndex ? 'selected' : ''}`}
1518
+ onClick={() => handleExecuteButtonSelect(_id, option, optIndex, option.value)}
1519
+ >
1520
+ <span dangerouslySetInnerHTML={{ __html: option.label }}></span>
1521
+ </div>
1522
+ ))}
1523
+ </> : <>
1524
+ <div className={`${args().prefix || 'custom-'}chatbox-mini-loader`}></div>
1525
+ </>}
1526
+
1436
1527
  </div>
1437
1528
  </div>
1438
1529
  );
@@ -1444,8 +1535,7 @@ const Chatbox = (props: ChatboxProps) => {
1444
1535
  key={index}
1445
1536
  id={_id}
1446
1537
  className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
1447
- onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
1448
- executeButtonAction(btn.onClick, _id, e.currentTarget)}
1538
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => executeButtonAction(btn.onClick, _id, e.currentTarget)}
1449
1539
  >
1450
1540
  <span dangerouslySetInnerHTML={{ __html: btn.label }}></span>
1451
1541
  </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
+ };