模板工作流
实验性功能
此功能属于实验性质,将来版本中可能会发生变化。
使用 AI 生成的内容填充结构化的 Tiptap 模板,并将结果插入编辑器中。
查看 GitHub 上的源码。
模板工作原理
Tiptap 模板是带有特殊属性的 Tiptap JSON 结构,用于标记动态部分:
_templateSlot(字符串键):整个节点将被 AI 生成的 HTML 内容替换。_templateIf(字符串键):节点仅在布尔值为真时才包含。_templateAttributes(由{key, attribute}对象组成的数组):将特定节点属性设为 AI 生成的值。
以下是一个 Tiptap JSON 格式的示例模板:
{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 1 },
"content": [{ "type": "text", "text": "保密协议" }]
},
{
"type": "paragraph",
"attrs": { "_templateSlot": "parties" },
"content": [{ "type": "text", "text": "当事方信息将在此生成。" }]
},
{
"type": "paragraph",
"attrs": { "_templateIf": "includeArbitration" },
"content": [{ "type": "text", "text": "仲裁条款内容..." }]
},
{
"type": "heading",
"attrs": {
"level": 1,
"_templateAttributes": [{ "key": "sectionLevel", "attribute": "level" }]
},
"content": [{ "type": "text", "text": "适用法律" }]
}
]
}AI 会生成含有每个键对应值的 JSON 对象:
{
"parties": "<p>本协议由 <strong>Acme Corp</strong> 与 <strong>Beta LLC</strong> 签订。</p>",
"includeArbitration": true,
"sectionLevel": 2
}技术栈
- React + Next.js
- Vercel AI SDK + OpenAI 模型
- Tiptap AI 工具包
项目概览
此示例使用 AI 工具包的模板工作流,将 AI 生成内容填充到法律文档模板(保密协议)中。模板包含预定义部分的插槽、条件条款和动态属性。AI 填充动态部分,其余固定的法律模板内容保持不变。
安装
创建一个 Next.js 项目:
npx create-next-app@latest template-workflow安装核心 Tiptap 包以及用于 OpenAI 的 Vercel AI SDK:
npm install @tiptap/react @tiptap/starter-kit ai @ai-sdk/openai zod安装 Tiptap AI 工具包:
Pro 版本包
AI 工具包为专业版包。安装前请先根据 私有注册表指南 配置对私有 NPM 注册表的访问权限。
npm install @tiptap-pro/ai-toolkit @tiptap-pro/ai-toolkit-tool-definitions服务端设置
创建一个 API 端点,使用 Vercel AI SDK 调用 OpenAI 模型。
如果你的后端不是 TypeScript,请参阅 非 TypeScript 后端。
在 API 端点内部,使用 createTemplateWorkflow 将 HTML 模板转换为工作流配置。该函数会自动提取 HTML 中所有模板键,生成系统提示,并创建输出模式。
// app/api/template-workflow/route.ts
import { openai } from '@ai-sdk/openai'
import { createTemplateWorkflow } from '@tiptap-pro/ai-toolkit-tool-definitions'
import { Output, streamText } from 'ai'
export async function POST(req: Request) {
const { htmlTemplate, task } = await req.json()
// 从 HTML 模板创建工作流。
// 会自动提取模板键,生成提示和输出模式。
const workflow = createTemplateWorkflow({ htmlTemplate })
const result = streamText({
model: openai('gpt-5.4-mini'),
system: workflow.systemPrompt,
prompt: JSON.stringify({
task,
context: 'Additional background information related to the task',
}),
output: Output.object({ schema: workflow.zodOutputSchema }),
})
return result.toTextStreamResponse()
}客户端设置
客户端定义模板为 Tiptap JSON。使用 Vercel AI SDK 的 useObject 钩子来流式接收服务器部分返回的值。传入一个宽松的 Zod schema (z.object({}).passthrough()) 来接受服务器返回的任意属性。
随着部分值的到达,调用 templateWorkflow 并传入 hasFinished 和 workflowId 来逐步填充模板。workflowId 允许流式模式,方法会追踪多次调用间的插入范围,迭代替换内容。在调用 API 之前,使用 createHtmlTemplate 方法将模板转换为 HTML,供服务器解析模板键。
// app/template-workflow/page.tsx
'use client'
import { experimental_useObject as useObject } from '@ai-sdk/react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'
import { useEffect, useState } from 'react'
import { v4 as uuid } from 'uuid'
import { z } from 'zod'
// 定义模板为 Tiptap JSON
const template = {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: '保密协议' }],
},
{
type: 'paragraph',
attrs: { _templateSlot: 'parties' },
content: [{ type: 'text', text: '当事方详情...' }],
},
// ... 更多模板内容
],
}
// 宽松的 schema,接受服务器返回任意属性
const templateSchema = z.object({}).passthrough()
export default function Page() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, AiToolkit],
content: '<p>点击“生成”以填充模板。</p>',
})
const [workflowId, setWorkflowId] = useState('')
const { submit, isLoading, object } = useObject({
api: '/api/template-workflow',
schema: templateSchema,
})
// 监听部分结果流式到达
useEffect(() => {
if (!editor || !object) return
const toolkit = getAiToolkit(editor)
toolkit.templateWorkflow({
template,
values: object as Record<string, unknown>,
position: 'document',
hasFinished: !isLoading,
workflowId,
})
}, [object, workflowId, editor, isLoading])
const generate = () => {
if (!editor) return
const toolkit = getAiToolkit(editor)
const htmlTemplate = toolkit.createHtmlTemplate(template)
setWorkflowId(uuid())
submit({
htmlTemplate,
task: '生成 Acme Corp 和 Beta LLC 之间的保密协议',
})
}
if (!editor) return null
return (
<div>
<EditorContent editor={editor} />
<button onClick={generate} disabled={isLoading}>
{isLoading ? '生成中…' : '生成'}
</button>
</div>
)
}最终效果
通过额外的 CSS 样式,效果是一个精致的模板工作流应用:
查看 GitHub 上的源码。
在文档部分插入模板
若要将模板结果插入到特定区域,而不是替换整个文档,请设置 position 选项。它可以是位置或一个 Range。
// 在特定范围插入模板结果
toolkit.templateWorkflow({
template,
values: object,
position: { from: 10, to: 50 },
hasFinished: !isLoading,
workflowId,
})构建模板编辑器
首先,在编辑器中注册 TemplateField 扩展。此扩展将四个模板属性(_templateSlot、_templateIf、_templateAttributes、_templateFieldMetadata)作为全局属性添加到编辑器 schema 中的所有节点类型:
import { TemplateField } from '@tiptap-pro/ai-toolkit'
const editor = new Editor({
extensions: [StarterKit, TemplateField],
})TemplateField 与 AiToolkit
你可以将 TemplateField 与 AiToolkit 扩展一起使用,或者单独使用它,若你只需要模板字段支持而不需要完整的 AI 工具包功能。
TemplateField 扩展提供命令,用于在当前选择的节点上设置和取消模板属性:
// 将选中节点标记为模板插槽
editor.commands.setTemplateSlot('intro')
// 移除模板插槽
editor.commands.unsetTemplateSlot()
// 将选中节点标记为条件包含
editor.commands.setTemplateIf('showDisclaimer')
// 移除条件
editor.commands.unsetTemplateIf()
// 将模板键映射到节点属性
editor.commands.setTemplateAttributes([{ key: 'sectionLevel', attribute: 'level' }])
// 移除属性映射
editor.commands.unsetTemplateAttributes()当你想要直接在模板中存储关于字段的额外信息时,请使用 _templateFieldMetadata。元数据值是可选的,可以包含任何 JSON 对象,以帮助你的应用程序稍后解释该字段:
{
"type": "paragraph",
"attrs": {
"_templateSlot": "intro",
"_templateFieldMetadata": {
"label": "Introduction",
"group": "summary",
"analyticsKey": "template.intro"
}
},
"content": [{ "type": "text", "text": "Placeholder" }]
}你可以将元数据附加到插槽字段、条件字段和属性字段。元数据不会改变模板的填充方式。它仅用于你自己的应用程序逻辑。
使用 [_templateslot] CSS 属性选择器为模板插槽设置样式。TemplateField 扩展在 DOM 中输出小写的模板属性:
[_templateslot] {
border: 2px dashed #6a00f5;
border-radius: 4px;
padding: 4px 8px;
background-color: rgba(106, 0, 245, 0.05);
position: relative;
}
[_templateslot]::after {
content: attr(_templateslot);
position: absolute;
top: -10px;
right: 4px;
font-size: 10px;
background: #6a00f5;
color: white;
padding: 0 4px;
border-radius: 2px;
}填充后保留模板字段
默认情况下,templateWorkflow 方法填充模板后会移除所有 _templateSlot 属性。设置 preserveSlotAttr 为 true,可在填充节点上保留这些属性:
toolkit.templateWorkflow({
template,
values: object,
position: 'document',
hasFinished: !isLoading,
workflowId,
preserveSlotAttr: true,
})当启用 preserveSlotAttr 时,每个填充节点都会保留其 _templateSlot 属性。这允许你:
- 识别 AI 填充的内容:检查文档的哪些部分是由 AI 生成的。
- 重新填充模板字段:将填充后的文档用作新模板以重新生成特定部分。
- 保持字段元数据可用:稍后从填充后的文档中读取
_templateFieldMetadata。
重新填充模板字段
当 preserveSlotAttr 为 true 时,填充后的文档仍包含 _templateSlot 属性。你可以提取文档 JSON,并传回 templateWorkflow 来重新填充特定字段:
// 将当前文档提取为模板
const currentDoc = editor.getJSON()
// 使用新值重新填充
const toolkit = getAiToolkit(editor)
toolkit.templateWorkflow({
template: currentDoc,
values: newValues,
position: 'document',
preserveSlotAttr: true,
})这支持迭代式编辑流程,用户可请求 AI 重新生成文档的单个部分,而不会影响其他内容。
读取字段内容和元数据
AI 工具包还导出 getTemplateFieldMatches(),这是一个辅助函数,用于扫描 Tiptap JSON 文档中的字段名,并按文档顺序返回所有匹配的字段。
当你想要以下操作时,这很有用:
- 读取模板字段的当前内容
- 访问存储在该字段上的
_templateFieldMetadata - 支持重新生成、分析或自定义 UI 标签等工作流
import { getTemplateFieldMatches } from '@tiptap-pro/ai-toolkit'
const matches = getTemplateFieldMatches({
document: editor.getJSON(),
fieldName: 'intro',
})
for (const match of matches) {
console.log(match.fieldType)
console.log(match.metadata)
console.log(match.content)
}每个结果包括:
fieldType:匹配是否来自_templateSlot、_templateIf或_templateAttributesmetadata:_templateFieldMetadata对象,或nullcontent:匹配节点的內容(Tiptap JSON 格式),空节点则为null
控制哪些模板字段是必填的
默认情况下,所有模板字段都是必填的——AI 必须填写每个字段。要将特定字段设为可选,请在服务器端使用 requiredSlots、requiredConditions 和 requiredAttributes。仅列表中的字段是必填,未列出的字段则为可选:
// 服务器端:仅 "intro" 是必填,其他插槽为可选
const workflow = createTemplateWorkflow({
htmlTemplate,
requiredSlots: ['intro'],
})AI 会始终填充必填插槽,也可能根据任务上下文选择填充其他插槽。
要将某类字段全部设为可选,请传入空数组:
// 所有插槽可选,但特定条件和属性为必填
const workflow = createTemplateWorkflow({
htmlTemplate,
requiredSlots: [],
requiredConditions: ['includeDisclaimer'],
requiredAttributes: ['headingLevel'],
})