与白板一起使用

让 AI 代理直接操作白板。Server AI Toolkit 的白板工具可以添加便签、绘制流程图、将便签分列、连接或修改形状、删除形状,以及为较长文本添加文档框架。工具会修改白板对应的 Tiptap Cloud 文档,因此所有协作者都能实时看到更改。

独立产品

Whiteboard 是独立于 Server AI Toolkit 的 Tiptap 产品,需单独购买。

实验性功能

Whiteboard 扩展目前处于 Alpha 阶段。

AI 代理聊天机器人指南的后续内容

本指南是 AI 代理聊天机器人指南 的后续内容。请先阅读该指南。

工作原理

  1. AI 使用 readBoard 读取白板:其形状、形状之间的箭头以及用户选择的形状。
  2. AI 使用其他白板工具修改白板,例如 createShapes 和 createDiagram。AI 不需要提供坐标;这些工具会调整新形状的大小,并将其放在 AI 指定或选中的形状旁边,或放在用户视口中不与其他形状重叠的位置。
  3. 每个文档框架都会在自己的 Tiptap Cloud 文档中显示内容。AI 使用 tiptapRead 和 tiptapEdit 操作文档。
  4. 设置 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),用户同时对之间的形状进行的编辑可能会丢失或落在另一个形状上。

后续步骤