streaming-markdown-react 0.1.4 → 0.2.0

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.
package/dist/index.mjs CHANGED
@@ -64,8 +64,7 @@ var MessageBlockStore = class {
64
64
  var messageBlockStore = new MessageBlockStore();
65
65
 
66
66
  // src/components/Markdown/StreamingMarkdown.tsx
67
- import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState2 } from "react";
68
- import ReactMarkdown from "react-markdown";
67
+ import { memo as memo3, useEffect as useEffect3, useId, useMemo, useRef as useRef2, useState as useState2 } from "react";
69
68
  import remarkGfm from "remark-gfm";
70
69
 
71
70
  // src/hooks/useSmoothStream.ts
@@ -179,6 +178,124 @@ function useSmoothStream({
179
178
  return { addChunk, reset };
180
179
  }
181
180
 
181
+ // src/utils/markdown/parseMarkdownIntoBlocks.ts
182
+ import { Lexer } from "marked";
183
+ var blockIdCounter = 0;
184
+ function generateBlockId() {
185
+ blockIdCounter += 1;
186
+ return `block-${Date.now()}-${blockIdCounter}`;
187
+ }
188
+ function parseMarkdownIntoBlocks(markdown) {
189
+ if (!markdown.trim()) {
190
+ return [];
191
+ }
192
+ const hasFootnoteReference = /\[\^[^\]\s]{1,200}\](?!:)/.test(markdown);
193
+ const hasFootnoteDefinition = /\[\^[^\]\s]{1,200}\]:/.test(markdown);
194
+ if (hasFootnoteReference || hasFootnoteDefinition) {
195
+ return [markdown];
196
+ }
197
+ const tokens = Lexer.lex(markdown, { gfm: true });
198
+ const mergedBlocks = [];
199
+ const htmlStack = [];
200
+ for (let i = 0; i < tokens.length; i++) {
201
+ const token = tokens[i];
202
+ const currentBlock = token.raw;
203
+ if (htmlStack.length > 0) {
204
+ mergedBlocks[mergedBlocks.length - 1] += currentBlock;
205
+ if (token.type === "html") {
206
+ const closingTagMatch = currentBlock.match(/<\/(\w+)>/);
207
+ if (closingTagMatch) {
208
+ const closingTag = closingTagMatch[1];
209
+ if (htmlStack[htmlStack.length - 1] === closingTag) {
210
+ htmlStack.pop();
211
+ }
212
+ }
213
+ }
214
+ continue;
215
+ }
216
+ if (token.type === "html" && token.block) {
217
+ const openingTagMatch = currentBlock.match(/<(\w+)[\s>]/);
218
+ if (openingTagMatch) {
219
+ const tagName = openingTagMatch[1];
220
+ const hasClosingTag = currentBlock.includes(`</${tagName}>`);
221
+ if (!hasClosingTag) {
222
+ htmlStack.push(tagName);
223
+ }
224
+ }
225
+ }
226
+ if (currentBlock.trim() === "$" && mergedBlocks.length > 0) {
227
+ const previousBlock = mergedBlocks.at(-1);
228
+ if (!previousBlock) {
229
+ mergedBlocks.push(currentBlock);
230
+ continue;
231
+ }
232
+ const prevStartsWith$ = previousBlock.trimStart().startsWith("$");
233
+ const prevDollarCount = (previousBlock.match(/\$\$/g) || []).length;
234
+ if (prevStartsWith$ && prevDollarCount % 2 === 1) {
235
+ mergedBlocks[mergedBlocks.length - 1] = previousBlock + currentBlock;
236
+ continue;
237
+ }
238
+ }
239
+ if (mergedBlocks.length > 0 && currentBlock.trimEnd().endsWith("$")) {
240
+ const previousBlock = mergedBlocks.at(-1);
241
+ if (!previousBlock) {
242
+ mergedBlocks.push(currentBlock);
243
+ continue;
244
+ }
245
+ const prevStartsWith$ = previousBlock.trimStart().startsWith("$");
246
+ const prevDollarCount = (previousBlock.match(/\$\$/g) || []).length;
247
+ const currDollarCount = (currentBlock.match(/\$\$/g) || []).length;
248
+ if (prevStartsWith$ && prevDollarCount % 2 === 1 && !currentBlock.trimStart().startsWith("$") && currDollarCount === 1) {
249
+ mergedBlocks[mergedBlocks.length - 1] = previousBlock + currentBlock;
250
+ continue;
251
+ }
252
+ }
253
+ mergedBlocks.push(currentBlock);
254
+ }
255
+ return mergedBlocks;
256
+ }
257
+ function splitMarkdownIntoBlocks({
258
+ messageId,
259
+ markdown,
260
+ status = "idle" /* IDLE */
261
+ }) {
262
+ const blocks = parseMarkdownIntoBlocks(markdown);
263
+ return blocks.map((content) => {
264
+ const block = {
265
+ id: generateBlockId(),
266
+ messageId,
267
+ type: "main_text" /* MAIN_TEXT */,
268
+ status,
269
+ content,
270
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
271
+ };
272
+ return block;
273
+ });
274
+ }
275
+
276
+ // src/components/Markdown/Block.tsx
277
+ import { memo } from "react";
278
+ import ReactMarkdown from "react-markdown";
279
+ import { jsx } from "react/jsx-runtime";
280
+ var Block = memo(
281
+ ({ content, components, remarkPlugins, rehypePlugins, className }) => {
282
+ return /* @__PURE__ */ jsx(
283
+ ReactMarkdown,
284
+ {
285
+ className,
286
+ components,
287
+ remarkPlugins,
288
+ rehypePlugins,
289
+ children: content
290
+ }
291
+ );
292
+ },
293
+ (prevProps, nextProps) => {
294
+ return prevProps.content === nextProps.content && prevProps.className === nextProps.className;
295
+ }
296
+ );
297
+ Block.displayName = "Block";
298
+
182
299
  // src/hooks/useShikiHighlight.ts
