AI
'use client';
import * as React from 'react';
import { Plate } from '@udecode/plate/react';
import { editorPlugins } from '@/components/editor/plugins/editor-plugins';
import { useCreateEditor } from '@/components/editor/use-create-editor';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { DEMO_VALUES } from './values/demo-values';
export default function Demo({ id }: { id: string }) {
const editor = useCreateEditor({
plugins: [...editorPlugins],
value: DEMO_VALUES[id],
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}
安装
pnpm add @udecode/plate-ai @udecode/plate-selection @udecode/plate-markdown @udecode/plate-basic-marks
使用指南
插件配置
import { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';
import {
BaseBoldPlugin,
BaseCodePlugin,
BaseItalicPlugin,
BaseStrikethroughPlugin,
BaseUnderlinePlugin,
} from '@udecode/plate-basic-marks';
import { BaseBlockquotePlugin } from '@udecode/plate-block-quote';
import {
BaseCodeBlockPlugin,
BaseCodeLinePlugin,
BaseCodeSyntaxPlugin,
} from '@udecode/plate-code-block';
import { BaseParagraphPlugin, createSlateEditor } from '@udecode/plate';
import { BaseHeadingPlugin, HEADING_LEVELS } from '@udecode/plate-heading';
import { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule';
import { BaseIndentListPlugin } from '@udecode/plate-indent-list';
import { BaseLinkPlugin } from '@udecode/plate-link';
import { MarkdownPlugin } from '@udecode/plate-markdown';
export const createAIEditor = () => {
const editor = createSlateEditor({
id: 'ai',
plugins: [
BaseBlockquotePlugin,
BaseBoldPlugin,
BaseCodeBlockPlugin,
BaseCodeLinePlugin,
BaseCodePlugin,
BaseCodeSyntaxPlugin,
BaseItalicPlugin,
BaseStrikethroughPlugin,
BaseUnderlinePlugin,
BaseHeadingPlugin,
BaseHorizontalRulePlugin,
BaseLinkPlugin,
BaseParagraphPlugin,
BaseIndentListPlugin.extend({
inject: {
targetPlugins: [
BaseParagraphPlugin.key,
...HEADING_LEVELS,
BaseBlockquotePlugin.key,
BaseCodeBlockPlugin.key,
],
},
options: {
listStyleTypes: {
todo: {
liComponent: TodoLiStatic,
markerComponent: TodoMarkerStatic,
type: 'todo',
},
},
},
}),
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
],
});
return editor;
};
const systemCommon = `\
你是一个先进的AI智能笔记助手,旨在提升笔记管理的生产力和创造力。
直接响应用户提示,提供清晰、简洁且相关的内容。保持中立、有帮助的语气。
规则:
- <Document>代表用户正在处理的整个笔记文档
- <Reminder>是关于如何响应INSTRUCTIONS的提示,不适用于问题回答
- 其他内容均为用户提示
- 你的响应应针对用户提示,提供精确的笔记管理协助
- 对于INSTRUCTIONS:严格遵循<Reminder>。仅提供需要插入或替换的内容,不要包含解释或评论
- 对于QUESTIONS:提供有帮助且简洁的回答,必要时可包含简短说明
- 关键:区分INSTRUCTIONS和QUESTIONS。指令通常要求修改或添加内容,问题则询问信息或澄清
`;
const systemDefault = `\
${systemCommon}
- <Block>代表用户当前处理的文本块
- 确保输出能无缝融入现有<Block>结构
- 关键:仅提供单个文本块,不要创建多个段落或独立块
<Block>
{block}
</Block>
`;
const systemSelecting = `\
${systemCommon}
- <Block>包含用户选区的文本块,提供上下文
- 确保输出能无缝融入现有<Block>结构
- <Selection>是用户在块中选定的特定文本,需要修改或询问
- 考虑<Block>提供的上下文,但仅修改<Selection>。响应应直接替换<Selection>
<Block>
{block}
</Block>
<Selection>
{selection}
</Selection>
`;
const systemBlockSelecting = `\
${systemCommon}
- <Selection>代表用户选中的完整文本块,需要修改或询问
- 响应应直接替换整个<Selection>
- 除非明确指示,否则保持选中块的整体结构和格式
- 关键:仅提供替换<Selection>的内容。除非特别要求,不要添加额外块或改变块结构
<Selection>
{block}
</Selection>
`;
const userDefault = `<Reminder>
关键:不要使用块级格式,仅允许行内格式
关键:不要换行或新建段落
永远不要输出<Block>
</Reminder>
{prompt}`;
const userSelecting = `<Reminder>
如果是问题,提供关于<Selection>的有帮助且简洁的回答
如果是指令,仅提供替换<Selection>的文本,不要解释
确保内容能无缝融入<Block>。如果<Block>为空,写一个随机句子
永远不要输出<Block>或<Selection>
</Reminder>
{prompt} 关于 <Selection>`;
const userBlockSelecting = `<Reminder>
如果是问题,提供关于<Selection>的有帮助且简洁的回答
如果是指令,仅提供替换整个<Selection>的内容,不要解释
除非另有指示,否则保持整体结构
永远不要输出<Block>或<Selection>
</Reminder>
{prompt} 关于 <Selection>`;
export const PROMPT_TEMPLATES = {
systemBlockSelecting,
systemDefault,
systemSelecting,
userBlockSelecting,
userDefault,
userSelecting,
};
const plugins = [
// ...其他插件
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
AIPlugin,
AIChatPlugin.configure({
options: {
createAIEditor,
promptTemplate: ({ isBlockSelecting, isSelecting }) => {
return isBlockSelecting
? PROMPT_TEMPLATES.userBlockSelecting
: isSelecting
? PROMPT_TEMPLATES.userSelecting
: PROMPT_TEMPLATES.userDefault;
},
systemTemplate: ({ isBlockSelecting, isSelecting }) => {
return isBlockSelecting
? PROMPT_TEMPLATES.systemBlockSelecting
: isSelecting
? PROMPT_TEMPLATES.systemSelecting
: PROMPT_TEMPLATES.systemDefault;
},
},
render: { afterEditable: () => <AIMenu /> },
}),
];
AI SDK集成
本插件依赖ai包:
- 使用streamText设置路由处理器
- 在AI菜单组件中集成useChat
将数据块转换为Plate节点
默认情况下,AI响应以分块方式流式传输到Plate编辑器。但这种方式在处理复杂节点(如表格和代码块)时可能存在问题。如果分块过于频繁,会导致这些重量级节点被反复替换,引发性能问题。
解决方案是使用streamText
函数中的experimental_transform
参数配合smoothStream
函数来优化分块传输。下面的分块函数实现了默认行为:对表格、数学公式、链接和代码块使用基于行的分块,对其他内容使用基于词的分块。
const result = streamText({
experimental_transform: smoothStream({
chunking: (buffer) => {
// 检测代码块标记
if (/```[^\s]+/.test(buffer)) {
isInCodeBlock = true
}else if(isInCodeBlock && buffer.includes('```') ) {
isInCodeBlock = false
}
// 测试用例:不应反序列化带有markdown语法的链接
if (buffer.includes('http')) {
isInLink = true;
} else if (buffer.includes('https')) {
isInLink = true;
} else if (buffer.includes('\n') && isInLink) {
isInLink = false;
}
if (buffer.includes('*') || buffer.includes('-')) {
isInList = true;
} else if (buffer.includes('\n') && isInList) {
isInList = false;
}
// 简单表格检测:遇到|进入表格模式,遇到双换行退出
if (!isInTable && buffer.includes('|')) {
isInTable = true;
} else if (isInTable && buffer.includes('\n\n')) {
isInTable = false;
}
// 根据内容类型选择分块策略
let match;
if (isInCodeBlock || isInTable || isInLink) {
// 对代码块和表格使用行分块
match = CHUNKING_REGEXPS.line.exec(buffer);
} else if (isInList) {
// 对列表使用列表分块
match = CHUNKING_REGEXPS.list.exec(buffer);
} else {
// 对常规文本使用词分块
match = CHUNKING_REGEXPS.word.exec(buffer);
}
if (!match) {
return null;
}
return buffer.slice(0, match.index) + match?.[0];
},
}),
// 其他配置项
// maxTokens: 2048,
// messages: convertToCoreMessages(messages),
// model: openai('gpt-4o'),
// system: system,
});
快捷键
Key | Description |
---|---|
Space | 在空段落中打开AI菜单(光标模式) |
Cmd + J | 打开AI菜单(光标或选区模式) |
Escape | 关闭AI菜单 |
示例
Plate UI
参考上方预览。
Plate Plus
Combobox menu with free-form prompt input
- Additional trigger methods:
- Block menu button
- Slash command menu
- Beautifully crafted UI
插件
AIPlugin
扩展编辑器以支持AI转换功能。
AIChatPlugin
此插件为实验性功能。
在编辑器中启用聊天操作和流式文本生成。
'chat'
:显示带接受/拒绝选项的预览'insert'
:直接将内容插入编辑器- 默认值:
'chat'
- 默认值:
false
- 默认值:
false
{block}
:选区中块的Markdown{editor}
:整个编辑器内容的Markdown{selection}
:当前选区的Markdown{prompt}
:实际用户提示- 默认值:
'{prompt}'
用于生成AI响应的编辑器实例。
由useChat返回的聊天助手。
指定助手消息处理方式:
AI聊天界面是否打开。
AI响应是否正在流式传输(仅光标模式)。
提示模板,支持占位符:
系统消息模板,支持与promptTemplate相同的占位符。
内部使用,用于streamInsertChunk。
内部使用,用于跟踪块路径。
API接口
api.aiChat.accept
接受当前AI建议:
- 从内容中移除AI标记
- 隐藏AI聊天界面
- 聚焦编辑器
api.aiChat.insertBelow
在当前段落下方插入AI内容。
处理块选区和普通选区模式:
- 块选区:在最后一个选中块后插入,应用最后一个块的格式
- 普通选区:在当前块后插入,应用当前块的格式
api.aiChat.replaceSelection
用AI内容替换当前选区。
处理不同选区模式:
- 单块选区:替换选中块,根据format选项应用格式
- 多块选区:替换所有选中块
format: 'none'
或'single'
:保留原始格式format: 'all'
:将第一个块的格式应用到所有内容
- 普通选区:替换当前选区同时保持上下文
api.aiChat.reset
重置聊天状态:
- 停止任何正在进行的生成
- 清除聊天消息
- 从编辑器中移除所有AI节点
api.aiChat.node
获取AI聊天节点entry。
api.aiChat.reload
重新加载当前AI聊天:
- 插入模式:撤销之前的AI更改
- 使用当前系统提示重新加载聊天
api.aiChat.show
显示AI聊天界面:
- 重置聊天状态
- 清除消息
- 将打开状态设为true
api.aiChat.hide
隐藏AI聊天界面:
- 重置聊天状态
- 将打开状态设为false
- 聚焦编辑器
- 移除AI锚点
api.aiChat.stop
停止当前AI生成:
- 将流式状态设为false
- 调用聊天停止函数
api.aiChat.submit
提交提示生成AI内容。
转换方法
tf.aiChat.removeAnchor
移除AI聊天锚点节点。
tf.ai.insertNodes
插入带AI标记的AI生成节点。
tf.ai.removeMarks
从指定位置的节点中移除AI标记。
tf.ai.removeNodes
移除具有AI标记的节点。
tf.ai.undo
AI更改的特殊撤销操作:
- 如果最后一个操作是AI生成的,则撤销该操作
- 移除重做栈条目以防止重做AI操作
useAIChatEditor
一个在AI聊天插件中注册编辑器的钩子,并使用块级记忆化反序列化markdown内容。
const AIChatEditor = ({ content }: { content: string }) => {
const aiEditor = usePlateEditor({
plugins: [
// 你的编辑器插件
MarkdownPlugin,
// 等等...
],
});
useAIChatEditor(aiEditor, content, {
// 可选的markdown解析器选项
parser: {
exclude: ['space'],
},
});
return <Editor editor={aiEditor} />;
};