🐛 fix: fix editor modal when Markdown rendering off (#11251)

fix: fix editor modal
This commit is contained in:
CanisMinor
2026-01-05 18:04:50 +08:00
committed by GitHub
parent 039b0a1064
commit eb86d3b11e
13 changed files with 183 additions and 443 deletions

View File

@@ -1,6 +1,6 @@
import { HotkeyEnum, getHotkeyById } from '@lobehub/editor';
import { FloatActions } from '@lobehub/editor/react';
import { type ChatInputActionsProps, CodeLanguageSelect } from '@lobehub/editor/react';
import { type ChatInputActionsProps } from '@lobehub/editor/react';
import {
BoldIcon,
CodeXmlIcon,
@@ -116,16 +116,6 @@ const TypoBar = memo(() => {
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
].filter(Boolean) as ChatInputActionsProps['items'];
}, [editorState, t]);

View File

@@ -1,6 +1,6 @@
import { HotkeyEnum, getHotkeyById } from '@lobehub/editor';
import { FloatActions } from '@lobehub/editor/react';
import { type ChatInputActionsProps, CodeLanguageSelect } from '@lobehub/editor/react';
import { type ChatInputActionsProps } from '@lobehub/editor/react';
import {
BoldIcon,
CodeXmlIcon,
@@ -116,16 +116,6 @@ const TypoBar = memo(() => {
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
].filter(Boolean) as ChatInputActionsProps['items'];
}, [editorState, t]);

View File

@@ -1,66 +1,16 @@
import {
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkHighlightPlugin,
ReactListPlugin,
ReactMathPlugin,
ReactTablePlugin,
} from '@lobehub/editor';
import { Editor, useEditor } from '@lobehub/editor/react';
import { Flexbox, Modal } from '@lobehub/ui';
import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
import { EditorModal } from '@/features/EditorModal';
import { useUserMemoryStore } from '@/store/userMemory';
import { LayersEnum } from '@/types/userMemory';
import TypoBar from './Typobar';
const EditableModal = memo(() => {
const { t } = useTranslation('common');
const editor = useEditor();
const [confirmLoading, setConfirmLoading] = useState(false);
const editingMemoryId = useUserMemoryStore((s) => s.editingMemoryId);
const editingMemoryContent = useUserMemoryStore((s) => s.editingMemoryContent);
const editingMemoryLayer = useUserMemoryStore((s) => s.editingMemoryLayer);
const clearEditingMemory = useUserMemoryStore((s) => s.clearEditingMemory);
const updateMemory = useUserMemoryStore((s) => s.updateMemory);
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
const richRenderProps = useMemo(
() =>
!enableRichRender
? {
enablePasteMarkdown: false,
markdownOption: {
bold: false,
code: false,
header: false,
italic: false,
quote: false,
strikethrough: false,
underline: false,
underlineStrikethrough: false,
},
}
: {
plugins: [
ReactListPlugin,
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkHighlightPlugin,
ReactTablePlugin,
ReactMathPlugin,
],
},
[enableRichRender],
);
const layerMap = {
context: LayersEnum.Context,
experience: LayersEnum.Experience,
@@ -69,58 +19,15 @@ const EditableModal = memo(() => {
};
return (
<Modal
cancelText={t('cancel')}
closable={false}
confirmLoading={confirmLoading}
destroyOnHidden
okText={t('ok')}
<EditorModal
onCancel={clearEditingMemory}
onOk={async () => {
if (!editor || !editingMemoryId || !editingMemoryLayer) return;
setConfirmLoading(true);
const newValue = editor.getDocument('markdown') as unknown as string;
await updateMemory(editingMemoryId, newValue, layerMap[editingMemoryLayer]);
setConfirmLoading(false);
onConfirm={async (value) => {
if (!editingMemoryId || !editingMemoryLayer) return;
await updateMemory(editingMemoryId, value, layerMap[editingMemoryLayer]);
}}
open={!!editingMemoryId}
styles={{
body: {
overflow: 'hidden',
padding: 0,
},
}}
title={null}
width={'min(90vw, 960px)'}
>
<TypoBar editor={editor} />
<Flexbox
onClick={() => {
editor.focus();
}}
paddingBlock={16}
paddingInline={48}
style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
>
<Editor
autoFocus
content={''}
editor={editor}
onInit={(editor) => {
if (!editor) return;
try {
editor?.setDocument('markdown', editingMemoryContent);
} catch {}
}}
style={{
paddingBottom: 120,
}}
type={'text'}
variant={'chat'}
{...richRenderProps}
/>
</Flexbox>
</Modal>
value={editingMemoryContent}
/>
);
});

View File

@@ -99,6 +99,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
underline: false,
underlineStrikethrough: false,
},
plugins: [ReactCodemirrorPlugin],
}
: {
plugins: [

View File

@@ -4,7 +4,6 @@ import {
ChatInputActionBar,
ChatInputActions,
type ChatInputActionsProps,
CodeLanguageSelect,
} from '@lobehub/editor/react';
import { cssVar } from 'antd-style';
import {
@@ -122,16 +121,6 @@ const TypoBar = memo(() => {
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
].filter(Boolean) as ChatInputActionsProps['items'],
[editorState],
);

View File

@@ -1,119 +0,0 @@
import {
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkHighlightPlugin,
ReactListPlugin,
ReactMathPlugin,
ReactTablePlugin,
} from '@lobehub/editor';
import { Editor, useEditor } from '@lobehub/editor/react';
import { Flexbox, Modal } from '@lobehub/ui';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
import TypoBar from './Typobar';
interface EditableMessageProps {
editing?: boolean;
onChange?: (value: string) => void;
onEditingChange?: (editing: boolean) => void;
value?: string;
}
const EditableMessage = memo<EditableMessageProps>(
({ editing, onEditingChange, onChange, value }) => {
const { t } = useTranslation('common');
const editor = useEditor();
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
const richRenderProps = useMemo(
() =>
!enableRichRender
? {
enablePasteMarkdown: false,
markdownOption: {
bold: false,
code: false,
header: false,
italic: false,
quote: false,
strikethrough: false,
underline: false,
underlineStrikethrough: false,
},
}
: {
plugins: [
ReactListPlugin,
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkHighlightPlugin,
ReactTablePlugin,
ReactMathPlugin,
],
},
[enableRichRender],
);
return (
<Modal
cancelText={t('cancel')}
closable={false}
destroyOnHidden
okText={t('ok')}
onCancel={() => onEditingChange?.(false)}
onOk={() => {
if (!editor) return;
const newValue = editor.getDocument('markdown') as unknown as string;
onChange?.(newValue);
onEditingChange?.(false);
}}
open={editing}
styles={{
body: {
overflow: 'hidden',
padding: 0,
},
}}
title={null}
width={'min(90vw, 960px)'}
>
<TypoBar editor={editor} />
<Flexbox
onClick={() => {
editor.focus();
}}
paddingBlock={16}
paddingInline={48}
style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
>
<Editor
autoFocus
content={''}
editor={editor}
onInit={(editor) => {
if (!editor) return;
try {
editor?.setDocument('markdown', value);
} catch {}
}}
style={{
paddingBottom: 120,
}}
type={'text'}
variant={'chat'}
{...richRenderProps}
/>
</Flexbox>
</Modal>
);
},
);
export default EditableMessage;

View File

@@ -1,150 +0,0 @@
import { HotkeyEnum, type IEditor, getHotkeyById } from '@lobehub/editor';
import { useEditorState } from '@lobehub/editor/react';
import {
ChatInputActionBar,
ChatInputActions,
type ChatInputActionsProps,
CodeLanguageSelect,
} from '@lobehub/editor/react';
import { cssVar } from 'antd-style';
import {
BoldIcon,
CodeXmlIcon,
ItalicIcon,
ListIcon,
ListOrderedIcon,
ListTodoIcon,
MessageSquareQuote,
SigmaIcon,
SquareDashedBottomCodeIcon,
StrikethroughIcon,
UnderlineIcon,
} from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
const { t } = useTranslation('editor');
const editorState = useEditorState(editor);
const items: ChatInputActionsProps['items'] = useMemo(
() =>
[
{
active: editorState.isBold,
icon: BoldIcon,
key: 'bold',
label: t('typobar.bold'),
onClick: editorState.bold,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
},
{
active: editorState.isItalic,
icon: ItalicIcon,
key: 'italic',
label: t('typobar.italic'),
onClick: editorState.italic,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
},
{
active: editorState.isUnderline,
icon: UnderlineIcon,
key: 'underline',
label: t('typobar.underline'),
onClick: editorState.underline,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
},
{
active: editorState.isStrikethrough,
icon: StrikethroughIcon,
key: 'strikethrough',
label: t('typobar.strikethrough'),
onClick: editorState.strikethrough,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
},
{
type: 'divider',
},
{
icon: ListIcon,
key: 'bulletList',
label: t('typobar.bulletList'),
onClick: editorState.bulletList,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
},
{
icon: ListOrderedIcon,
key: 'numberlist',
label: t('typobar.numberList'),
onClick: editorState.numberList,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
},
{
icon: ListTodoIcon,
key: 'tasklist',
label: t('typobar.taskList'),
onClick: editorState.checkList,
},
{
type: 'divider',
},
{
active: editorState.isBlockquote,
icon: MessageSquareQuote,
key: 'blockquote',
label: t('typobar.blockquote'),
onClick: editorState.blockquote,
},
{
type: 'divider',
},
{
icon: SigmaIcon,
key: 'math',
label: t('typobar.tex'),
onClick: editorState.insertMath,
},
{
active: editorState.isCode,
icon: CodeXmlIcon,
key: 'code',
label: t('typobar.code'),
onClick: editorState.code,
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
},
{
icon: SquareDashedBottomCodeIcon,
key: 'codeblock',
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
].filter(Boolean) as ChatInputActionsProps['items'],
[editorState],
);
return (
<ChatInputActionBar
left={<ChatInputActions items={items} />}
style={{
background: cssVar.colorFillQuaternary,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
}}
/>
);
});
TypoBar.displayName = 'TypoBar';
export default TypoBar;

View File

@@ -7,7 +7,10 @@ import { useConversationStore } from '@/features/Conversation/store';
import { type ChatItemProps } from '../../type';
const EditableModal = dynamic(() => import('./EditableModal'), { ssr: false });
const EditorModal = dynamic(
() => import('@/features/EditorModal').then((mode) => mode.EditorModal),
{ ssr: false },
);
export const MSG_CONTENT_CLASSNAME = 'msg_content_flag';
@@ -60,13 +63,6 @@ const MessageContent = memo<MessageContentProps>(
s.updateMessageContent,
]);
const onChange = useCallback(
(value: string) => {
updateMessageContent(id, value);
},
[id, updateMessageContent],
);
const onEditingChange = useCallback(
(edit: boolean) => toggleMessageEditing(id, edit),
[id, toggleMessageEditing],
@@ -90,10 +86,13 @@ const MessageContent = memo<MessageContentProps>(
</Flexbox>
<Suspense fallback={null}>
{editing && (
<EditableModal
editing={editing}
onChange={onChange}
onEditingChange={onEditingChange}
<EditorModal
onCancel={() => onEditingChange(false)}
onConfirm={async (value) => {
await updateMessageContent(id, value);
onEditingChange(false);
}}
open={editing}
value={message ? String(message) : ''}
/>
)}

View File

@@ -0,0 +1,81 @@
import {
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkPlugin,
ReactListPlugin,
ReactMathPlugin,
ReactTablePlugin,
} from '@lobehub/editor';
import { Editor, useEditor } from '@lobehub/editor/react';
import { Flexbox } from '@lobehub/ui';
import { FC } from 'react';
import TypoBar from './Typobar';
interface EditorCanvasProps {
onChange?: (value: string) => void;
value?: string;
}
const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
const editor = useEditor();
return (
<>
<TypoBar editor={editor} />
<Flexbox
onClick={() => {
editor?.focus();
}}
padding={16}
style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
>
<div
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Editor
autoFocus
content={''}
editor={editor}
onInit={(editor) => {
if (!editor || !value) return;
try {
editor?.setDocument('markdown', value);
} catch (e) {
console.error('setDocument error:', e);
}
}}
onTextChange={(editor) => {
try {
const newValue = editor.getDocument('markdown') as unknown as string;
onChange?.(newValue);
} catch (e) {
console.error('getDocument error:', e);
onChange?.('');
}
}}
plugins={[
ReactListPlugin,
ReactCodePlugin,
ReactCodemirrorPlugin,
ReactHRPlugin,
ReactLinkPlugin,
ReactTablePlugin,
ReactMathPlugin,
]}
style={{
paddingBottom: 120,
}}
type={'text'}
variant={'chat'}
/>
</div>
</Flexbox>
</>
);
};
export default EditorCanvas;

View File

@@ -0,0 +1,28 @@
import { TextArea } from '@lobehub/ui';
import { FC } from 'react';
interface EditorCanvasProps {
onChange?: (value: string) => void;
value?: string;
}
const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
return (
<TextArea
onChange={(e) => {
onChange?.(e.target.value);
}}
style={{
cursor: 'text',
maxHeight: '80vh',
minHeight: '50vh',
overflowY: 'auto',
padding: 16,
}}
value={value}
variant={'borderless'}
/>
);
};
export default EditorCanvas;

View File

@@ -4,7 +4,6 @@ import {
ChatInputActionBar,
ChatInputActions,
type ChatInputActionsProps,
CodeLanguageSelect,
} from '@lobehub/editor/react';
import { cssVar } from 'antd-style';
import {
@@ -119,16 +118,6 @@ const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
].filter(Boolean) as ChatInputActionsProps['items'],
[editorState],
);

View File

@@ -0,0 +1,51 @@
import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
import EditorCanvas from './EditorCanvas';
import TextareCanvas from './TextareCanvas';
interface EditorModalProps extends ModalProps {
onConfirm?: (value: string) => Promise<void>;
value?: string;
}
export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const { t } = useTranslation('common');
const [v, setV] = useState(value);
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
const Canvas = enableRichRender ? EditorCanvas : TextareCanvas;
return (
<Modal
cancelText={t('cancel')}
closable={false}
confirmLoading={confirmLoading}
destroyOnHidden
okText={t('ok')}
onOk={async () => {
setConfirmLoading(true);
await onConfirm?.(v || '');
setConfirmLoading(false);
}}
styles={{
body: {
overflow: 'hidden',
padding: 0,
},
}}
title={null}
width={'min(90vw, 920px)'}
{...rest}
>
<Canvas onChange={(v) => setV(v)} value={v} />
</Modal>
);
});
export const createEditorModal = (props: EditorModalProps) => createRawModal(EditorModal, props);

View File

@@ -7,12 +7,7 @@ import {
INSERT_HEADING_COMMAND,
getHotkeyById,
} from '@lobehub/editor';
import {
ChatInputActions,
type ChatInputActionsProps,
CodeLanguageSelect,
FloatActions,
} from '@lobehub/editor/react';
import { ChatInputActions, type ChatInputActionsProps, FloatActions } from '@lobehub/editor/react';
import { Block } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import {
@@ -283,17 +278,6 @@ const TypoBar = memo<ToolbarProps>(({ floating, style, className }) => {
label: t('typobar.codeblock'),
onClick: editorState.codeblock,
},
!floating &&
editorState.isCodeblock && {
children: (
<CodeLanguageSelect
onSelect={(value) => editorState.updateCodeblockLang(value)}
value={editorState.codeblockLang}
/>
),
disabled: !editorState.isCodeblock,
key: 'codeblockLang',
},
];
return baseItems.filter(Boolean) as ChatInputActionsProps['items'];