无限画布无限画布

提示词面板引用显示图片缩略图规划

将提示词面板输入框从 textarea 改造为 contentEditable,让 @ 引用直接显示真实图片缩略图

提示词面板 @ 引用显示真实图片缩略图

背景 / 要解决的问题

提示词面板(CanvasNodePromptPanel)的输入框里,@ 引用一张图片后,显示的是蓝色文字 chip「图片1」。期望直接显示这张图片的真实缩略图,而不是文字编号。

当前该输入框是原生 <textarea>,只能承载纯文本,无法内嵌图片。要显示缩略图,必须改用 contentEditable(可编辑 div),把引用做成带 <img> 的原子小卡片。项目里「组装提示词」组件(CanvasConfigComposer)已经是这套 contentEditable + 图片 chip 的成熟实现,可作蓝本。

关键约束(为什么不能直接改现有组件)

CanvasResourceMentionTextarea 被两处复用:

  1. 提示词面板 web/src/components/canvas/canvas-node-prompt-panel.tsx —— 本次要改的对象。highlightLabels 默认 true,value 是提示词字符串,引用以 label 文本「图片1」嵌在其中,直接发给 AI(配合 web/src/lib/image-reference-prompt.ts 的「参考图片编号:图片1」前缀)。
  2. 节点内文本编辑 web/src/components/canvas/canvas-node.tsxisEditingContent 分支)—— highlightLabels={false},通过 ref 调用 textarea.setSelectionRange(...)textarea 专有 API。若把组件改成 contentEditable,这处会直接崩。

结论:新建一个 contentEditable chip 输入组件,只替换提示词面板那一处CanvasResourceMentionTextarea(textarea 版)保持不动,继续服务节点内文本编辑。

方案:新增 CanvasPromptChipInput 组件

新文件 web/src/components/canvas/canvas-prompt-chip-input.tsx,参照 canvas-config-composer.tsx 的 contentEditable 模式,但有一处核心差异

  • 「组装提示词」把引用序列化成 @[node:UUID] 标记;
  • 本组件的 value 必须保持引用的 label 文本(如「图片1」),这样发给生成的提示词语义不变。因此 chip ↔ 文本的序列化用 label 文本,不是 node 标记。

组件契约(与现用法一致)

Props:value: stringreferences: CanvasResourceReference[]onChange(value)onSubmit?()classNamestyleplaceholder。图片缩略图取 reference.previewUrl(即 node.metadata.content,已存在于 CanvasResourceReference)。

实现要点(大量复用 composer 的做法)

  1. DOM ← value(未聚焦时重建):仿 CanvasConfigComposer 的重建 effect。用 active label 列表(references.filter(active).map(label),按长度降序)把 value 字符串切成「文本片段 + 命中 label」,命中处插入 chip。聚焦时不重建(document.activeElement === editor 直接 return),避免与输入 / IME 打架。
  2. chip 渲染:仿 createReferenceChip。image 类型 → size-6<img src={reference.previewUrl}>;其它类型 → 截断文字。chip 为 contentEditable="false"<span>,用 dataset.refLabel 存 label 以便序列化。
  3. value ← DOM(syncFromEditor):仿 serializeNodes,但 chip 序列化为 dataset.refLabel(label 文本)而非 node 标记;文本节点原样拼接,<br>\n
  4. @ 菜单:复用 textBeforeCaret + /@([^\s@]*)$/ 检测;菜单定位改用 getSelection().getRangeAt(0).getBoundingClientRect()(比 textarea 的镜像 div 更简单、天然贴着光标)。菜单 UI 沿用现有 MentionMenu 的视觉(缩略图 + label + 副标题)。
  5. 原子删除:直接复用 composer 的 deleteAdjacentReference / adjacentReferenceNode / findReferenceSibling——contentEditable + contentEditable="false" chip 天然整体删除。可把这几个通用 DOM 助手连同 textBeforeCaret / placeCaretAtEnd / closestEditor 复制进新组件(保持 composer 不动,规避回归风险)。
  6. 回车发送 + IME:复用 isPlainEnterKey / isImeComposing@/lib/keyboard-event)与 onCompositionStart/End,发送时调用 onSubmit
  7. 占位符 & 样式:空值时显示 placeholder(absolute 占位层);容器套用面板传入的 className/stylemin-h-40、透明底、无边框,与当前一致)。

替换点

canvas-node-prompt-panel.tsx 里把 <CanvasResourceMentionTextarea .../> 换成 <CanvasPromptChipInput .../>(同样的 value/references/onChange/onSubmit/className/style/placeholder)。其余(模型选择、设置、生成按钮)不动。

不改动

  • CanvasResourceMentionTextarea(textarea 版)保留,节点内文本编辑继续用它。
  • 生成链路、image-reference-prompt.ts 前缀逻辑不变(value 仍是 label 文本)。

影响文件

  • 新增:web/src/components/canvas/canvas-prompt-chip-input.tsx
  • 修改:web/src/components/canvas/canvas-node-prompt-panel.tsx(仅替换输入框组件 + 相应 import)

验证

  1. npx tsc --noEmit 通过。
  2. dev server 刷新后:
    • 连一张图片到图片 / 视频节点 → 面板输入框输入 @ → 菜单贴着光标弹出 → 选中 → 输入框内显示该图的真实缩略图 chip(不再是「图片1」文字)。
    • 光标在 chip 后按一次 Backspace → 整个 chip 一次删除。
    • 普通文字 + chip 混排,回车触发生成;确认发给生成的 prompt 里引用仍是「图片1」文本(生成结果正常引用到图)。
    • 中文输入法输入不吞字、不误触发发送。
    • 回归:双击文本节点进入内文本编辑仍正常(未受影响)。

On this page