建议
接续 AI 代理聊天机器人指南
本指南是接续AI 代理聊天机器人指南。 请先阅读该指南。
AI 工具包让你可以在漂亮的差异 UI 中展示 AI 生成的更改,方便用户预览并接受或拒绝这些更改。
查看 GitHub 上的源码。
首先,配置 reviewOptions 参数。将模式设置为 'preview',以在更改应用之前显示预览。
// 在 useChat 钩子内部
const result = toolkit.executeTool({
toolName,
input,
reviewOptions: { mode: 'preview' },
})执行工具后,多个建议会显示在编辑器内。每条建议都代表 AI 模型对文档所做的更改。
你可以在用户审核建议时继续对话。在每次调用工具后立即调用 addToolOutput,并同时显示审核 UI。
修改代码,使聊天机器人在显示审核 UI 的同时继续运行:
const [reviewState, setReviewState] = useState({
// 是否显示审核 UI
isReviewing: false,
// 工具调用结果数据
tool: '',
toolCallId: '',
output: null as unknown,
// 从用户操作中收集的反馈事件
userFeedback: [] as SuggestionFeedbackEvent[],
})
const { messages, sendMessage, addToolOutput } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
async onToolCall({ toolCall }) {
if (!editor) return
const { toolName, input, toolCallId } = toolCall
// 使用 AI 工具包执行工具
const toolkit = getAiToolkit(editor)
const result = toolkit.executeTool({
toolName,
input,
reviewOptions: {
mode: 'preview',
},
})
// 立即继续对话
addToolOutput({ tool: toolName, toolCallId, output: result.output })
// 如果工具调用修改了文档,也显示审核 UI
if (result.docChanged) {
setReviewState({
isReviewing: true,
tool: toolName,
toolCallId,
output: result.output,
userFeedback: [],
})
}
},
})样式建议
创建一个 CSS 文件(app/suggestions.css)将建议样式化为红色/绿色。
/* 将删除的文本突出显示为红色 */
.tiptap-ai-suggestion,
.tiptap-ai-suggestion > * {
background-color: oklch(80.8% 0.114 19.571);
color: oklch(0.396 0.141 25.723);
}
/* 较浅的背景色用于更改组(子更改带有更明显的高亮) */
.tiptap-ai-suggestion.tiptap-ai-suggestion--change-group,
.tiptap-ai-suggestion.tiptap-ai-suggestion--change-group > *:not(.tiptap-ai-suggestion-sub-change) {
background-color: oklch(0.936 0.032 17.717);
}
/* 突出显示内联组内的子更改 */
.tiptap-ai-suggestion-sub-change {
background-color: oklch(80.8% 0.114 19.571);
}
/* 将插入文本突出显示为绿色 */
.tiptap-ai-suggestion-diff,
.tiptap-ai-suggestion-diff > * {
background-color: oklch(87.1% 0.15 154.449);
}
/* 较浅背景色用于差异更改组(子更改带有更明显的高亮) */
.tiptap-ai-suggestion-diff.tiptap-ai-suggestion-diff--change-group,
.tiptap-ai-suggestion-diff.tiptap-ai-suggestion-diff--change-group
> *:not(.tiptap-ai-suggestion-diff-sub-change) {
background-color: oklch(0.962 0.044 156.743);
}
/* 在替换差异部件中突出子更改 */
.tiptap-ai-suggestion-diff-sub-change {
background-color: oklch(87.1% 0.15 154.449);
}
/* 正确渲染表格行插入 */
.tiptap-ai-suggestion-diff:has(tr) {
display: contents;
}
.tiptap-ai-suggestion-diff:has(tr) td,
.tiptap-ai-suggestion-diff:has(tr) th {
background-color: oklch(87.1% 0.15 154.449);
}在应用中导入样式表:
// app/page.tsx
import './suggestions.css'接受/拒绝所有建议
然后,在 React 组件中显示按钮以拒绝/接受这些更改:
{
reviewState.isReviewing && (
<div>
<h2>审核更改</h2>
<button
onClick={() => {
const toolkit = getAiToolkit(editor)
const result = toolkit.acceptAllSuggestions()
// 合并所有反馈事件(之前的 + 新的)
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
let output = reviewState.output
// 如果有未接受的更改,附加反馈到工具输出
if (userFeedback.length > 0 && userFeedback.some((event) => !event.accepted)) {
const outputString =
typeof output === 'string' ? output : JSON.stringify(output)
output = `${outputString}\n\n<user_feedback>\n${JSON.stringify(userFeedback)}\n</user_feedback>`
}
addToolOutput({
tool: reviewState.tool,
toolCallId: reviewState.toolCallId,
output,
})
// 重置反馈事件并关闭审核 UI
setReviewState({
...reviewState,
isReviewing: false,
userFeedback: [],
})
}}
>
全部接受
</button>
<button
onClick={() => {
const toolkit = getAiToolkit(editor)
const result = toolkit.rejectAllSuggestions()
// 合并所有反馈事件(之前的 + 新的)
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
// 将拒绝信息和反馈放进 XML 标签中
const rejectionMessage =
'Some changes you made were rejected by the user. Ask the user why, and what you can do to improve them.'
const outputWithFeedback = `${rejectionMessage}\n\n<user_feedback>\n${JSON.stringify(userFeedback)}\n</user_feedback>`
addToolOutput({
tool: reviewState.tool,
toolCallId: reviewState.toolCallId,
output: outputWithFeedback,
})
// 重置反馈事件并关闭审核 UI
setReviewState({
...reviewState,
isReviewing: false,
userFeedback: [],
})
}}
>
全部拒绝
</button>
</div>
)
}收集 AI 反馈
当用户接受或拒绝建议时,AI 工具包会提供反馈,你可以将其发送回 AI,帮助它从用户偏好中学习。acceptSuggestion、acceptAllSuggestions、rejectSuggestion 和 rejectAllSuggestions 等方法都会返回包含更改信息的 aiFeedback 属性。
const result = toolkit.acceptSuggestion('suggestion-1')
console.log(result.aiFeedback.events)如上例所示,反馈事件被收集在 reviewState.userFeedback 数组中,然后在用户接受或拒绝所有更改时以 XML 标签包裹的形式发送给 AI。这使得 AI 能够理解哪些更改被接受或拒绝,从而改进未来的建议。
接受/拒绝单条建议
你还可以在建议上显示按钮或弹出框,添加接受或拒绝操作。
要在 UI 中渲染自定义元素,设置 reviewOptions.displayOptions 参数。在这里你可以设置 renderDecorations 选项。它是一个返回 ProseMirror decorations 列表的函数。
const result = toolkit.executeTool({
toolName,
input,
reviewOptions: {
mode: 'preview',
displayOptions: {
renderDecorations(options) {
return [
...options.defaultRenderDecorations(),
// 接受按钮
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = '接受'
element.addEventListener('click', () => {
const result = toolkit.acceptSuggestion(options.suggestion.id)
// 使用函数式更新收集反馈事件
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
})
return element
}),
// 拒绝按钮
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = '拒绝'
element.addEventListener('click', () => {
const result = toolkit.rejectSuggestion(options.suggestion.id)
// 使用函数式更新收集反馈事件
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
})
return element
}),
]
},
},
},
})完整演示代码
// app/page.tsx
'use client'
import { useChat } from '@ai-sdk/react'
import { Decoration } from '@tiptap/pm/view'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { AiToolkit, getAiToolkit, type SuggestionFeedbackEvent } from '@tiptap-pro/ai-toolkit'
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
import { useRef, useState } from 'react'
import './suggestions.css'
export default function Page() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, AiToolkit],
content: `<h1>AI 代理演示</h1><p>请 AI 优化这段内容。</p>`,
})
const [reviewState, setReviewState] = useState({
// 是否显示审核 UI
isReviewing: false,
// 工具调用结果数据
tool: '',
toolCallId: '',
output: null as unknown,
// 从用户操作中收集的反馈事件
userFeedback: [] as SuggestionFeedbackEvent[],
})
const acceptButtonRef = useRef<HTMLButtonElement>(null)
const rejectButtonRef = useRef<HTMLButtonElement>(null)
const { messages, sendMessage, addToolOutput } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
async onToolCall({ toolCall }) {
if (!editor) return
const { toolName, input, toolCallId } = toolCall
// 使用 AI 工具包执行工具
const toolkit = getAiToolkit(editor)
const result = toolkit.executeTool({
toolName,
input,
reviewOptions: {
mode: 'preview',
displayOptions: {
renderDecorations(options) {
return [
...options.defaultRenderDecorations(),
// 接受按钮
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = '接受'
element.addEventListener('click', () => {
const result = toolkit.acceptSuggestion(options.suggestion.id)
// 使用函数式更新收集反馈事件
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
acceptButtonRef.current?.click()
}
})
return element
}),
// 拒绝按钮
Decoration.widget(options.range.to, () => {
const element = document.createElement('button')
element.textContent = '拒绝'
element.addEventListener('click', () => {
const result = toolkit.rejectSuggestion(options.suggestion.id)
// 使用函数式更新收集反馈事件
setReviewState((prev) => ({
...prev,
userFeedback: [...prev.userFeedback, ...result.aiFeedback.events],
}))
if (toolkit.getSuggestions().length === 0) {
rejectButtonRef.current?.click()
}
})
return element
}),
]
},
},
},
})
// 立即继续对话
addToolOutput({ tool: toolName, toolCallId, output: result.output })
// 如果工具调用修改了文档,也显示审核 UI
if (result.docChanged) {
setReviewState({
isReviewing: true,
tool: toolName,
toolCallId,
output: result.output,
userFeedback: [],
})
}
},
})
const [input, setInput] = useState('用一个关于 Tiptap 的短故事替换最后一段')
if (!editor) return null
return (
<div>
<EditorContent editor={editor} />
{messages?.map((message) => (
<div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.role}</strong>
<br />
{message.parts
.filter((p) => p.type === 'text')
.map((p) => p.text)
.join('\n')}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault()
if (input.trim()) {
sendMessage({ text: input })
setInput('')
}
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
{reviewState.isReviewing && (
<div>
<h2>审核更改</h2>
<button
ref={acceptButtonRef}
onClick={() => {
const toolkit = getAiToolkit(editor)
const result = toolkit.acceptAllSuggestions()
// 合并所有反馈事件(之前的 + 新的)
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
// 如果有未接受的更改,附加反馈到工具输出
if (userFeedback.length > 0 && userFeedback.some((event) => !event.accepted)) {
sendMessage({
text: `\n\n<user_feedback>\n${JSON.stringify(userFeedback)}\n</user_feedback>`,
})
}
addToolOutput({
tool: reviewState.tool,
toolCallId: reviewState.toolCallId,
output: reviewState.output,
})
// 重置反馈事件并关闭审核 UI
setReviewState({
...reviewState,
isReviewing: false,
userFeedback: [],
})
}}
>
全部接受
</button>
<button
ref={rejectButtonRef}
onClick={() => {
const toolkit = getAiToolkit(editor)
const result = toolkit.rejectAllSuggestions()
// 合并所有反馈事件(之前的 + 新的)
const userFeedback = [...reviewState.userFeedback, ...result.aiFeedback.events]
// 将拒绝信息和反馈放进 XML 标签中
const rejectionMessage =
'Some changes you made were rejected by the user. Ask the user why, and what you can do to improve them.'
const outputWithFeedback = `${rejectionMessage}\n\n<user_feedback>\n${JSON.stringify(userFeedback)}\n</user_feedback>`
sendMessage({ text: outputWithFeedback })
// 重置反馈事件并关闭审核 UI
setReviewState({
...reviewState,
isReviewing: false,
userFeedback: [],
})
}}
>
全部拒绝
</button>
</div>
)}
</div>
)
}最终效果
通过额外的 CSS 样式,结果是一个简单但精致的 AI 聊天机器人应用,用户可以审核 AI 生成的更改:
查看 GitHub 上的源码。
接下来的步骤
高级审核选项
reviewOptions 参数让你可以完全控制审核流程和 UI。
使用 reviewOptions.mode 参数,你可以控制何时将更改应用到文档。
preview:在更改被应用前显示预览。用户接受之前,更改不会应用到文档。review:立即应用更改,然后再进行审核。
完整的 reviewOptions 参数参考,请见 executeTool 方法的 API 参考。
你可以将本指南与 Streaming 结合使用,以便在建议被审核时,流式传输操作并保持助手的响应性。
自定义建议的显示方式
在审核更改时,AI Toolkit 会将更改的预览显示为 建议。
你可以通过设置 reviewOptions.displayOptions 参数来自定义这些建议的显示方式。你甚至可以在其中 渲染 React 组件。