183
300
  import { useEffect as useEffect2, useState } from "react";
184
301
  import { createHighlighterCore } from "shiki/core";
@@ -270,7 +387,7 @@ function escapeHtml(unsafe) {
270
387
  }
271
388
 
272
389
  // src/components/Markdown/CodeBlock.tsx
273
- import { jsx } from "react/jsx-runtime";
390
+ import { jsx as jsx2 } from "react/jsx-runtime";
274
391
  function CodeBlock({
275
392
  code,
276
393
  language = "text",
@@ -279,9 +396,9 @@ function CodeBlock({
279
396
  }) {
280
397
  const { html, isLoading } = useShikiHighlight({ code, language, theme });
281
398
  if (isLoading) {
282
- return /* @__PURE__ */ jsx("pre", { className, children: /* @__PURE__ */ jsx("code", { "data-language": language, children: code }) });
399
+ return /* @__PURE__ */ jsx2("pre", { className, children: /* @__PURE__ */ jsx2("code", { "data-language": language, children: code }) });
283
400
  }
284
- return /* @__PURE__ */ jsx(
401
+ return /* @__PURE__ */ jsx2(
285
402
  "div",
286
403
  {
287
404
  dangerouslySetInnerHTML: { __html: html },
@@ -294,89 +411,286 @@ function CodeBlock({
294
411
  );
295
412
  }
296
413
 
297
- // src/components/Markdown/StreamingMarkdown.tsx
298
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
299
- function StreamingMarkdown({
300
- children,
301
- className,
302
- components: customComponents,
303
- status = "idle",
304
- onComplete,
305
- minDelay = 10
306
- }) {
307
- const markdown = typeof children === "string" ? children : String(children || "");
308
- const [displayedText, setDisplayedText] = useState2(status !== "streaming" ? markdown : "");
309
- const previousChildrenRef = useRef2(status !== "streaming" ? markdown : "");
310
- console.log("StreamingMarkdown children:", markdown);
311
- const { addChunk, reset } = useSmoothStream({
312
- onUpdate: setDisplayedText,
313
- streamDone: status !== "streaming",
314
- minDelay,
315
- initialText: status !== "streaming" ? markdown : "",
316
- onComplete
317
- });
318
- useEffect3(() => {
319
- const currentContent = markdown;
320
- const previousContent = previousChildrenRef.current;
321
- if (currentContent !== previousContent) {
322
- if (currentContent.startsWith(previousContent)) {
323
- const delta = currentContent.slice(previousContent.length);
324
- addChunk(delta);
325
- } else {
326
- reset(currentContent);
327
- }
328
- previousChildrenRef.current = currentContent;
414
+ // src/components/Markdown/MemoizedComponents.tsx
415
+ import { memo as memo2 } from "react";
416
+
417
+ // src/utils/markdown/sameNodePosition.ts
418
+ function sameNodePosition(prev, next) {
419
+ if (!(prev?.position || next?.position)) {
420
+ return true;
421
+ }
422
+ if (!(prev?.position && next?.position)) {
423
+ return false;
424
+ }
425
+ const prevStart = prev.position.start;
426
+ const nextStart = next.position.start;
427
+ const prevEnd = prev.position.end;
428
+ const nextEnd = next.position.end;
429
+ return prevStart?.line === nextStart?.line && prevStart?.column === nextStart?.column && prevEnd?.line === nextEnd?.line && prevEnd?.column === nextEnd?.column;
430
+ }
431
+ function sameClassAndNode(prev, next) {
432
+ return prev.className === next.className && sameNodePosition(prev.node, next.node);
433
+ }
434
+
435
+ // src/components/Markdown/MemoizedComponents.tsx
436
+ import { jsx as jsx3 } from "react/jsx-runtime";
437
+ var MemoH1 = memo2(
438
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h1", { className, ...props, children }),
439
+ (p, n) => sameClassAndNode(p, n)
440
+ );
441
+ MemoH1.displayName = "MemoH1";
442
+ var MemoH2 = memo2(
443
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h2", { className, ...props, children }),
444
+ (p, n) => sameClassAndNode(p, n)
445
+ );
446
+ MemoH2.displayName = "MemoH2";
447
+ var MemoH3 = memo2(
448
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h3", { className, ...props, children }),
449
+ (p, n) => sameClassAndNode(p, n)
450
+ );
451
+ MemoH3.displayName = "MemoH3";
452
+ var MemoH4 = memo2(
453
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h4", { className, ...props, children }),
454
+ (p, n) => sameClassAndNode(p, n)
455
+ );
456
+ MemoH4.displayName = "MemoH4";
457
+ var MemoH5 = memo2(
458
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h5", { className, ...props, children }),
459
+ (p, n) => sameClassAndNode(p, n)
460
+ );
461
+ MemoH5.displayName = "MemoH5";
462
+ var MemoH6 = memo2(
463
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("h6", { className, ...props, children }),
464
+ (p, n) => sameClassAndNode(p, n)
465
+ );
466
+ MemoH6.displayName = "MemoH6";
467
+ var MemoParagraph = memo2(
468
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("p", { className, ...props, children }),
469
+ (p, n) => sameClassAndNode(p, n)
470
+ );
471
+ MemoParagraph.displayName = "MemoParagraph";
472
+ var MemoLink = memo2(
473
+ ({ children, className, href, title, ...props }) => /* @__PURE__ */ jsx3(
474
+ "a",
475
+ {
476
+ className,
477
+ href,
478
+ title,
479
+ target: "_blank",
480
+ rel: "noopener noreferrer",
481
+ ...props,
482
+ children
329
483
  }
330
- }, [markdown, addChunk, reset]);
331
- const components = useMemo(() => {
332
- const baseComponents = {
333
- code: (props) => {
334
- const { node, inline, className: className2, children: children2, ...rest } = props;
335
- if (inline) {
336
- return /* @__PURE__ */ jsx2("code", { className: className2, ...rest, children: children2 });
337
- }
338
- const match = /language-(\w+)/.exec(className2 || "");
339
- const language = match ? match[1] : void 0;
340
- const code = String(children2).replace(/\n$/, "");
341
- return /* @__PURE__ */ jsx2(CodeBlock, { code, language, className: className2 });
342
- },
343
- pre: (props) => {
344
- const { children: children2, ...rest } = props;
345
- if (children2?.type === CodeBlock) {
346
- return children2;
347
- }
348
- return /* @__PURE__ */ jsx2("pre", { style: { overflow: "visible" }, ...rest, children: children2 });
349
- },
350
- p: (props) => {
351
- const hasCodeBlock = props?.node?.children?.some(
352
- (child) => child.tagName === "pre" || child.tagName === "code"
353
- );
354
- if (hasCodeBlock) {
355
- return /* @__PURE__ */ jsx2(Fragment, { children: props.children });
356
- }
357
- const hasImage = props?.node?.children?.some((child) => child.tagName === "img");
358
- if (hasImage) return /* @__PURE__ */ jsx2("div", { ...props });
359
- return /* @__PURE__ */ jsx2("p", { ...props });
360
- }
361
- };
362
- if (/<style\b[^>]*>/i.test(markdown)) {
363
- baseComponents.style = (props) => /* @__PURE__ */ jsx2("div", { ...props });
484
+ ),
485
+ (p, n) => sameClassAndNode(p, n) && p.href === n.href && p.title === n.title
486
+ );
487
+ MemoLink.displayName = "MemoLink";
488
+ var MemoBlockquote = memo2(
489
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("blockquote", { className, ...props, children }),
490
+ (p, n) => sameClassAndNode(p, n)
491
+ );
492
+ MemoBlockquote.displayName = "MemoBlockquote";
493
+ var MemoList = memo2(
494
+ ({ children, className, ordered, start, ...props }) => {
495
+ if (ordered) {
496
+ return /* @__PURE__ */ jsx3("ol", { className, start, ...props, children });
364
497
  }
365
- return { ...baseComponents, ...customComponents };
366
- }, [markdown, customComponents]);
367
- return /* @__PURE__ */ jsx2(
368
- ReactMarkdown,
498
+ return /* @__PURE__ */ jsx3("ul", { className, ...props, children });
499
+ },
500
+ (p, n) => sameClassAndNode(p, n) && p.ordered === n.ordered && p.start === n.start
501
+ );
502
+ MemoList.displayName = "MemoList";
503
+ var MemoListItem = memo2(
504
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("li", { className, ...props, children }),
505
+ (p, n) => sameClassAndNode(p, n)
506
+ );
507
+ MemoListItem.displayName = "MemoListItem";
508
+ var MemoTable = memo2(
509
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("table", { className, ...props, children }),
510
+ (p, n) => sameClassAndNode(p, n)
511
+ );
512
+ MemoTable.displayName = "MemoTable";
513
+ var MemoTableHead = memo2(
514
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("thead", { className, ...props, children }),
515
+ (p, n) => sameClassAndNode(p, n)
516
+ );
517
+ MemoTableHead.displayName = "MemoTableHead";
518
+ var MemoTableBody = memo2(
519
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("tbody", { className, ...props, children }),
520
+ (p, n) => sameClassAndNode(p, n)
521
+ );
522
+ MemoTableBody.displayName = "MemoTableBody";
523
+ var MemoTableRow = memo2(
524
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("tr", { className, ...props, children }),
525
+ (p, n) => sameClassAndNode(p, n)
526
+ );
527
+ MemoTableRow.displayName = "MemoTableRow";
528
+ var MemoTableCell = memo2(
529
+ ({ children, className, isHeader, align, ...props }) => {
530
+ const style = align ? { textAlign: align } : void 0;
531
+ if (isHeader) {
532
+ return /* @__PURE__ */ jsx3("th", { className, style, ...props, children });
533
+ }
534
+ return /* @__PURE__ */ jsx3("td", { className, style, ...props, children });
535
+ },
536
+ (p, n) => sameClassAndNode(p, n) && p.isHeader === n.isHeader && p.align === n.align
537
+ );
538
+ MemoTableCell.displayName = "MemoTableCell";
539
+ var MemoCode = memo2(
540
+ ({ children, className, inline, ...props }) => {
541
+ if (inline) {
542
+ return /* @__PURE__ */ jsx3("code", { className, ...props, children });
543
+ }
544
+ return /* @__PURE__ */ jsx3("code", { className, ...props, children });
545
+ },
546
+ (p, n) => sameClassAndNode(p, n) && p.inline === n.inline
547
+ );
548
+ MemoCode.displayName = "MemoCode";
549
+ var MemoPreformatted = memo2(
550
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("pre", { className, ...props, children }),
551
+ (p, n) => sameClassAndNode(p, n)
552
+ );
553
+ MemoPreformatted.displayName = "MemoPreformatted";
554
+ var MemoStrong = memo2(
555
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("strong", { className, ...props, children }),
556
+ (p, n) => sameClassAndNode(p, n)
557
+ );
558
+ MemoStrong.displayName = "MemoStrong";
559
+ var MemoEmphasis = memo2(
560
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("em", { className, ...props, children }),
561
+ (p, n) => sameClassAndNode(p, n)
562
+ );
563
+ MemoEmphasis.displayName = "MemoEmphasis";
564
+ var MemoHr = memo2(
565
+ ({ className, ...props }) => /* @__PURE__ */ jsx3("hr", { className, ...props }),
566
+ (p, n) => sameClassAndNode(p, n)
567
+ );
568
+ MemoHr.displayName = "MemoHr";
569
+ var MemoImage = memo2(
570
+ ({ className, href, title, ...props }) => /* @__PURE__ */ jsx3(
571
+ "img",
369
572
  {
370
573
  className,
371
- remarkPlugins: [remarkGfm],
372
- components,
373
- children: displayedText
574
+ src: href,
575
+ alt: title ?? "",
576
+ title,
577
+ ...props
374
578
  }
375
- );
376
- }
579
+ ),
580
+ (p, n) => sameClassAndNode(p, n) && p.href === n.href && p.title === n.title
581
+ );
582
+ MemoImage.displayName = "MemoImage";
583
+ var MemoDelete = memo2(
584
+ ({ children, className, ...props }) => /* @__PURE__ */ jsx3("del", { className, ...props, children }),
585
+ (p, n) => sameClassAndNode(p, n)
586
+ );
587
+ MemoDelete.displayName = "MemoDelete";
588
+
589
+ // src/components/Markdown/StreamingMarkdown.tsx
590
+ import { jsx as jsx4 } from "react/jsx-runtime";
591
+ var StreamingMarkdown = memo3(
592
+ function StreamingMarkdown2({
593
+ children,
594
+ className,
595
+ components: customComponents,
596
+ status = "idle",
597
+ onComplete,
598
+ minDelay = 10
599
+ }) {
600
+ const markdown = typeof children === "string" ? children : String(children || "");
601
+ const [displayedText, setDisplayedText] = useState2(status !== "streaming" ? markdown : "");
602
+ const previousChildrenRef = useRef2(status !== "streaming" ? markdown : "");
603
+ const generatedId = useId();
604
+ const { addChunk, reset } = useSmoothStream({
605
+ onUpdate: setDisplayedText,
606
+ streamDone: status !== "streaming",
607
+ minDelay,
608
+ initialText: status !== "streaming" ? markdown : "",
609
+ onComplete
610
+ });
611
+ useEffect3(() => {
612
+ const currentContent = markdown;
613
+ const previousContent = previousChildrenRef.current;
614
+ if (currentContent !== previousContent) {
615
+ if (currentContent.startsWith(previousContent)) {
616
+ const delta = currentContent.slice(previousContent.length);
617
+ addChunk(delta);
618
+ } else {
619
+ reset(currentContent);
620
+ }
621
+ previousChildrenRef.current = currentContent;
622
+ }
623
+ }, [markdown, addChunk, reset]);
624
+ const blocks = useMemo(
625
+ () => parseMarkdownIntoBlocks(displayedText),
626
+ [displayedText]
627
+ );
628
+ const components = useMemo(() => {
629
+ const baseComponents = {
630
+ h1: MemoH1,
631
+ h2: MemoH2,
632
+ h3: MemoH3,
633
+ h4: MemoH4,
634
+ h5: MemoH5,
635
+ h6: MemoH6,
636
+ p: MemoParagraph,
637
+ a: MemoLink,
638
+ blockquote: MemoBlockquote,
639
+ ul: (props) => /* @__PURE__ */ jsx4(MemoList, { ordered: false, ...props }),
640
+ ol: (props) => /* @__PURE__ */ jsx4(MemoList, { ordered: true, ...props }),
641
+ li: MemoListItem,
642
+ table: MemoTable,
643
+ thead: MemoTableHead,
644
+ tbody: MemoTableBody,
645
+ tr: MemoTableRow,
646
+ th: (props) => /* @__PURE__ */ jsx4(MemoTableCell, { isHeader: true, ...props }),
647
+ td: MemoTableCell,
648
+ strong: MemoStrong,
649
+ em: MemoEmphasis,
650
+ hr: MemoHr,
651
+ img: (props) => /* @__PURE__ */ jsx4(MemoImage, { href: props.src, title: props.alt, ...props }),
652
+ del: MemoDelete,
653
+ code: (props) => {
654
+ const { node, inline, className: className2, children: children2, ...rest } = props;
655
+ if (inline) {
656
+ return /* @__PURE__ */ jsx4(MemoCode, { inline: true, className: className2, node, ...rest, children: children2 });
657
+ }
658
+ const match = /language-(\w+)/.exec(className2 || "");
659
+ const language = match ? match[1] : void 0;
660
+ const code = String(children2).replace(/\n$/, "");
661
+ return /* @__PURE__ */ jsx4(CodeBlock, { code, language, className: className2 });
662
+ },
663
+ pre: (props) => {
664
+ const { children: children2, ...rest } = props;
665
+ if (children2?.type === CodeBlock) {
666
+ return children2;
667
+ }
668
+ return /* @__PURE__ */ jsx4(MemoPreformatted, { ...rest, children: children2 });
669
+ }
670
+ };
671
+ if (/<style\b[^>]*>/i.test(markdown)) {
672
+ baseComponents.style = (props) => /* @__PURE__ */ jsx4("div", { ...props });
673
+ }
674
+ return { ...baseComponents, ...customComponents };
675
+ }, [markdown, customComponents]);
676
+ return /* @__PURE__ */ jsx4("div", { className, children: blocks.map((block, index) => /* @__PURE__ */ jsx4(
677
+ Block,
678
+ {
679
+ content: block,
680
+ components,
681
+ remarkPlugins: [remarkGfm]
682
+ },
683
+ `${generatedId}-block-${index}`
684
+ )) });
685
+ },
686
+ (prevProps, nextProps) => {
687
+ return prevProps.children === nextProps.children && prevProps.status === nextProps.status && prevProps.className === nextProps.className;
688
+ }
689
+ );
690
+ StreamingMarkdown.displayName = "StreamingMarkdown";
377
691
 
378
692
  // src/components/Message/MessageBlockRenderer.tsx
379
- import { jsx as jsx3 } from "react/jsx-runtime";
693
+ import { jsx as jsx5 } from "react/jsx-runtime";
380
694
  function MessageBlockRenderer({ block, className }) {
381
695
  switch (block.type) {
382
696
  case "main_text" /* MAIN_TEXT */:
@@ -385,7 +699,7 @@ function MessageBlockRenderer({ block, className }) {
385
699
  case "error" /* ERROR */:
386
700
  case "unknown" /* UNKNOWN */: {
387
701
  const textBlock = block;
388
- return /* @__PURE__ */ jsx3(
702
+ return /* @__PURE__ */ jsx5(
389
703
  StreamingMarkdown,
390
704
  {
391
705
  className,
@@ -396,18 +710,18 @@ function MessageBlockRenderer({ block, className }) {
396
710
  }
397
711
  case "code" /* CODE */: {
398
712
  const codeBlock = block;
399
- return /* @__PURE__ */ jsx3("div", { className, children: /* @__PURE__ */ jsx3("pre", { children: /* @__PURE__ */ jsx3("code", { children: codeBlock.content }) }) });
713
+ return /* @__PURE__ */ jsx5("div", { className, children: /* @__PURE__ */ jsx5("pre", { children: /* @__PURE__ */ jsx5("code", { children: codeBlock.content }) }) });
400
714
  }
401
715
  case "image" /* IMAGE */:
402
716
  case "video" /* VIDEO */:
403
717
  case "file" /* FILE */: {
404
718
  const mediaBlock = block;
405
- return /* @__PURE__ */ jsx3("div", { className, children: /* @__PURE__ */ jsx3("a", { href: mediaBlock.url, target: "_blank", rel: "noopener noreferrer", children: mediaBlock.name ?? "Media File" }) });
719
+ return /* @__PURE__ */ jsx5("div", { className, children: /* @__PURE__ */ jsx5("a", { href: mediaBlock.url, target: "_blank", rel: "noopener noreferrer", children: mediaBlock.name ?? "Media File" }) });
406
720
  }
407
721
  case "tool" /* TOOL */:
408
722
  case "citation" /* CITATION */: {
409
723
  const toolBlock = block;
410
- return /* @__PURE__ */ jsx3("div", { className, children: /* @__PURE__ */ jsx3("pre", { children: JSON.stringify(toolBlock.payload ?? {}, null, 2) }) });
724
+ return /* @__PURE__ */ jsx5("div", { className, children: /* @__PURE__ */ jsx5("pre", { children: JSON.stringify(toolBlock.payload ?? {}, null, 2) }) });
411
725
  }
412
726
  default:
413
727
  return null;
@@ -415,17 +729,17 @@ function MessageBlockRenderer({ block, className }) {
415
729
  }
416
730
 
417
731
  // src/components/Message/MessageItem.tsx
418
- import { useId, useMemo as useMemo2 } from "react";
732
+ import { useId as useId2, useMemo as useMemo2 } from "react";
419
733
  import ReactMarkdown2 from "react-markdown";
420
734
  import remarkGfm2 from "remark-gfm";
421
735
 
422
736
  // src/utils/markdown/splitMarkdownIntoBlocks.ts
423
- var blockIdCounter = 0;
424
- function generateBlockId() {
425
- blockIdCounter += 1;
426
- return `block-${Date.now()}-${blockIdCounter}`;
737
+ var blockIdCounter2 = 0;
738
+ function generateBlockId2() {
739
+ blockIdCounter2 += 1;
740
+ return `block-${Date.now()}-${blockIdCounter2}`;
427
741
  }
428
- function splitMarkdownIntoBlocks({
742
+ function splitMarkdownIntoBlocks2({
429
743
  messageId,
430
744
  markdown,
431
745
  status = "idle" /* IDLE */
@@ -434,7 +748,7 @@ function splitMarkdownIntoBlocks({
434
748
  return [];
435
749
  }
436
750
  const block = {
437
- id: generateBlockId(),
751
+ id: generateBlockId2(),
438
752
  messageId,
439
753
  type: "main_text" /* MAIN_TEXT */,
440
754
  status,
@@ -445,7 +759,7 @@ function splitMarkdownIntoBlocks({
445
759
  }
446
760
 
447
761
  // src/components/Message/MessageItem.tsx
448
- import { jsx as jsx4 } from "react/jsx-runtime";
762
+ import { jsx as jsx6 } from "react/jsx-runtime";
449
763
  function MessageItem({
450
764
  children,
451
765
  role = "assistant",
@@ -455,9 +769,9 @@ function MessageItem({
455
769
  }) {
456
770
  const markdown = typeof children === "string" ? children : String(children ?? "");
457
771
  if (role === "user") {
458
- return /* @__PURE__ */ jsx4("div", { className, "data-role": "user", children: /* @__PURE__ */ jsx4(ReactMarkdown2, { remarkPlugins: [remarkGfm2], children: markdown }) });
772
+ return /* @__PURE__ */ jsx6("div", { className, "data-role": "user", children: /* @__PURE__ */ jsx6(ReactMarkdown2, { remarkPlugins: [remarkGfm2], children: markdown }) });
459
773
  }
460
- const generatedId = useId();
774
+ const generatedId = useId2();
461
775
  const messageIdRef = messageId ?? generatedId;
462
776
  const blocks = useMemo2(() => {
463
777
  const allBlocks = messageBlockStore.selectAll();
@@ -465,7 +779,7 @@ function MessageItem({
465
779
  if (oldBlockIds.length > 0) {
466
780
  messageBlockStore.remove(oldBlockIds);
467
781
  }
468
- const newBlocks = splitMarkdownIntoBlocks({
782
+ const newBlocks = splitMarkdownIntoBlocks2({
469
783
  messageId: messageIdRef,
470
784
  markdown,
471
785
  status: "idle" /* IDLE */
@@ -473,18 +787,236 @@ function MessageItem({
473
787
  messageBlockStore.upsert(newBlocks);
474
788
  return newBlocks;
475
789
  }, [markdown, messageIdRef]);
476
- return /* @__PURE__ */ jsx4("div", { className, "data-message-id": messageIdRef, "data-role": role, children: blocks.map((block) => /* @__PURE__ */ jsx4(MessageBlockRenderer, { block, className: blockClassName }, block.id)) });
790
+ return /* @__PURE__ */ jsx6("div", { className, "data-message-id": messageIdRef, "data-role": role, children: blocks.map((block) => /* @__PURE__ */ jsx6(MessageBlockRenderer, { block, className: blockClassName }, block.id)) });
791
+ }
792
+
793
+ // src/utils/shiki/ShikiHighlighterManager.ts
794
+ import { createHighlighterCore as createHighlighterCore2 } from "shiki/core";
795
+ import { createJavaScriptRegexEngine as createJavaScriptRegexEngine2 } from "shiki/engine/javascript";
796
+
797
+ // src/utils/shiki/languageRegistry.ts
798
+ var LANGUAGE_REGISTRY = {
799
+ javascript: () => import("shiki/langs/javascript.mjs"),
800
+ js: () => import("shiki/langs/javascript.mjs"),
801
+ typescript: () => import("shiki/langs/typescript.mjs"),
802
+ ts: () => import("shiki/langs/typescript.mjs"),
803
+ tsx: () => import("shiki/langs/tsx.mjs"),
804
+ jsx: () => import("shiki/langs/jsx.mjs"),
805
+ python: () => import("shiki/langs/python.mjs"),
806
+ py: () => import("shiki/langs/python.mjs"),
807
+ java: () => import("shiki/langs/java.mjs"),
808
+ json: () => import("shiki/langs/json.mjs"),
809
+ html: () => import("shiki/langs/html.mjs"),
810
+ css: () => import("shiki/langs/css.mjs"),
811
+ bash: () => import("shiki/langs/bash.mjs"),
812
+ sh: () => import("shiki/langs/bash.mjs"),
813
+ shell: () => import("shiki/langs/shell.mjs"),
814
+ sql: () => import("shiki/langs/sql.mjs"),
815
+ markdown: () => import("shiki/langs/markdown.mjs"),
816
+ md: () => import("shiki/langs/markdown.mjs"),
817
+ go: () => import("shiki/langs/go.mjs"),
818
+ rust: () => import("shiki/langs/rust.mjs"),
819
+ rs: () => import("shiki/langs/rust.mjs"),
820
+ c: () => import("shiki/langs/c.mjs"),
821
+ cpp: () => import("shiki/langs/cpp.mjs"),
822
+ "c++": () => import("shiki/langs/cpp.mjs"),
823
+ csharp: () => import("shiki/langs/csharp.mjs"),
824
+ "c#": () => import("shiki/langs/csharp.mjs"),
825
+ cs: () => import("shiki/langs/csharp.mjs"),
826
+ php: () => import("shiki/langs/php.mjs"),
827
+ ruby: () => import("shiki/langs/ruby.mjs"),
828
+ rb: () => import("shiki/langs/ruby.mjs"),
829
+ swift: () => import("shiki/langs/swift.mjs"),
830
+ kotlin: () => import("shiki/langs/kotlin.mjs"),
831
+ kt: () => import("shiki/langs/kotlin.mjs"),
832
+ yaml: () => import("shiki/langs/yaml.mjs"),
833
+ yml: () => import("shiki/langs/yaml.mjs"),
834
+ xml: () => import("shiki/langs/xml.mjs"),
835
+ dockerfile: () => import("shiki/langs/dockerfile.mjs"),
836
+ graphql: () => import("shiki/langs/graphql.mjs"),
837
+ gql: () => import("shiki/langs/graphql.mjs"),
838
+ terraform: () => import("shiki/langs/terraform.mjs"),
839
+ tf: () => import("shiki/langs/terraform.mjs"),
840
+ lua: () => import("shiki/langs/lua.mjs"),
841
+ r: () => import("shiki/langs/r.mjs"),
842
+ scala: () => import("shiki/langs/scala.mjs"),
843
+ elixir: () => import("shiki/langs/elixir.mjs"),
844
+ ex: () => import("shiki/langs/elixir.mjs"),
845
+ dart: () => import("shiki/langs/dart.mjs"),
846
+ vue: () => import("shiki/langs/vue.mjs"),
847
+ svelte: () => import("shiki/langs/svelte.mjs"),
848
+ astro: () => import("shiki/langs/astro.mjs")
849
+ };
850
+ var THEME_REGISTRY = {
851
+ "github-light": () => import("shiki/themes/github-light.mjs"),
852
+ "github-dark": () => import("shiki/themes/github-dark.mjs"),
853
+ "github-dark-dimmed": () => import("shiki/themes/github-dark-dimmed.mjs"),
854
+ "dracula": () => import("shiki/themes/dracula.mjs"),
855
+ "monokai": () => import("shiki/themes/monokai.mjs"),
856
+ "nord": () => import("shiki/themes/nord.mjs"),
857
+ "one-dark-pro": () => import("shiki/themes/one-dark-pro.mjs"),
858
+ "solarized-light": () => import("shiki/themes/solarized-light.mjs"),
859
+ "solarized-dark": () => import("shiki/themes/solarized-dark.mjs"),
860
+ "vitesse-light": () => import("shiki/themes/vitesse-light.mjs"),
861
+ "vitesse-dark": () => import("shiki/themes/vitesse-dark.mjs")
862
+ };
863
+ function getLanguageImport(lang) {
864
+ const normalizedLang = lang.toLowerCase();
865
+ const importFn = LANGUAGE_REGISTRY[normalizedLang];
866
+ if (!importFn) {
867
+ console.warn(`[Shiki] Language "${lang}" not found in registry, falling back to plain text`);
868
+ return import("shiki/langs/javascript.mjs");
869
+ }
870
+ return importFn();
871
+ }
872
+ function getThemeImport(theme) {
873
+ const normalizedTheme = theme.toLowerCase();
874
+ const importFn = THEME_REGISTRY[normalizedTheme];
875
+ if (!importFn) {
876
+ console.warn(`[Shiki] Theme "${theme}" not found in registry, falling back to github-light`);
877
+ return import("shiki/themes/github-light.mjs");
878
+ }
879
+ return importFn();
880
+ }
881
+ function isLanguageSupported(lang) {
882
+ return lang.toLowerCase() in LANGUAGE_REGISTRY;
883
+ }
884
+ function isThemeSupported(theme) {
885
+ return theme.toLowerCase() in THEME_REGISTRY;
886
+ }
887
+ function getSupportedLanguages() {
888
+ return Object.keys(LANGUAGE_REGISTRY);
477
889
  }
890
+ function getSupportedThemes() {
891
+ return Object.keys(THEME_REGISTRY);
892
+ }
893
+
894
+ // src/utils/shiki/ShikiHighlighterManager.ts
895
+ var ShikiHighlighterManager = class {
896
+ constructor() {
897
+ this.lightHighlighter = null;
898
+ this.darkHighlighter = null;
899
+ this.lightTheme = null;
900
+ this.darkTheme = null;
901
+ this.loadedLanguages = /* @__PURE__ */ new Set();
902
+ }
903
+ /**
904
+ * 高亮代码(返回 light 和 dark 两个主题的 HTML)
905
+ *
906
+ * @param code - 代码字符串
907
+ * @param language - 语言标识符
908
+ * @param themes - [light theme, dark theme] 元组
909
+ * @returns [lightHtml, darkHtml] - 两个主题的 HTML 元组
910
+ */
911
+ async highlightCode(code, language, themes) {
912
+ const [lightTheme, darkTheme] = themes;
913
+ const needsLightRecreation = !this.lightHighlighter || this.lightTheme !== lightTheme;
914
+ const needsDarkRecreation = !this.darkHighlighter || this.darkTheme !== darkTheme;
915
+ if (needsLightRecreation || needsDarkRecreation) {
916
+ this.loadedLanguages.clear();
917
+ }
918
+ const languageSupported = isLanguageSupported(language);
919
+ const needsLanguageLoad = !this.loadedLanguages.has(language) && languageSupported;
920
+ if (needsLightRecreation) {
921
+ this.lightHighlighter = await createHighlighterCore2({
922
+ themes: [getThemeImport(lightTheme)],
923
+ langs: languageSupported ? [getLanguageImport(language)] : [],
924
+ engine: createJavaScriptRegexEngine2({ forgiving: true })
925
+ });
926
+ this.lightTheme = lightTheme;
927
+ if (languageSupported) {
928
+ this.loadedLanguages.add(language);
929
+ }
930
+ } else if (needsLanguageLoad && this.lightHighlighter) {
931
+ await this.lightHighlighter.loadLanguage(getLanguageImport(language));
932
+ this.loadedLanguages.add(language);
933
+ }
934
+ if (needsDarkRecreation) {
935
+ this.darkHighlighter = await createHighlighterCore2({
936
+ themes: [getThemeImport(darkTheme)],
937
+ langs: languageSupported ? [getLanguageImport(language)] : [],
938
+ engine: createJavaScriptRegexEngine2({ forgiving: true })
939
+ });
940
+ this.darkTheme = darkTheme;
941
+ } else if (needsLanguageLoad && this.darkHighlighter) {
942
+ await this.darkHighlighter.loadLanguage(getLanguageImport(language));
943
+ }
944
+ const lang = languageSupported ? language : "text";
945
+ const light = this.lightHighlighter?.codeToHtml(code, {
946
+ lang,
947
+ theme: lightTheme
948
+ }) ?? "";
949
+ const dark = this.darkHighlighter?.codeToHtml(code, {
950
+ lang,
951
+ theme: darkTheme
952
+ }) ?? "";
953
+ return [light, dark];
954
+ }
955
+ /**
956
+ * 高亮代码(单一主题版本)
957
+ */
958
+ async highlightCodeSingle(code, language, theme) {
959
+ const [light] = await this.highlightCode(code, language, [theme, theme]);
960
+ return light;
961
+ }
962
+ /**
963
+ * 清除缓存(用于测试或内存清理)
964
+ */
965
+ clear() {
966
+ this.lightHighlighter = null;
967
+ this.darkHighlighter = null;
968
+ this.lightTheme = null;
969
+ this.darkTheme = null;
970
+ this.loadedLanguages.clear();
971
+ }
972
+ };
973
+ var highlighterManager = new ShikiHighlighterManager();
478
974
  export {
975
+ Block,
479
976
  CodeBlock,
977
+ MemoBlockquote,
978
+ MemoCode,
979
+ MemoDelete,
980
+ MemoEmphasis,
981
+ MemoH1,
982
+ MemoH2,
983
+ MemoH3,
984
+ MemoH4,
985
+ MemoH5,
986
+ MemoH6,
987
+ MemoHr,
988
+ MemoImage,
989
+ MemoLink,
990
+ MemoList,
991
+ MemoListItem,
992
+ MemoParagraph,
993
+ MemoPreformatted,
994
+ MemoStrong,
995
+ MemoTable,
996
+ MemoTableBody,
997
+ MemoTableCell,
998
+ MemoTableHead,
999
+ MemoTableRow,
480
1000
  MessageBlockRenderer,
481
1001
  MessageBlockStatus,
482
1002
  MessageBlockStore,
483
1003
  MessageBlockType,
484
1004
  MessageItem,
485
1005
  MessageStatus,
1006
+ ShikiHighlighterManager,
486
1007
  StreamingMarkdown,
1008
+ getLanguageImport,
1009
+ getSupportedLanguages,
1010
+ getSupportedThemes,
1011
+ getThemeImport,
1012
+ highlighterManager,
1013
+ isLanguageSupported,
1014
+ isThemeSupported,
487
1015
  messageBlockStore,
1016
+ parseMarkdownIntoBlocks,
1017
+ sameClassAndNode,
1018
+ sameNodePosition,
1019
+ splitMarkdownIntoBlocks,
488
1020
  useShikiHighlight,
489
1021
  useSmoothStream
490
1022
  };