与白板一起使用
让 AI 代理直接操作白板。Server AI Toolkit 的白板工具可以添加便签、绘制流程图、将便签分列、连接或修改形状、删除形状,以及为较长文本添加文档框架。工具会修改白板对应的 Tiptap Cloud 文档,因此所有协作者都能实时看到更改。
独立产品
Whiteboard 是独立于 Server AI Toolkit 的 Tiptap 产品,需单独购买。
实验性功能
Whiteboard 扩展目前处于 Alpha 阶段。
AI 代理聊天机器人指南的后续内容
本指南是 AI 代理聊天机器人指南 的后续内容。请先阅读该指南。
工作原理
- AI 使用
readBoard读取白板:其形状、形状之间的箭头以及用户选择的形状。 - AI 使用其他白板工具修改白板,例如
createShapes和createDiagram。AI 不需要提供坐标;这些工具会调整新形状的大小,并将其放在 AI 指定或选中的形状旁边,或放在用户视口中不与其他形状重叠的位置。 - 每个文档框架都会在自己的 Tiptap Cloud 文档中显示内容。AI 使用
tiptapRead和tiptapEdit操作文档。 - 设置
reviewOptions后,更改会作为修订跟踪建议提交,用户可以在白板上接受或拒绝。
设置白板
白板工具操作的是白板对应的 Tiptap Cloud 文档。请在白板编辑器中配置协作,参见白板协作指南。然后从该编辑器获取上下文,并随每条聊天消息发送,做法与 AI 代理聊天机器人指南 相同:
import { getEditorContext } from '@tiptap/ai-toolkit'
// The board's editor, with WhiteboardKit and BoardCollaboration
const boardEditorContext = getEditorContext(editor)白板工具不会通过哈希定位节点,因此白板编辑器无需添加 ServerAiToolkit 扩展。
启用白板工具
在获取工具定义时启用白板工具,并使用白板的编辑器上下文:
import { getAuthHeaders } from '@/lib/server-ai-toolkit/get-auth-headers'
const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/fetch-tools`, {
method: 'POST',
headers: await getAuthHeaders(),
body: JSON.stringify({
editorContext: boardEditorContext,
tools: {
readBoard: true,
createShapes: true,
arrangeShapes: true,
createDiagram: true,
updateShapes: true,
deleteShapes: true,
connectShapes: true,
createDocFrame: true,
},
}),
})仅启用代理所需的工具。例如,如果没有 deleteShapes,AI 就无法删除形状。
白板工具采用纯文本,而不是文档内容,因此它们在每个 format 中的工作方式相同。它们的描述告诉 AI 如何使用它们,因此不需要此请求的 systemPrompt。无法通过 stream-tool 使用白板工具。
执行板卡工具
执行板卡文档上的板卡工具。 tool.config 告诉他们用户选择和看到的内容:
const result = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
method: 'POST',
headers: await getAuthHeaders(documentId),
body: JSON.stringify({
editorContext: boardEditorContext,
document: { type: 'cloud', id: documentId },
user: 'ai-assistant',
tool: {
name: toolName,
input,
// What the user selected and saw when they sent the message
config: { selectedIds, viewport },
},
}),
})selectedIds是用户选择的形状。readBoard首先列出它们及其全文,因此人工智能知道“这些注释”意味着哪些形状。新的形状位于它们的右侧,或者当距离较近时位于它们的下方。viewport是用户看到的白板部分(以白板为单位)。如果没有选择,新的形状就会出现在中间。
如果没有其中任何一个,新形状将位于白板上所有形状的右侧,或到达空白板的原点。缺失、null 或无效的值会自行回退。 AI 还可以命名 near 中的形状,因此摘要会显示在其图表旁边。请参阅白板布局。
当用户发送消息时在浏览器中读取这两个消息,并将它们一起发送:
import { getSelectedShapeIds, visibleBoardRect } from '@tiptap-pro/extension-whiteboard'
sendMessage(
{ text: input },
{
body: {
editorContext: boardEditorContext,
selectedIds: getSelectedShapeIds(editor.state),
viewport: visibleBoardRect({
// Your app's camera, and the stage element around the board
camera,
viewport: { width: stage.clientWidth, height: stage.clientHeight },
}),
},
},
)当用户在聊天中键入内容时,白板会保留其选择,因此您无需再次聚焦白板。
当用户在工具运行时更换白板时,execute-tool 可以用 concurrent_edit_conflict 回答 409 并且不进行任何更改。再次运行该工具。服务器在 Tiptap Cloud 文档的 default 字段中检查这一点。在另一个field中,它保存而不检查。当文档或编辑器上下文不是白板时,它会用 board_schema_not_supported 回答 403。
编写文档框架的文档
每个文档框架显示其自己的文档。 createDocFrame 添加一个带有新的空文档的框架并返回其 docId,readBoard 列出每个框架的 docId。然后,AI 使用 tiptapRead 和 tiptapEdit 读取和写入文档。
将文档保存在 Tiptap Cloud 中
AI 服务器将文档写入 Tiptap Cloud 中。将框架文档的扩展保留在浏览器和服务器共享的模块中,因为服务器需要相同的架构。向它们添加 ServerAiToolkit 扩展,因为文档工具通过哈希来定位节点:
// lib/whiteboard-docs.ts, shared by the browser and the server
import { ServerAiToolkit } from '@tiptap/ai-toolkit'
import StarterKit from '@tiptap/starter-kit'
export const docExtensions = [StarterKit.configure({ undoRedo: false }), ServerAiToolkit]
// The name of the Tiptap Cloud document of a frame, on the board `documentId`
export const frameDocumentName = (documentId: string, docId: string) => `${documentId}/doc/${docId}`在浏览器中,将每个框架的编辑器连接到其 docId 的 Tiptap Cloud 文档:
import { Editor } from '@tiptap/core'
import { Collaboration } from '@tiptap/extension-collaboration'
import type { CreateDocEditorOptions } from '@tiptap-pro/extension-whiteboard'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import * as Y from 'yjs'
import { docExtensions, frameDocumentName } from '@/lib/whiteboard-docs'
function createEditor({ element, docId }: CreateDocEditorOptions) {
const document = new Y.Doc()
const provider = new TiptapCollabProvider({
name: frameDocumentName(documentId, docId),
appId,
token,
document,
})
return new Editor({
element,
extensions: [...docExtensions, Collaboration.configure({ document })],
onDestroy: () => provider.destroy(),
})
}在 WhiteboardKit 的 docs 选项中将其用作 createEditor。请参阅 copy 和 start 的文档框架 。 AI 添加的帧来自 Yjs,因此 start 不会为它们运行。 createEditor 通过 docId 打开他们的文档,并显示 AI 在那里写入的内容。
浏览器的token 也必须允许框架文件,而不仅仅是白板文件。允许名称以 ${documentId}/doc/ 开头的文档,并带有 前缀约束:
permissions: [
{ action: 'Documents:Write', resource: documentId },
{ action: 'Documents:Write', resource: '*', constraints: { prefix: `${documentId}/doc/` } },
],每个提供程序都会打开自己的连接。要通过一个连接打开所有框架的文档,请参阅Multiplexing.
为文档工具指定一个 docId
文档工具可处理一个文档。在服务器上,使用 docExtensions 从编辑器获取框架文档的编辑器上下文。为其提供 JSON 内容,因为默认内容是 HTML,需要浏览器:
import { getEditorContext } from '@tiptap/ai-toolkit'
import { Editor } from '@tiptap/core'
import { docExtensions } from '@/lib/whiteboard-docs'
// Only for the schema of the frames' documents
const docEditor = new Editor({
extensions: docExtensions,
content: { type: 'doc', content: [{ type: 'paragraph' }] },
})
const docEditorContext = getEditorContext(docEditor)
docEditor.destroy()然后使用该编辑器上下文获取 tiptapRead 和 tiptapEdit。将 docId 添加到他们的输入中,并在 docId 的文档上执行它们:
import { tool } from 'ai'
import z from 'zod'
import { executeTool } from '@/lib/server-ai-toolkit/execute-tool'
import { getToolDefinitions } from '@/lib/server-ai-toolkit/get-tool-definitions'
import { frameDocumentName } from '@/lib/whiteboard-docs'
// tiptapRead and tiptapEdit, and the prompt about the format of the documents
const { prompt: docsPrompt, tools: docToolDefinitions } = await getToolDefinitions(docEditorContext)
const docTools = Object.fromEntries(
docToolDefinitions.map((definition) => [
definition.name,
tool({
description: definition.description,
inputSchema: z.fromJSONSchema({
...definition.inputSchema,
properties: {
docId: {
type: 'string',
description: 'The docId of the doc frame, from readBoard or createDocFrame.',
},
...definition.inputSchema.properties,
},
required: ['docId', ...(definition.inputSchema.required ?? [])],
}),
strict: false,
execute: async (toolInput) => {
const { docId, ...input } = toolInput as Record<string, unknown>
// The docId comes from the AI, and becomes part of a document name
if (typeof docId !== 'string' || !/^[\w-]+$/.test(docId)) {
return { success: false, error: 'Use the docId of a doc frame, from readBoard.' }
}
try {
const result = await executeTool(
definition.name,
input,
frameDocumentName(documentId, docId),
docEditorContext,
)
return result.tool.output
} catch (error) {
console.error(`Failed to execute tool ${definition.name}:`, error)
return { error: error instanceof Error ? error.message : 'Unknown error' }
}
},
}),
]),
)将这些工具与 AI 代理使用的其他工具一起提供,并把 docsPrompt 加入代理指令,让 AI 了解文档格式。
白板会按文档内容绘制文档框架,但 createDocFrame 保存的高度来自 lines 输入。箭头、选区和布局使用保存的高度。只有用户直接编辑文档时,白板才会更新该高度,因此 AI 对文档的更改不会自动改变框架高度。
查看 AI 的更改
在白板编辑器中启用修订跟踪,参见白板修订跟踪指南,并发送该编辑器的上下文。然后在执行白板工具时传入 reviewOptions:
reviewOptions: {
mode: 'trackedChanges',
trackedChangesOptions: {
userId: 'ai-assistant',
userMetadata: { name: 'AI Assistant' },
},
},这样白板工具就会将更改作为建议提交,而不会直接应用:
- 新形状会成为
add建议。移动形状、修改颜色或类型会生成包含新旧值的attributeChange建议;替换文本时,新文本会作为建议替换原段落。 - 被删除的形状会保留在白板上,直到用户接受
delete建议。通过待处理建议添加的形状会立即显示。 - 一次调用添加或删除的形状共享同一条建议,因此调用一次
acceptSuggestion或rejectSuggestion即可处理全部形状。 - 每个被修改的形状和每段新文本都有各自的建议。
suggestionIds列出本次调用创建或继续处理的建议;查看列表中的所有建议即可审核整次调用。继续处理已有建议时,建议也会保留该形状之前的更改。 - 如果某个形状正等待其他用户审核,工具会跳过它,并在
skipped中说明原因。 - 当建议新增、删除或修改形状,或仅修改形状中的文本时,
readBoard会在该形状上标记suggestion。
如果编辑器上下文中没有启用修订跟踪,execute-tool 会像文档工具一样返回 403 和 tracked_changes_unsupported_schema,不会执行更改。若未提供 reviewOptions,工具会直接修改白板,但会跳过所有正在等待审核的形状。
对于文档框架中的文档,请将 TrackedChanges 添加到 docExtensions,确保框架编辑器和 docEditorContext 都启用了修订跟踪。否则,execute-tool 会返回 403 和 tracked_changes_unsupported_schema。然后将相同的 reviewOptions 添加到 executeTool 帮助函数的请求正文中,并传给文档工具。参见与修订跟踪配合使用。如果 AI 在同一轮回答中创建文档,可以跳过审核,因为该文档框架本身已作为建议等待审核。
限制
- 这些工具仅添加白板自己的形状:便签、几何形状、自由文本、箭头和文档框架。
readBoard列出具有 id 和框的每个形状。 - 服务器无法测量浏览器的字体,因此工具会估计标题、标签和文档框架的高度。该白板仅测量其自身用户的更改,因此这些高度将保持不变,直到用户编辑文本。
- 箭头是其形状中心之间的直线,因此长箭头可以穿过其他形状。箭头的标签是自由文本形状,当用户移动箭头的形状时,它会保持在原位。
readBoard最多列出 300 个形状和箭头,首先列出选定的形状。它会在 280 个字符后剪切其他形状的文本。- 服务器通过比较整个白板,将调用写入白板的 Yjs 文档中。当调用删除一个形状并更改后面的形状时(例如释放箭头末端的
deleteShapes),用户同时对之间的形状进行的编辑可能会丢失或落在另一个形状上。