diff --git a/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx b/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx index 00b46b658..3d79df338 100644 --- a/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx +++ b/apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx @@ -1,5 +1,11 @@ import type { ErrorInfo, ReactNode } from 'react'; -import { Component, createContext, useContext } from 'react'; +import { + Children, + Component, + createContext, + isValidElement, + useContext, +} from 'react'; import ReactMarkdown, { type Components } from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -61,7 +67,9 @@ export class MarkdownErrorBoundary extends Component< const ListDepthContext = createContext(0); const ListKindContext = createContext<'unordered' | 'ordered' | null>(null); -const ListItemContext = createContext(false); +type ListItemParagraphPosition = 'first' | 'continuation'; + +const ListItemContext = createContext(null); function MarkdownUnorderedList({ children }: { children?: ReactNode }) { const depth = useContext(ListDepthContext); @@ -80,12 +88,21 @@ function MarkdownUnorderedList({ children }: { children?: ReactNode }) { ); } -function MarkdownOrderedList({ children }: { children?: ReactNode }) { +function MarkdownOrderedList({ + children, + start, +}: { + children?: ReactNode; + start?: number; +}) { const depth = useContext(ListDepthContext); return ( -
    +
      {children}
    @@ -94,10 +111,33 @@ function MarkdownOrderedList({ children }: { children?: ReactNode }) { } function MarkdownParagraph({ children }: { children?: ReactNode }) { - const inListItem = useContext(ListItemContext); + const paragraphPosition = useContext(ListItemContext); return (

    + {children} +

    + ); +} + +function StreamingMarkdownParagraph({ children }: { children?: ReactNode }) { + const paragraphPosition = useContext(ListItemContext); + return ( +

    {children}

    @@ -106,13 +146,31 @@ function MarkdownParagraph({ children }: { children?: ReactNode }) { function MarkdownListItem({ children }: { children?: ReactNode }) { const listKind = useContext(ListKindContext); + let paragraphIndex = 0; + const childrenWithParagraphContext = Children.map( + children, + (child, index) => { + if ( + isValidElement(child) && + (child.type === MarkdownParagraph || + child.type === StreamingMarkdownParagraph) + ) { + const position: ListItemParagraphPosition = + paragraphIndex++ === 0 ? 'first' : 'continuation'; + return ( + + {child} + + ); + } + return child; + }, + ); return ( - -
  1. - {listKind === 'unordered' ? '- ' : null} - {children} -
  2. -
    +
  3. + {listKind === 'unordered' ? '- ' : null} + {childrenWithParagraphContext} +
  4. ); } @@ -195,9 +253,7 @@ const markdownComponents: Components = { const streamingMarkdownComponents: Components = { ...markdownComponents, - p: ({ children }) => ( -

    {children}

    - ), + p: StreamingMarkdownParagraph, }; export function ChatMarkdownMessage({ diff --git a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx index ee328f5bc..6df7c4333 100644 --- a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx +++ b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx @@ -79,6 +79,27 @@ describe('ChatMarkdownMessage', () => { expect(items[0]?.textContent).not.toContain('-'); }); + it('保留 Markdown 有序列表的起始编号', () => { + const { container } = render( + , + ); + + expect(container.querySelector('ol')?.getAttribute('start')).toBe('3'); + expect(container.querySelectorAll('ol > li')).toHaveLength(2); + }); + + it('为列表项后续段落保留段落间距', () => { + const { container } = render( + , + ); + + const paragraphs = container.querySelectorAll('ul > li p'); + expect(paragraphs).toHaveLength(2); + expect(paragraphs[0]?.className).toContain('inline'); + expect(paragraphs[1]?.className).toContain('mt-2'); + expect(paragraphs[1]?.className).not.toContain('inline'); + }); + it('不会把 react-markdown 的 node 元数据泄漏到代码节点', () => { const { container } = render(