WCP 5.0.3 将 AI 操作直接嵌入富文本编辑器:用户选中一段文字后,界面浮出 AI 按钮,可以继续执行润色、续写、翻译、总结或简化,并把生成结果替换到原位置,或者插入当前文档。关键变化不只是增加了几个 AI 命令,而是把调用 AI 的动作压缩进现有写作流程,省掉切换窗口、复制文本、粘贴结果等步骤。
AI 功能为什么适合出现在选区旁边
传统 AI 写作工具往往要求用户离开编辑器:复制原文,打开对话框,解释任务,再把输出贴回来。每次跳转都带来额外成本,还容易丢失段落位置、标题层级和当前写作意图。
选区式交互把上下文变得明确:
- 用户选中的文本就是本次处理对象,不需要重复说明范围。
- AI 按钮靠近选区出现,操作与当前注意力位置一致。
- 结果可以原地替换,也可以作为新内容插入,适配润色和续写两类不同任务。
- 编辑器仍然是主界面,AI 只是其中一个文本工具,而不是另一个独立工作台。
这里需要区分两种写入语义。润色、翻译、总结和简化通常适合“替换选区”;续写更适合“在选区后插入”。如果所有命令都直接覆盖原文,用户很容易因为误操作丢失内容。
在 TipTap 中可以怎样实现
下面是一个可改造的最小 React 示例。它不是对 WCP 内部实现的推断,而是根据“选中文字后浮出 AI 操作”这一交互方式给出的实践方案。
先创建项目并安装依赖:
npm create vite@latest tiptap-ai-demo -- --template react
cd tiptap-ai-demo
npm install
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-bubble-menu
npm run dev
将 src/App.jsx 替换为以下内容。示例使用本地函数模拟 AI 响应,因此可以直接运行;接入真实模型时,只需替换 runAI 函数。
import { useState } from 'react'
import { EditorContent, useEditor } from '@tiptap/react'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import StarterKit from '@tiptap/starter-kit'
import './App.css'
const actions = {
polish: '润色',
continue: '续写',
translate: '翻译',
summarize: '总结',
simplify: '简化',
}
async function runAI(action, text) {
await new Promise(resolve => setTimeout(resolve, 400))
const demoResults = {
polish: `经过润色:${text}`,
continue: `${text} 接下来可以围绕这一观点补充事实、案例和边界条件。`,
translate: `Translation: ${text}`,
summarize: `摘要:${text.slice(0, 30)}${text.length > 30 ? '……' : ''}`,
simplify: `简化表达:${text}`,
}
return demoResults[action]
}
export default function App() {
const [loading, setLoading] = useState(false)
const editor = useEditor({
extensions: [StarterKit, BubbleMenu],
content: `
<h2>选中这段文字试一试</h2>
<p>把人工智能能力放进编辑器,可以减少复制、切换窗口和重新定位内容带来的操作成本。</p>
`,
})
async function handleAction(action) {
if (!editor || loading) return
const { from, to } = editor.state.selection
const selectedText = editor.state.doc.textBetween(from, to, ' ')
if (selectedText.trim().length < 2) return
setLoading(true)
try {
const result = await runAI(action, selectedText)
const chain = editor.chain().focus()
if (action === 'continue') {
chain.insertContentAt(to, `<p>${result}</p>`).run()
} else {
chain.insertContentAt({ from, to }, result).run()
}
} finally {
setLoading(false)
}
}
if (!editor) return null
return (
<main>
<BubbleMenu
editor={editor}
shouldShow={({ state }) => !state.selection.empty}
>
<div className='ai-menu'>
{Object.entries(actions).map(([action, label]) => (
<button
key={action}
disabled={loading}
onClick={() => handleAction(action)}
>
{loading ? '处理中…' : `AI ${label}`}
</button>
))}
</div>
</BubbleMenu>
<EditorContent editor={editor} />
</main>
)
}
再把下面的样式写入 src/App.css:
body {
margin: 0;
color: #202124;
background: #f5f6f8;
font-family: system-ui, sans-serif;
}
main {
max-width: 760px;
min-height: 420px;
margin: 48px auto;
padding: 32px;
background: white;
border: 1px solid #dfe1e5;
}
.ProseMirror {
min-height: 320px;
line-height: 1.8;
outline: none;
}
.ai-menu {
display: flex;
gap: 6px;
padding: 6px;
background: #202124;
border-radius: 6px;
box-shadow: 0 4px 14px rgb(0 0 0 / 20%);
}
.ai-menu button {
padding: 6px 9px;
color: white;
background: transparent;
border: 0;
cursor: pointer;
}
.ai-menu button:hover {
background: #3c4043;
}
.ai-menu button:disabled {
cursor: wait;
opacity: 0.6;
}
这段实现展示了选区式 AI 编辑的核心链路:读取 TipTap selection,提取纯文本,调用处理函数,然后按命令类型替换选区或在选区后插入内容。生产环境还应保存选区位置或创建事务映射,避免 AI 请求期间用户继续编辑,导致返回结果写入错误位置。
接入真实模型时,别把密钥放进浏览器
前端应该调用自己的服务端接口,由服务端保存模型密钥、限制输入长度并记录必要的审计信息。下面是一个可以改造的请求约定:
curl -X POST http://localhost:3000/api/ai/edit \
-H 'Content-Type: application/json' \
-d '{
"action": "polish",
"text": "这是一段需要调整得更加清楚的文字。",
"locale": "zh-CN"
}'
服务端可以把动作映射到固定提示词,而不是允许浏览器提交任意系统提示词:
const instructions = {
polish: '润色文本,保持事实和原意不变,只返回修改后的正文。',
continue: '续写文本,保持语气一致,只返回新增内容。',
translate: '将文本翻译为目标语言,只返回译文。',
summarize: '压缩为简洁摘要,不补充原文之外的事实。',
simplify: '简化句式和词汇,保留关键信息。',
}
app.post('/api/ai/edit', async (req, res) => {
const { action, text, locale = 'zh-CN' } = req.body
if (!instructions[action]) {
return res.status(400).json({ error: 'unsupported_action' })
}
if (typeof text !== 'string' || text.length < 2 || text.length > 5000) {
return res.status(400).json({ error: 'invalid_text' })
}
const result = await callYourModel({
instruction: instructions[action],
text,
locale,
})
res.json({ result })
})
callYourModel 需要替换为实际使用的模型 SDK。固定动作、限制文本长度和只返回正文,可以降低提示词注入、费用失控以及模型输出说明性废话的概率,但不能完全消除这些风险。
上线前要补齐的产品边界
AI 原地改写很顺手,也意味着错误结果可能更快进入正式文档。实际部署时建议检查以下事项:
- 保留撤销能力:每次 AI 写入都应成为独立的 TipTap transaction,让用户可以用一次撤销恢复原文。
- 提供结果预览:高风险文档可先显示原文与结果差异,再由用户确认替换。
- 处理并发编辑:AI 返回前文档可能已变化,需要校验 selection、文档版本或协同编辑位置映射。
- 限制敏感数据外发:明确哪些知识库、客户资料和内部文档允许发送给模型服务。
- 保留结构信息:只提取纯文本可能丢失链接、加粗和列表格式;复杂场景应同时处理 TipTap JSON,并严格校验模型返回结构。
- 区分失败状态:超时、限流和内容审核失败不能覆盖原文,应给出可重试反馈。
WCP 5.0.3 所体现的方向,是让 AI 从独立对话窗口变成编辑器中的上下文工具。采用这类能力时,值得优先做好三个基础环节:准确读取选区、可撤销地写回结果,以及通过服务端控制模型访问。只有这些边界可靠,选中文字后“一点即用”才不会以文档安全和编辑确定性为代价。