news 2026/9/10 6:50:24

用 tldraw 构建 AI 聊天应用:白板绘画、图片标注与多模态上下文实战指南(chat 模板全解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 tldraw 构建 AI 聊天应用:白板绘画、图片标注与多模态上下文实战指南(chat 模板全解析)

用 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.jsontldraw_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 的说明,本地启动非常直接:

  1. 安装依赖:使用yarnnpm install(该模板是 tldraw monorepo 的一部分,仓库根目录使用 yarn workspace 管理,推荐yarn);
  2. 启动开发服务器:运行yarn devnpm run devpackage.json中该脚本为next dev -H 0.0.0.0,会监听所有网卡地址,方便容器或局域网访问);
  3. 打开应用:浏览器访问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_here

Key 的获取方式为 Google AI Studio。模板同时说明:如果你不想用 Google,可以通过 Vercel AI SDK 的 Providers 机制切换到其他模型提供商(例如 OpenAI、Anthropic 等),只需修改 聊天 API 路由 中streamTextmodel参数。

从源码看,这个 Key 有两处消费方:

  • 聊天接口:@ai-sdk/googlegoogle('gemini-3.7-flash')模型调用;
  • 文件上传接口:@google/genaiGoogleGenAI客户端用于上传图片文件,若未配置 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.tsNext.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 中的chatInputReduceropenWhiteboard置为非空对象,从而渲染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会:

  1. 检查editor.getCurrentPageShapes()画布为空则直接取消,避免发送空白图;
  2. editor.toImageDataUrl(shapes, { format: 'png' })把当前画布渲染为 PNG 的 data URL;
  3. editor.getSnapshot()保存完整画布状态快照(TLEditorSnapshot),这样图片加入对话后仍可"重新打开继续编辑";
  4. 组装WhiteboardImage(含idnameurlsnapshottype: 'image/png'、宽高等)交给父组件加入输入区。

4. 组装消息并发送

Chat.tsx 的handleSendMessage把输入区的文本与所有白板图片组装为 AI SDK 的 UI Parts:每张图片是一个FileUIPart(携带urlfilenamemediaType,以及providerMetadata.tldraw中的快照与图片名),文本则是TextUIPart。随后调用useChat返回的sendMessage({ parts })

消息发送前还经过DefaultChatTransportprepareSendMessagesRequest钩子:先调用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 展示,同时把上传元数据(uploadedUrlexpiresAt)暂存在providerMetadata.tldraw_uploaded中。下次发送时若上传未过期(expiresAt > now)则直接复用,过期则重新上传——这就是useChatMessageStorage里消息可长期复用、图片仍能显示的原因。

对应的 upload/route.ts 读取content-typex-file-name请求头(缺失返回 400),然后用GoogleGenAIfiles.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(携带snapshotiduploadedFileimageName)/closeWhiteboard:打开与关闭白板弹窗,snapshot的存在让"历史图片重新编辑"成为可能;
  • dragEnter/dragLeave/drop:拖拽上传的三态流转,drop直接把文件存入openWhiteboard触发白板。

Chat.tsx的拖拽处理器还做了细节防御:仅在拖入的是文件(dataTransfer.types.includes('Files'))且白板未打开时才进入拖拽态;drop时校验file.type.startsWith('image/'),非图片则退出拖拽态。

可扩展方向

基于模板源码,可以自然延伸出以下改造点:

  1. 更换模型:修改 chat 路由 的google('gemini-3.7-flash')为其他 Vercel AI SDK 提供商模型,并同步调整系统提示词;
  2. 更换文件托管/api/upload的 Google GenAI 上传可替换为任意对象存储(S3、R2 等),只需保持返回{ uploadedUrl, expiresAt }的结构,前端uploadMessageContents无需改动;
  3. 开放画布给模型:当前系统提示词明确禁止模型操作白板,若想让 AI 反向生成草图,可在此基础上接入 tldraw 的文档快照写入能力(如editor.loadSnapshot)自行扩展;
  4. 自定义画布选项WhiteboardModal中的TldrawOptionsmaxPagesactionShortcutsLocationmaxFontsToLoadBeforeRender)是理解 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:49:35

Linux磁盘与文件系统从入门到排查:分区、LVM、NFS与常见故障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:49:32

共享储能下多微电网优化调度:Stackelberg博弈与Matlab仿真实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:48:22

danswer(Onyx)Box 连接器每日集成测试环境搭建与运行指南

danswer&#xff08;Onyx&#xff09;Box 连接器每日集成测试环境搭建与运行指南 【免费下载链接】danswer Open Source AI Platform - AI Chat with advanced features that works with every LLM 项目地址: https://gitcode.com/GitHub_Trending/da/danswer 本文以仓库…

作者头像 李华
网站建设 2026/9/10 6:47:11

伴随灵敏度分析在肿瘤生长模型与时空放疗优化中的应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:45:30

并网微电网经济调度:粒子群算法的建模、仿真与工程调参

并网微电网的经济调度&#xff0c;表面上是个优化问题&#xff0c;实际上是个“既要又要还要”的复杂决策。很多人一开始觉得&#xff0c;这不就是让成本最低的机组多发电吗&#xff1f;真做起来会发现完全不是这么回事——光伏和风电的出力随风随云飘忽不定&#xff0c;蓄电池…

作者头像 李华