From 7e89b2dc94505092eb42b40e4a43399191ab3193 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 13 Feb 2025 09:04:40 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20latex=20in=20thinki?= =?UTF-8?q?ng=20tag=20render=20(#6063)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix latex-in-thinking * fix latex-in-thinking * fix latex-in-thinking --- .../components/ChatItem/index.tsx | 6 +- .../MarkdownElements/Thinking/index.ts | 2 +- .../createRemarkCustomTagPlugin.ts} | 15 +- .../remarkPlugins/getNodeContent.test.ts | 396 ++++++++++++++++++ .../remarkPlugins/getNodeContent.ts | 55 +++ 5 files changed, 458 insertions(+), 16 deletions(-) rename src/features/Conversation/components/MarkdownElements/{Thinking/remarkPlugin.ts => remarkPlugins/createRemarkCustomTagPlugin.ts} (78%) create mode 100644 src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.test.ts create mode 100644 src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.ts diff --git a/src/features/Conversation/components/ChatItem/index.tsx b/src/features/Conversation/components/ChatItem/index.tsx index 0efe03af32b90..fac434a611b64 100644 --- a/src/features/Conversation/components/ChatItem/index.tsx +++ b/src/features/Conversation/components/ChatItem/index.tsx @@ -174,10 +174,10 @@ const Item = memo( () => ({ components, customRender: markdownCustomRender, - rehypePlugins, - remarkPlugins, + rehypePlugins: item?.role === 'user' ? undefined : rehypePlugins, + remarkPlugins: item?.role === 'user' ? undefined : remarkPlugins, }), - [components, markdownCustomRender], + [components, markdownCustomRender, item?.role], ); const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]); diff --git a/src/features/Conversation/components/MarkdownElements/Thinking/index.ts b/src/features/Conversation/components/MarkdownElements/Thinking/index.ts index d9a27175f6079..52580a493252f 100644 --- a/src/features/Conversation/components/MarkdownElements/Thinking/index.ts +++ b/src/features/Conversation/components/MarkdownElements/Thinking/index.ts @@ -1,6 +1,6 @@ +import { createRemarkCustomTagPlugin } from '../remarkPlugins/createRemarkCustomTagPlugin'; import { MarkdownElement } from '../type'; import Component from './Render'; -import { createRemarkCustomTagPlugin } from './remarkPlugin'; const ThinkingElement: MarkdownElement = { Component, diff --git a/src/features/Conversation/components/MarkdownElements/Thinking/remarkPlugin.ts b/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkCustomTagPlugin.ts similarity index 78% rename from src/features/Conversation/components/MarkdownElements/Thinking/remarkPlugin.ts rename to src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkCustomTagPlugin.ts index 054113c5c9842..85f8027f0c2db 100644 --- a/src/features/Conversation/components/MarkdownElements/Thinking/remarkPlugin.ts +++ b/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkCustomTagPlugin.ts @@ -1,6 +1,7 @@ -import { toMarkdown } from 'mdast-util-to-markdown'; import { SKIP, visit } from 'unist-util-visit'; +import { treeNodeToString } from './getNodeContent'; + export const createRemarkCustomTagPlugin = (tag: string) => () => { return (tree: any) => { visit(tree, 'html', (node, index, parent) => { @@ -31,17 +32,7 @@ export const createRemarkCustomTagPlugin = (tag: string) => () => { ); // 转换为 Markdown 字符串 - const content = contentNodes - .map((n: any) => { - // fix https://github.com/lobehub/lobe-chat/issues/5668 - if (n.type === 'paragraph') { - return n.children.map((child: any) => child.value).join(''); - } - - return toMarkdown(n); - }) - .join('\n\n') - .trim(); + const content = treeNodeToString(contentNodes); // 创建自定义节点 const customNode = { diff --git a/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.test.ts b/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.test.ts new file mode 100644 index 0000000000000..1084b91ca62e7 --- /dev/null +++ b/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.test.ts @@ -0,0 +1,396 @@ +import { toMarkdown } from 'mdast-util-to-markdown'; +import { Parent } from 'unist'; +import { expect } from 'vitest'; + +import { treeNodeToString } from '@/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent'; + +describe('treeNodeToString', () => { + it('with latex', () => { + const nodes = [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: '设正向数列 ', + position: { + start: { + line: 3, + column: 1, + offset: 9, + }, + end: { + line: 3, + column: 7, + offset: 15, + }, + }, + }, + { + type: 'inlineMath', + value: '{ a_n }', + data: { + hName: 'code', + hProperties: { + className: ['language-math', 'math-inline'], + }, + hChildren: [ + { + type: 'text', + value: '{ a_n }', + }, + ], + }, + position: { + start: { + line: 3, + column: 7, + offset: 15, + }, + end: { + line: 3, + column: 18, + offset: 26, + }, + }, + }, + { + type: 'text', + value: ' 的首项为 ', + position: { + start: { + line: 3, + column: 18, + offset: 26, + }, + end: { + line: 3, + column: 24, + offset: 32, + }, + }, + }, + { + type: 'inlineMath', + value: '4', + data: { + hName: 'code', + hProperties: { + className: ['language-math', 'math-inline'], + }, + hChildren: [ + { + type: 'text', + value: '4', + }, + ], + }, + position: { + start: { + line: 3, + column: 24, + offset: 32, + }, + end: { + line: 3, + column: 29, + offset: 37, + }, + }, + }, + { + type: 'text', + value: ' ,满足 ', + position: { + start: { + line: 3, + column: 29, + offset: 37, + }, + end: { + line: 3, + column: 34, + offset: 42, + }, + }, + }, + { + type: 'inlineMath', + value: 'a^2_n = a_{n+1} + 3na_n - 3', + data: { + hName: 'code', + hProperties: { + className: ['language-math', 'math-inline'], + }, + hChildren: [ + { + type: 'text', + value: 'a^2_n = a_{n+1} + 3na_n - 3', + }, + ], + }, + position: { + start: { + line: 3, + column: 34, + offset: 42, + }, + end: { + line: 3, + column: 65, + offset: 73, + }, + }, + }, + { + type: 'text', + value: '.', + position: { + start: { + line: 3, + column: 65, + offset: 73, + }, + end: { + line: 3, + column: 66, + offset: 74, + }, + }, + }, + ], + position: { + start: { + line: 3, + column: 1, + offset: 9, + }, + end: { + line: 3, + column: 66, + offset: 74, + }, + }, + }, + { + type: 'list', + ordered: true, + start: 1, + spread: false, + children: [ + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: '求 ', + position: { + start: { + line: 5, + column: 5, + offset: 80, + }, + end: { + line: 5, + column: 7, + offset: 82, + }, + }, + }, + { + type: 'inlineMath', + value: 'a_2', + data: { + hName: 'code', + hProperties: { + className: ['language-math', 'math-inline'], + }, + hChildren: [ + { + type: 'text', + value: 'a_2', + }, + ], + }, + position: { + start: { + line: 5, + column: 7, + offset: 82, + }, + end: { + line: 5, + column: 14, + offset: 89, + }, + }, + }, + { + type: 'text', + value: ' 和 ', + position: { + start: { + line: 5, + column: 14, + offset: 89, + }, + end: { + line: 5, + column: 17, + offset: 92, + }, + }, + }, + { + type: 'inlineMath', + value: 'a_3', + data: { + hName: 'code', + hProperties: { + className: ['language-math', 'math-inline'], + }, + hChildren: [ + { + type: 'text', + value: 'a_3', + }, + ], + }, + position: { + start: { + line: 5, + column: 17, + offset: 92, + }, + end: { + line: 5, + column: 24, + offset: 99, + }, + }, + }, + { + type: 'text', + value: ',根据前三项的规律猜想该数列的通项公式', + position: { + start: { + line: 5, + column: 24, + offset: 99, + }, + end: { + line: 5, + column: 43, + offset: 118, + }, + }, + }, + ], + position: { + start: { + line: 5, + column: 5, + offset: 80, + }, + end: { + line: 5, + column: 43, + offset: 118, + }, + }, + }, + ], + position: { + start: { + line: 5, + column: 2, + offset: 77, + }, + end: { + line: 5, + column: 43, + offset: 118, + }, + }, + }, + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: '用数学归纳法证明你的猜想。', + position: { + start: { + line: 6, + column: 5, + offset: 123, + }, + end: { + line: 6, + column: 18, + offset: 136, + }, + }, + }, + ], + position: { + start: { + line: 6, + column: 5, + offset: 123, + }, + end: { + line: 6, + column: 18, + offset: 136, + }, + }, + }, + ], + position: { + start: { + line: 6, + column: 2, + offset: 120, + }, + end: { + line: 6, + column: 18, + offset: 136, + }, + }, + }, + ], + position: { + start: { + line: 5, + column: 2, + offset: 77, + }, + end: { + line: 6, + column: 18, + offset: 136, + }, + }, + }, + ]; + + const result = treeNodeToString(nodes as Parent[]); + + expect(result).toEqual(`设正向数列 \${ a_n }$ 的首项为 $4$ ,满足 $a^2_n = a_{n+1} + 3na_n - 3$. + +1. 求 $a_2$ 和 $a_3$,根据前三项的规律猜想该数列的通项公式 +2. 用数学归纳法证明你的猜想。`); + }); +}); diff --git a/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.ts b/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.ts new file mode 100644 index 0000000000000..4ddf8942e1128 --- /dev/null +++ b/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.ts @@ -0,0 +1,55 @@ +import { toMarkdown } from 'mdast-util-to-markdown'; +import { Parent } from 'unist'; + +const processNode = (node: any): string => { + // 处理数学公式节点 + if (node.type === 'inlineMath') { + return `$${node.value}$`; + } + + // 处理带有子节点的容器 + if (node.children) { + const content = node.children.map((element: Parent) => processNode(element)).join(''); + + // 处理列表的特殊换行逻辑 + if (node.type === 'list') { + return `\n${content}\n`; + } + + // 处理列表项的前缀 + if (node.type === 'listItem') { + const prefix = node.checked !== null ? `[${node.checked ? 'x' : ' '}] ` : ''; + return `${prefix}${content}`; + } + + return content; + } + + // 处理文本节点 + if (node.value) { + // 保留原始空白字符处理逻辑 + return node.value.replaceAll(/^\s+|\s+$/g, ' '); + } + + // 兜底使用标准转换 + return toMarkdown(node); +}; + +export const treeNodeToString = (nodes: Parent[]) => { + return nodes + .map((node) => { + // 处理列表的缩进问题 + if (node.type === 'list') { + return node.children + .map((item, index) => { + const prefix = (node as any).ordered ? `${(node as any).start + index}. ` : '- '; + return `${prefix}${processNode(item)}`; + }) + .join('\n'); + } + + return processNode(node); + }) + .join('\n\n') + .trim(); +};