用 tldraw 构建 AI 聊天应用:白板绘画、图片标注与多模态上下文实战指南(chat 模板全解析)
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
导读
本文围绕 tldraw 仓库内 templates/chat 模板展开,讲解如何搭建一个"白板 + 对话"深度融合的 AI 聊天应用:用户既可以用自然语言与 Gemini 模型对话,也可以一键打开 tldraw 画布绘制草图、标注图片,再把画布快照作为视觉上下文发送给模型。读完本文,你将掌握该模板的完整目录结构、本地运行与 API Key 配置方法,并理解其底层实现——从useChat消息流、Google GenAI 文件上传,到 tldraw 白板快照与图像导出的完整调用链。
模板定位:tldraw 生态中的 AI 聊天 Starter Kit
templates/chat是 tldraw 仓库提供的一个 Next.js Starter Kit(在package.json的tldraw_template字段中标注为 "Create an AI chat that uses tldraw for sketches and annotations")。它的核心思路是:把 tldraw 白板作为 AI 对话的视觉输入通道,让模型能够"看见"用户画的图。
这个模板演示了三个核心能力:
- 集成白板提供视觉上下文:聊天过程中随时打开 tldraw 画布作画,草图随消息一起发给模型;
- 图片标注与二次编辑:上传的图片先进入白板,可裁剪、标注后再加入对话,历史消息中的图片也能点击重新打开画布继续编辑;
- 文本聊天与画布输入无缝切换:输入框同时承载文字、图片和白板入口,发送时统一组装为消息内容。
其技术栈在 templates/chat/package.json 中清晰可见:Next.js 16、React 19、Vercel AI SDK(ai、@ai-sdk/react、@ai-sdk/google)、Google GenAI 官方库(@google/genai),以及tldraw(通过workspace:*直接引用仓库内源码)。响应端使用react-markdown渲染 Markdown,并用zod做数据校验。
本地开发:三步跑起来
按 README 的说明,本地启动非常直接:
- 安装依赖:使用
yarn或npm install(该模板是 tldraw monorepo 的一部分,仓库根目录使用 yarn workspace 管理,推荐yarn); - 启动开发服务器:运行
yarn dev或npm run dev(package.json中该脚本为next dev -H 0.0.0.0,会监听所有网卡地址,方便容器或局域网访问); - 打开应用:浏览器访问
http://localhost:3000/即可看到聊天界面。
生产构建则使用yarn build+yarn start。值得注意的是dev脚本显式加了-H 0.0.0.0,这是为远程/容器化开发环境准备的,在纯本地开发时同样适用。
环境变量:配置 Gemini API Key
模板默认使用 Google 的 Gemini 模型,需要在项目根目录创建.env.local并写入 API Key:
GOOGLE_GENERATIVE_AI_API_KEY=your_gemini_api_key_hereKey 的获取方式为 Google AI Studio。模板同时说明:如果你不想用 Google,可以通过 Vercel AI SDK 的 Providers 机制切换到其他模型提供商(例如 OpenAI、Anthropic 等),只需修改 聊天 API 路由 中streamText的model参数。
从源码看,这个 Key 有两处消费方:
- 聊天接口:
@ai-sdk/google的google('gemini-3.7-flash')模型调用; - 文件上传接口:
@google/genai的GoogleGenAI客户端用于上传图片文件,若未配置 Key 会直接返回 500 错误。
文件结构:每个文件负责什么
README 给出了模板的顶层文件地图,结合源码可以进一步明确各自职责:
| 文件(仓库相对路径) | 职责 |
|---|---|
| src/app/page.tsx | 应用入口,渲染<Chat />,外层包tl-theme__light类名套用 tldraw 亮色主题 |
| src/components/Chat.tsx | 聊天主容器,用 Vercel AI SDK 的useChat管理对话流 |
| src/components/MessageList.tsx | 可滚动的消息历史列表,带加载状态 |
| src/components/ChatMessage.tsx | 单条消息渲染(文本用 Markdown,图片用可点击的<img>) |
| src/components/ChatInput.tsx | 输入区:文本框、图片上传、白板按钮、发送按钮 |
| src/components/WhiteboardModal.tsx | 集成 tldraw 的画布弹窗,负责绘画、标注与图像导出 |
| src/app/api/chat/route.ts | Next.js API 路由,用 Vercel AI SDK 对接 Gemini 并流式返回 |
| src/app/api/upload/route.ts | 图片上传 API,把图片托管到 Google GenAI 文件服务 |
| src/app/styles.css | 全部组件的响应式样式 |
README 未列出的支撑文件同样关键:useChatInputState.ts 用useReducer统一管理输入框/白板弹窗/拖拽状态;useChatMessageStorage.tsx 用浏览器 OPFS 持久化消息;useScrollToBottom.ts 维护消息自动滚动;uploadMessageContents.ts 负责消息中图片的"本地 URL → 远端 URL"转换。另有 icons 目录提供 Upload / Whiteboard / Send / Image / X 等 SVG 图标组件。
核心交互流程:从画布到模型
README 描述的关键交互包括:自然语言聊天、点击白板按钮打开画布、绘画与画图补充对话、在画布上标注图片。下面按代码路径拆解这条主链路。
1. 打开白板
ChatInput.tsx 底部有三个按钮:图片上传(ImageIcon)、白板绘图(WhiteboardIcon)、发送(SendIcon)。点击白板按钮触发dispatch({ type: 'openWhiteboard' }),由 useChatInputState.ts 中的chatInputReducer把openWhiteboard置为非空对象,从而渲染WhiteboardModal。
上传图片走的也是白板:点击图片按钮后创建隐藏的<input type="file" accept="image/*">,选中的文件同样以openWhiteboard动作(携带uploadedFile)进入白板弹窗——这意味着任何图片都会先经白板,再进对话。此外还支持把图片文件直接拖拽到输入区域(dragEnter/dragLeave/drop三个动作维护拖拽状态),拖入后同样打开白板。
2. 画布内的处理
WhiteboardModal.tsx 是集成 tldraw 的核心。它用useMemo缓存TLComponents,覆盖了右上角的SharePanel,替换为 Cancel / Add(Save) 两个按钮——注释明确指出 components 必须 memoize 或定义在组件外部,避免 tldraw 频繁重渲染。
模板通过Partial<TldrawOptions>定制了画布行为:
const options: Partial<TldrawOptions> = { // 禁用新建页面:画布始终只有 1 页 maxPages: 1, // 操作快捷键始终显示在右上角菜单区,而不是工具栏上 actionShortcutsLocation: 'menu', // 禁用字体预加载,避免弹窗出现后 UI 才"弹"出来 maxFontsToLoadBeforeRender: 0, }如果是从图片上传进入的,InsideOfTldrawContext组件会在 tldraw 上下文内完成图片插入:先通过notifyIfFileNotAllowed校验文件合法性(不合法时用 tldraw 的 toast 提示),再editor.getAssetForExternalContent生成图片 asset,按最长边 1000px 缩放居中创建imageshape 并选中,最后setCurrentTool('select.crop')直接进入裁剪工具——这就是"上传即标注"交互的实现。
3. 保存画布为图片
点击 Save 后,handleSave会:
- 检查
editor.getCurrentPageShapes(),画布为空则直接取消,避免发送空白图; - 用
editor.toImageDataUrl(shapes, { format: 'png' })把当前画布渲染为 PNG 的 data URL; - 用
editor.getSnapshot()保存完整画布状态快照(TLEditorSnapshot),这样图片加入对话后仍可"重新打开继续编辑"; - 组装
WhiteboardImage(含id、name、url、snapshot、type: 'image/png'、宽高等)交给父组件加入输入区。
4. 组装消息并发送
Chat.tsx 的handleSendMessage把输入区的文本与所有白板图片组装为 AI SDK 的 UI Parts:每张图片是一个FileUIPart(携带url、filename、mediaType,以及providerMetadata.tldraw中的快照与图片名),文本则是TextUIPart。随后调用useChat返回的sendMessage({ parts })。
消息发送前还经过DefaultChatTransport的prepareSendMessagesRequest钩子:先调用uploadMessageContents把消息里所有data:开头的图片 URL 上传为远端 URL(详见下一节),再把处理后的消息放入请求体,发给/api/chat。
5. 服务端流式回复
route.ts 接收UIMessage[],用streamText组合模型与系统提示词:
export const maxDuration = 60 // 允许流式响应最长 60 秒 const result = streamText({ model: google('gemini-3.7-flash'), system: [ "You're a friendly AI chatbot.", 'The user can send you images, sketches and diagrams using your built-in tldraw whiteboard.', 'You cannot create or edit whiteboards yourself.', // ... ].join(' '), messages: convertToModelMessages(messages), }) return result.toUIMessageStreamResponse()系统提示词明确告知模型:用户会通过内置 tldraw 白板发送图片、草图和示意图,模型自身不能创建或编辑白板。回复经toUIMessageStreamResponse以流式 UI 消息返回,前端 ChatMessage.tsx 用react-markdown渲染文本部分。
6. 历史消息中的图片回看
聊天记录里的白板图片都带有providerMetadata.tldraw快照。点击图片时,ChatMessage.tsx 会优先把快照交给白板弹窗(onImageClick触发openWhiteboard),实现"点击历史图片 → 在白板中继续编辑 → 重新加入对话"的闭环;若图片没有 tldraw 快照(例如普通上传图),则先用FileHelpers.urlToBlob转成File再传入白板。
图片上传的工程细节:绕开 4.5MB 限制
多模态消息必然涉及图片字节流。模板的 uploadMessageContents.ts 注释解释了关键约束与对策:
- Vercel 限制请求体最大 4.5MB,因此图片不能直接塞进聊天请求,而是改走
/api/upload上传到 Google GenAI 文件服务; - Google 只保存文件 24 小时;
- Google 托管的是私有 URL,不能直接用于前端
<img>展示。
解决方案是"一图两版":发送给服务端的版本用data:URL 换成 Google 返回的uploadedUrl(并剔除tldraw等 providerMetadata);本地保存的版本保留原始 URL 用于 UI 展示,同时把上传元数据(uploadedUrl、expiresAt)暂存在providerMetadata.tldraw_uploaded中。下次发送时若上传未过期(expiresAt > now)则直接复用,过期则重新上传——这就是useChatMessageStorage里消息可长期复用、图片仍能显示的原因。
对应的 upload/route.ts 读取content-type与x-file-name请求头(缺失返回 400),然后用GoogleGenAI的files.upload上传并返回{ uploadedUrl, expiresAt }。上传所用依赖FileHelpers.urlToBlob来自 tldraw 的@tldraw/utils(在 packages/utils 中实现),负责把 data URL 还原为 Blob。
消息持久化:浏览器本地的聊天记忆
useChatMessageStorage.tsx 用浏览器 Origin Private File System(OPFS)把聊天记录存为chat-messages.json。代码注释说明了选型理由:OPFS 容量比 localStorage 大,且比 IndexedDB 更易用。
- 加载:
navigator.storage.getDirectory()→getFileHandle('chat-messages.json')→FileHelpers.blobToText读出文本 →JSON.parse→ 用 AI SDK 的validateUIMessages校验后作为initialMessages; - 保存:
Chat.tsx中监听chat.status === 'ready',在每次对话完成后把chat.messages写入文件(createWritable({ keepExistingData: false })覆盖写)。
当消息为空时,界面进入"空聊天"态:标题显示 "How can I help?",输入框居中展示;有历史消息时则切换到聊天头(含清空按钮ClearChatIcon)+ 消息列表 + 底部输入区的标准布局。
状态管理:一个 Reducer 管住所有输入态
输入区相关的全部状态都被收拢在 useChatInputState.ts 的chatInputReducer中,动作类型覆盖了完整交互矩阵:
setInput:更新文本输入值;setImage:按id新增或更新输入区的图片(去重逻辑:存在则替换,不存在则追加);removeImage/clear:删除单张图片 / 清空全部输入;openWhiteboard(携带snapshot、id、uploadedFile、imageName)/closeWhiteboard:打开与关闭白板弹窗,snapshot的存在让"历史图片重新编辑"成为可能;dragEnter/dragLeave/drop:拖拽上传的三态流转,drop直接把文件存入openWhiteboard触发白板。
Chat.tsx的拖拽处理器还做了细节防御:仅在拖入的是文件(dataTransfer.types.includes('Files'))且白板未打开时才进入拖拽态;drop时校验file.type.startsWith('image/'),非图片则退出拖拽态。
可扩展方向
基于模板源码,可以自然延伸出以下改造点:
- 更换模型:修改 chat 路由 的
google('gemini-3.7-flash')为其他 Vercel AI SDK 提供商模型,并同步调整系统提示词; - 更换文件托管:
/api/upload的 Google GenAI 上传可替换为任意对象存储(S3、R2 等),只需保持返回{ uploadedUrl, expiresAt }的结构,前端uploadMessageContents无需改动; - 开放画布给模型:当前系统提示词明确禁止模型操作白板,若想让 AI 反向生成草图,可在此基础上接入 tldraw 的文档快照写入能力(如
editor.loadSnapshot)自行扩展; - 自定义画布选项:
WhiteboardModal中的TldrawOptions(maxPages、actionShortcutsLocation、maxFontsToLoadBeforeRender)是理解 tldraw 配置入口的良好起点,可依需调整。
许可证说明
模板以 MIT 协议发布(见 templates/chat/LICENSE.md),tldraw SDK 本身遵循仓库根目录 LICENSE.md 的 tldraw 许可;tldraw 名称与 Logo 为 tldraw Inc. 的商标,使用需遵守 TRADEMARKS.md 中的商标指南。
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考