From ac29897d72a44145e2d8cb4eb1774d969e815298 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 18 Mar 2026 21:58:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(perf):=20user=20m?= =?UTF-8?q?essage=20renderer=20(#13108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(perf): user message renderer --- package.json | 4 +- .../InputEditor/ActionTag/ActionTag.tsx | 53 +-------------- .../InputEditor/ActionTag/ActionTagNode.ts | 13 +++- .../InputEditor/ActionTag/ActionTagView.tsx | 65 +++++++++++++++++++ .../InputEditor/ReferTopic/ReferTopic.tsx | 39 +---------- .../InputEditor/ReferTopic/ReferTopicNode.ts | 14 +++- .../InputEditor/ReferTopic/ReferTopicView.tsx | 48 ++++++++++++++ .../User/components/RichTextMessage.tsx | 43 +++--------- 8 files changed, 156 insertions(+), 123 deletions(-) create mode 100644 src/features/ChatInput/InputEditor/ActionTag/ActionTagView.tsx create mode 100644 src/features/ChatInput/InputEditor/ReferTopic/ReferTopicView.tsx diff --git a/package.json b/package.json index db1b72135b..cfc2c60757 100644 --- a/package.json +++ b/package.json @@ -252,7 +252,7 @@ "@lobehub/chat-plugin-sdk": "^1.32.4", "@lobehub/chat-plugins-gateway": "^1.9.0", "@lobehub/desktop-ipc-typings": "workspace:*", - "@lobehub/editor": "^4.0.0", + "@lobehub/editor": "^4.2.0", "@lobehub/icons": "^5.0.0", "@lobehub/market-sdk": "^0.31.3", "@lobehub/tts": "^5.1.2", @@ -530,4 +530,4 @@ "stylelint-config-clean-order": "7.0.0" } } -} \ No newline at end of file +} diff --git a/src/features/ChatInput/InputEditor/ActionTag/ActionTag.tsx b/src/features/ChatInput/InputEditor/ActionTag/ActionTag.tsx index 88d2450256..1fcbf5e2f5 100644 --- a/src/features/ChatInput/InputEditor/ActionTag/ActionTag.tsx +++ b/src/features/ChatInput/InputEditor/ActionTag/ActionTag.tsx @@ -1,12 +1,8 @@ -import { Tag, Tooltip } from '@lobehub/ui'; -import { cx } from 'antd-style'; import { CLICK_COMMAND, COMMAND_PRIORITY_LOW, type LexicalEditor } from 'lexical'; import { memo, useCallback, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import type { ActionTagNode } from './ActionTagNode'; -import { styles } from './style'; -import type { ActionTagCategory } from './types'; +import { ActionTagView } from './ActionTagView'; interface ActionTagProps { editor: LexicalEditor; @@ -14,39 +10,8 @@ interface ActionTagProps { node: ActionTagNode; } -const CATEGORY_COLOR: Record = { - command: 'purple', - skill: 'blue', - tool: 'gold', -}; - -const CATEGORY_I18N_KEY: Record = { - command: 'actionTag.category.command', - skill: 'actionTag.category.skill', - tool: 'actionTag.category.tool', -}; - -const CATEGORY_TOOLTIP_I18N_KEY: Record = { - command: 'actionTag.tooltip.command', - skill: 'actionTag.tooltip.skill', - tool: 'actionTag.tooltip.tool', -}; - -const CATEGORY_STYLE_KEY: Record = { - command: 'commandTag', - skill: 'skillTag', - tool: 'toolTag', -}; - const ActionTag = memo(({ node, editor, label }) => { const spanRef = useRef(null); - const { t } = useTranslation('editor'); - - const category = node.actionCategory; - const categoryLabel = t(CATEGORY_I18N_KEY[category] as any); - const categoryDescription = t(CATEGORY_TOOLTIP_I18N_KEY[category] as any); - const color = CATEGORY_COLOR[category]; - const styleKey = CATEGORY_STYLE_KEY[category]; const onClick = useCallback((payload: MouseEvent) => { if (payload.target === spanRef.current || spanRef.current?.contains(payload.target as Node)) { @@ -60,20 +25,8 @@ const ActionTag = memo(({ node, editor, label }) => { }, [editor, onClick]); return ( - - -
{label}
-
{categoryLabel}
-
{categoryDescription}
- - } - > - - {label} - -
+ + ); }); diff --git a/src/features/ChatInput/InputEditor/ActionTag/ActionTagNode.ts b/src/features/ChatInput/InputEditor/ActionTag/ActionTagNode.ts index 4df94b98b0..e95518581b 100644 --- a/src/features/ChatInput/InputEditor/ActionTag/ActionTagNode.ts +++ b/src/features/ChatInput/InputEditor/ActionTag/ActionTagNode.ts @@ -1,5 +1,6 @@ import { addClassNamesToElement } from '@lexical/utils'; import { getKernelFromEditor } from '@lobehub/editor'; +import type { HeadlessRenderableNode, HeadlessRenderContext } from '@lobehub/editor/renderer'; import { $applyNodeReplacement, DecoratorNode, @@ -11,7 +12,9 @@ import { type SerializedLexicalNode, type Spread, } from 'lexical'; +import { createElement } from 'react'; +import { ActionTagView } from './ActionTagView'; import type { ActionTagCategory, ActionTagType } from './types'; export type SerializedActionTagNode = Spread< @@ -23,7 +26,7 @@ export type SerializedActionTagNode = Spread< SerializedLexicalNode >; -export class ActionTagNode extends DecoratorNode { +export class ActionTagNode extends DecoratorNode implements HeadlessRenderableNode { __actionType: ActionTagType; __actionCategory: ActionTagCategory; __actionLabel: string; @@ -121,6 +124,14 @@ export class ActionTagNode extends DecoratorNode { render: decorator.render(this, editor), }; } + + renderHeadless({ key }: HeadlessRenderContext) { + return createElement(ActionTagView, { + category: this.__actionCategory as ActionTagCategory, + key, + label: this.__actionLabel, + }); + } } export function $createActionTagNode( diff --git a/src/features/ChatInput/InputEditor/ActionTag/ActionTagView.tsx b/src/features/ChatInput/InputEditor/ActionTag/ActionTagView.tsx new file mode 100644 index 0000000000..6ab505eb87 --- /dev/null +++ b/src/features/ChatInput/InputEditor/ActionTag/ActionTagView.tsx @@ -0,0 +1,65 @@ +import { Tag, Tooltip } from '@lobehub/ui'; +import { cx } from 'antd-style'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { styles } from './style'; +import type { ActionTagCategory } from './types'; + +export interface ActionTagViewProps { + category: ActionTagCategory; + label: string; +} + +const CATEGORY_COLOR: Record = { + command: 'purple', + skill: 'blue', + tool: 'gold', +}; + +const CATEGORY_I18N_KEY: Record = { + command: 'actionTag.category.command', + skill: 'actionTag.category.skill', + tool: 'actionTag.category.tool', +}; + +const CATEGORY_TOOLTIP_I18N_KEY: Record = { + command: 'actionTag.tooltip.command', + skill: 'actionTag.tooltip.skill', + tool: 'actionTag.tooltip.tool', +}; + +const CATEGORY_STYLE_KEY: Record = { + command: 'commandTag', + skill: 'skillTag', + tool: 'toolTag', +}; + +export const ActionTagView = memo(({ category, label }) => { + const { t } = useTranslation('editor'); + + const categoryLabel = t(CATEGORY_I18N_KEY[category] as any); + const categoryDescription = t(CATEGORY_TOOLTIP_I18N_KEY[category] as any); + const color = CATEGORY_COLOR[category]; + const styleKey = CATEGORY_STYLE_KEY[category]; + + return ( + + +
{label}
+
{categoryLabel}
+
{categoryDescription}
+ + } + > + + {label} + +
+
+ ); +}); + +ActionTagView.displayName = 'ActionTagView'; diff --git a/src/features/ChatInput/InputEditor/ReferTopic/ReferTopic.tsx b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopic.tsx index d514b85605..358225f1ae 100644 --- a/src/features/ChatInput/InputEditor/ReferTopic/ReferTopic.tsx +++ b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopic.tsx @@ -1,47 +1,14 @@ -import { Tag } from '@lobehub/ui'; -import { MessageSquarePlusIcon } from 'lucide-react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; -import { useChatStore } from '@/store/chat'; - -import { TAG_MARGIN_INLINE_END } from '../constants'; import type { ReferTopicNode } from './ReferTopicNode'; +import { ReferTopicView } from './ReferTopicView'; interface ReferTopicProps { node: ReferTopicNode; } const ReferTopic = memo(({ node }) => { - const { t } = useTranslation('topic'); - const title = node.topicTitle || t('defaultTitle'); - const switchTopic = useChatStore((s) => s.switchTopic); - - const handleClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - if (node.topicId) { - switchTopic(node.topicId); - } - }, - [node.topicId, switchTopic], - ); - - return ( - - } variant="outlined"> - {title} - - - ); + return ; }); ReferTopic.displayName = 'ReferTopic'; diff --git a/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicNode.ts b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicNode.ts index ec8cda629c..0a891d3c53 100644 --- a/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicNode.ts +++ b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicNode.ts @@ -1,5 +1,6 @@ import { addClassNamesToElement } from '@lexical/utils'; import { getKernelFromEditor } from '@lobehub/editor'; +import type { HeadlessRenderableNode, HeadlessRenderContext } from '@lobehub/editor/renderer'; import { $applyNodeReplacement, DecoratorNode, @@ -11,6 +12,9 @@ import { type SerializedLexicalNode, type Spread, } from 'lexical'; +import { createElement } from 'react'; + +import { ReferTopicView } from './ReferTopicView'; export type SerializedReferTopicNode = Spread< { @@ -20,7 +24,7 @@ export type SerializedReferTopicNode = Spread< SerializedLexicalNode >; -export class ReferTopicNode extends DecoratorNode { +export class ReferTopicNode extends DecoratorNode implements HeadlessRenderableNode { __topicId: string; __topicTitle: string; @@ -99,6 +103,14 @@ export class ReferTopicNode extends DecoratorNode { render: decorator.render(this, editor), }; } + + renderHeadless({ key }: HeadlessRenderContext) { + return createElement(ReferTopicView, { + fallbackTitle: this.__topicTitle, + key, + topicId: this.__topicId, + }); + } } export function $createReferTopicNode(topicId: string, topicTitle: string): ReferTopicNode { diff --git a/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicView.tsx b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicView.tsx new file mode 100644 index 0000000000..f0a98c6bd9 --- /dev/null +++ b/src/features/ChatInput/InputEditor/ReferTopic/ReferTopicView.tsx @@ -0,0 +1,48 @@ +import { Tag } from '@lobehub/ui'; +import { MessageSquarePlusIcon } from 'lucide-react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useChatStore } from '@/store/chat'; +import { topicSelectors } from '@/store/chat/slices/topic/selectors'; + +import { TAG_MARGIN_INLINE_END } from '../constants'; + +export interface ReferTopicViewProps { + fallbackTitle?: string; + topicId: string; +} + +export const ReferTopicView = memo(({ topicId, fallbackTitle }) => { + const { t } = useTranslation('topic'); + const title = useChatStore(topicSelectors.getTopicById(topicId))?.title || fallbackTitle; + const switchTopic = useChatStore((s) => s.switchTopic); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (topicId) { + switchTopic(topicId); + } + }, + [switchTopic, topicId], + ); + + return ( + + } variant="outlined"> + {title || t('defaultTitle')} + + + ); +}); + +ReferTopicView.displayName = 'ReferTopicView'; diff --git a/src/features/Conversation/Messages/User/components/RichTextMessage.tsx b/src/features/Conversation/Messages/User/components/RichTextMessage.tsx index a96d7adf86..eed561031b 100644 --- a/src/features/Conversation/Messages/User/components/RichTextMessage.tsx +++ b/src/features/Conversation/Messages/User/components/RichTextMessage.tsx @@ -1,49 +1,26 @@ -import { ReactMentionPlugin, ReactTablePlugin } from '@lobehub/editor'; -import { Editor, useEditor } from '@lobehub/editor/react'; -import { memo, useEffect, useMemo } from 'react'; +import { LexicalRenderer } from '@lobehub/editor/renderer'; +import type { SerializedEditorState } from 'lexical'; +import { memo, useMemo } from 'react'; -import { createChatInputRichPlugins } from '@/features/ChatInput/InputEditor/plugins'; +import { ActionTagNode } from '@/features/ChatInput/InputEditor/ActionTag/ActionTagNode'; +import { ReferTopicNode } from '@/features/ChatInput/InputEditor/ReferTopic/ReferTopicNode'; interface RichTextMessageProps { editorState: unknown; } -const EDITOR_PLUGINS = [...createChatInputRichPlugins(), ReactTablePlugin, ReactMentionPlugin]; +const EXTRA_NODES = [ActionTagNode, ReferTopicNode]; const RichTextMessage = memo(({ editorState }) => { - const editor = useEditor(); - - const content = useMemo(() => { + const value = useMemo(() => { if (!editorState || typeof editorState !== 'object') return null; if (Object.keys(editorState as Record).length === 0) return null; - - try { - return JSON.stringify(editorState); - } catch { - return null; - } + return editorState as SerializedEditorState; }, [editorState]); - useEffect(() => { - if (editor && content) { - editor.setDocument('json', content); - } - }, [editor, content]); + if (!value) return null; - if (!content) return null; - - return ( - - ); + return ; }); RichTextMessage.displayName = 'RichTextMessage';