☰
ZCode 中的 PromptInput 组件指南:构建支持附件、模型选择与流式状态的 AI 对话输入框
2026/10/1 7:54:48 网站建设 项目流程
  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载

导读

PromptInput是 ZCode 仓库内置的 AI Elements 组件体系中用于消息输入的核心组件,它把「文本输入、文件上传、截图抓取、模型选择、提交按钮」整合进一个自管理的表单容器,可直接与 AI SDK 的useChat配合构建完整聊天应用。本文将完整讲解PromptInput的安装方式、AI SDK 前后端集成示例、全部子组件与 Props、四个 Hooks 的用法,并结合仓库内的真实源码实现(packages/ui/src/components/ai-elements/prompt-input.tsx)说明其底层工作原理,帮助你在 ZCode 及其衍生项目中快速落地一个功能完整的 AI 对话输入框。

PromptInput 是什么

PromptInput允许用户向大语言模型发送带有文件附件(attachments)的消息。它由一个 textarea、文件上传能力、一个提交按钮,以及一个用于选择模型的 dropdown 组合而成。在 AI Elements 技能文档 中,PromptInput被定位为构建 AI 聊天界面(chatbot、AI 助手 UI)时的核心输入组件,常与Conversation、Message等组件搭配使用。

在仓库中,PromptInput的完整 React 实现位于 packages/ui/src/components/ai-elements/prompt-input.tsx(共 753 行),并拆分出若干辅助模块:

  • packages/ui/src/components/ai-elements/prompt-input-primitives.tsx:基础原语
  • packages/ui/src/components/ai-elements/prompt-input-textarea.tsx:textarea 实现
  • packages/ui/src/components/ai-elements/prompt-input-buttons.tsx:按钮实现
  • packages/ui/src/components/ai-elements/prompt-input-actions.tsx:动作菜单实现

从源码结构可以推断,PromptInput通过createContext提供PromptInputController与ProviderAttachmentsContext两个上下文,把文本输入与附件状态提升为可共享的全局状态,任何包裹在PromptInputProvider内的组件都能读写输入状态(详见下文「Hooks」一节)。附件(Attachment、AttachmentPreview、AttachmentRemove、Attachments)则被独立拆分到单独模块,见 附件组件文档。

安装

与 AI Elements 的其他组件一致,PromptInput通过 CLI 以源码形式(非隐藏库)安装到你的项目组件目录(默认@/components/ai-elements/):

npx ai-elements@latest add prompt-input

注意:应根据项目声明的packageManager使用对应的包管理器运行器,例如pnpm dlx ai-elements@latest或bunx --bun ai-elements@latest。如果项目尚未安装 shadcn/ui,安装命令会自动将其一并安装。

安装完成后,组件代码会落到项目的components/ai-elements/prompt-input.tsx等文件中,你可以像对待自己写的代码一样直接打开修改样式与逻辑(这正是 AI Elements 的设计理念:组件即源码,可任意定制)。

与 AI SDK 集成:构建完整聊天应用

文档给出了一个端到端示例:用PromptInput+Conversation+ 模型选择器,搭建一个支持附件、Web 搜索开关、流式响应的聊天界面。

前端组件(app/page.tsx)

"use client"; import { Attachment, AttachmentPreview, AttachmentRemove, Attachments, } from "@/components/ai-elements/attachments"; import { PromptInput, PromptInputActionAddAttachments, PromptInputActionAddScreenshot, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuTrigger, PromptInputBody, PromptInputButton, PromptInputHeader, type PromptInputMessage, PromptInputSelect, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectTrigger, PromptInputSelectValue, PromptInputSubmit, PromptInputTextarea, PromptInputFooter, PromptInputTools, usePromptInputAttachments, } from "@/components/ai-elements/prompt-input"; import { GlobeIcon } from "lucide-react"; import { useState } from "react"; import { useChat } from "@ai-sdk/react"; import { Conversation, ConversationContent, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; const PromptInputAttachmentsDisplay = () => { const attachments = usePromptInputAttachments(); if (attachments.files.length === 0) { return null; } return ( <Attachments variant="inline"> {attachments.files.map((attachment) => ( <Attachment data={attachment} key={attachment.id} onRemove={() => attachments.remove(attachment.id)} > <AttachmentPreview /> <AttachmentRemove /> </Attachment> ))} </Attachments> ); }; const models = [ { id: "gpt-4o", name: "GPT-4o" }, { id: "claude-opus-4-20250514", name: "Claude 4 Opus" }, ]; const InputDemo = () => { const [text, setText] = useState<string>(""); const [model, setModel] = useState<string>(models[0].id); const [useWebSearch, setUseWebSearch] = useState<boolean>(false); const { messages, status, sendMessage } = useChat(); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); if (!(hasText || hasAttachments)) { return; } sendMessage( { text: message.text || "Sent with attachments", files: message.files, }, { body: { model: model, webSearch: useWebSearch, }, }, ); setText(""); }; return ( <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"> <div className="flex flex-col h-full"> <Conversation> <ConversationContent> {messages.map((message) => ( <Message from={message.role} key={message.id}> <MessageContent> {message.parts.map((part, i) => { switch (part.type) { case "text": return ( <MessageResponse key={`${message.id}-${i}`}>{part.text}</MessageResponse> ); default: return null; } })} </MessageContent> </Message> ))} </ConversationContent> <ConversationScrollButton /> </Conversation> <PromptInput onSubmit={handleSubmit} className="mt-4" globalDrop multiple> <PromptInputHeader> <PromptInputAttachmentsDisplay /> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea onChange={(e) => setText(e.target.value)} value={text} /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <PromptInputActionMenu> <PromptInputActionMenuTrigger /> <PromptInputActionMenuContent> <PromptInputActionAddAttachments /> <PromptInputActionAddScreenshot /> </PromptInputActionMenuContent> </PromptInputActionMenu> <PromptInputButton onClick={() => setUseWebSearch(!useWebSearch)} tooltip={{ content: "Search the web", shortcut: "⌘K" }} variant={useWebSearch ? "default" : "ghost"} > <GlobeIcon size={16} /> <span>Search</span> </PromptInputButton> <PromptInputSelect onValueChange={(value) => { setModel(value); }} value={model} > <PromptInputSelectTrigger> <PromptInputSelectValue /> </PromptInputSelectTrigger> <PromptInputSelectContent> {models.map((model) => ( <PromptInputSelectItem key={model.id} value={model.id}> {model.name} </PromptInputSelectItem> ))} </PromptInputSelectContent> </PromptInputSelect> </PromptInputTools> <PromptInputSubmit disabled={!text && !status} status={status} /> </PromptInputFooter> </PromptInput> </div> </div> ); }; export default InputDemo;

示例中的关键模式:

  • 组合式结构:PromptInput按Header(附件展示区)→Body(textarea)→Footer(工具栏 + 提交按钮)三明治式组织子组件,层级清晰。
  • 附件读取:usePromptInputAttachments()从上下文读取当前附件列表,为空时直接渲染null,避免出现空附件条。
  • 模型选择:PromptInputSelect系列(Trigger/Value/Content/Item)是内置于输入框的工具下拉,切换时更新本地model状态,并在提交时通过body透传给后端。
  • 提交语义:onSubmit接收的PromptInputMessage统一携带text与files;空消息(既无文本也无附件)会被直接拦截。
  • 状态驱动提交按钮:PromptInputSubmit接收status(来自useChat),按钮图标会随submitted / streaming / error等状态自动切换。

后端路由(app/api/chat/route.ts)

import { streamText, UIMessage, convertToModelMessages } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { model, messages, webSearch, }: { messages: UIMessage[]; model: string; webSearch?: boolean; } = await req.json(); const result = streamText({ model: webSearch ? "perplexity/sonar" : model, messages: await convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); }

后端要点:convertToModelMessages把前端 UI 消息(含附件 part)转换为模型消息格式;webSearch为真时切换为perplexity/sonar模型,从而把「Web 搜索开关」下沉到服务端路由;maxDuration = 30允许流式响应最长 30 秒。前端示例代码可在仓库中直接查看:scripts/prompt-input.tsx。

功能特性一览

PromptInput内置的完整能力清单如下:

  • 自动调整高度的 textarea(随内容增长)
  • 支持拖拽的文件附件上传
  • 内置截图抓取动作(screenshot capture)
  • 图片附件的预览能力
  • 可配置的文件约束(最大文件数、最大大小、可接受类型)
  • 根据状态自动切换图标的提交按钮
  • 键盘快捷键支持(Enter 提交、Shift+Enter 换行)
  • textarea 最小/最大高度可自定义
  • 灵活工具栏,支持自定义动作与工具
  • 内置模型选择下拉
  • 内置原生语音识别按钮(Web Speech API)
  • 可选 Provider,用于把状态提升到组件外部管理
  • 表单提交后自动重置
  • 移动端友好的响应式设计
  • 干净、现代、主题可自定义的样式
  • 基于表单的提交处理
  • 隐藏文件输入同步(hidden file input sync),支持原生表单提交
  • 全局文档拖放支持(opt-in,通过globalDrop开启)

其中「隐藏文件输入同步」对应<PromptInput />的syncHiddenInputprop:渲染一个带指定name的隐藏<input>,使PromptInput可以被嵌入传统 HTML 表单进行原生 post;「全局拖放」对应globalDrop,开启后在整个 document 上接受文件拖放,而不局限于输入框区域。

进阶示例

Cursor 风格(scripts/prompt-input-cursor.tsx)

仓库提供了一个仿 Cursor 的复杂输入栏示例,见 scripts/prompt-input-cursor.tsx。它展示了PromptInput的高度可扩展性:

  • 用PromptInputHoverCard/Trigger/Content实现「@ 引用文件、项目规则、标签页」等悬停面板;
  • 用PromptInputCommand系列实现「Add files, folders, docs...」的斜杠命令式文件选择;
  • 用PromptInputTab系列展示 Active Tabs / Recents 两个标签组;
  • 用usePromptInputReferencedSources()展示引用来源(source documents)的添加与移除;
  • 用ModelSelector(来自 模型选择器文档)替代简单的PromptInputSelect,提供带厂商 Logo、可搜索的模型选择。

按钮 Tooltip(scripts/prompt-input-tooltip.tsx)

工具栏按钮可显示带可选键盘快捷键提示的 tooltip,见 scripts/prompt-input-tooltip.tsx:

// 简单字符串 tooltip <PromptInputButton tooltip="Attach files"> <PaperclipIcon size={16} /> </PromptInputButton> // 带键盘快捷键提示的 tooltip <PromptInputButton tooltip={{ content: "Search the web", shortcut: "⌘K" }}> <GlobeIcon size={16} /> </PromptInputButton> // 自定义弹出位置 <PromptInputButton tooltip={{ content: "Voice input", shortcut: "⌘M", side: "bottom" }}> <MicIcon size={16} /> </PromptInputButton>

tooltip支持字符串与对象两种形态,对象可包含content(内容)、shortcut(快捷键提示文本)与side(弹出方向)。完整的 tooltip 示例代码见上文 PromptInputButton Props 小节下方的演示。

组件与 Props 完整参考

<PromptInput />

表单容器根组件,负责管理 textarea、附件、文件校验与提交。

Prop类型默认值说明
onSubmit(message: PromptInputMessage, event: FormEvent) => void-表单提交时回调,携带消息文本与文件
acceptstring-可接受的文件类型(如image/*)
multipleboolean-是否允许多文件选择
globalDropboolean-为 true 时接受 document 任意位置的文件拖放
syncHiddenInputboolean-渲染带指定 name 的隐藏 input,用于原生表单提交
maxFilesnumber-允许的最大文件数
maxFileSizenumber-最大文件大小(字节)
onError(err: { code: ... }) => void-文件校验错误处理器
...propsReact.HTMLAttributes<HTMLFormElement>-其余 props 透传到根 form 元素

从 源码实现 可以看到,附件经convertBlobUrlToDataUrl转换为 Data URL 后再进入提交链路;文件校验错误(数量、大小、类型)由onError统一上报,供上层做 toast 等提示。

<PromptInputTextarea />

<PromptInputTextarea placeholder="Plan, search, build anything" />
Prop类型默认值说明
...propsReact.ComponentProps<typeof Textarea>-其余 props 透传到底层 Textarea 组件

textarea 实现见 prompt-input-textarea.tsx,支持内容驱动的自动高度调整、最小/最大高度约束,以及 Enter 提交、Shift+Enter 换行的快捷键逻辑。

布局容器:Header/Body/Footer/Tools

  • <PromptInputHeader />:|...props|Omit<React.ComponentProps<typeof InputGroupAddon>, "align">| - | 其余 props(除 align)透传到 InputGroupAddon 组件 |。用于放置附件展示、悬停面板等头部内容。
  • <PromptInputBody />:|...props|React.HTMLAttributes<HTMLDivElement>| - | 其余 props 透传到 body div |。包裹 textarea。
  • <PromptInputFooter />:|...props|React.HTMLAttributes<HTMLDivElement>| - | 其余 props 透传到工具栏 div |。承载工具栏与提交按钮。
  • <PromptInputTools />:|...props|React.HTMLAttributes<HTMLDivElement>| - | 其余 props 透传到 tools div |。工具栏按钮组容器。

<PromptInputButton />

Prop类型默认值说明
tooltipstring \| { content: ReactNode; shortcut?: string; side?: ... }-悬停 tooltip,可为字符串或含 content/shortcut/side 的对象
...propsReact.ComponentProps<typeof Button>-其余 props 透传到底层 shadcn/ui Button
Tooltip 示例
// 简单字符串 tooltip <PromptInputButton tooltip="Search the web"> <GlobeIcon size={16} /> </PromptInputButton> // 带键盘快捷键提示 <PromptInputButton tooltip={{ content: "Search", shortcut: "⌘K" }}> <GlobeIcon size={16} /> </PromptInputButton> // 自定义位置 <PromptInputButton tooltip={{ content: "Search", side: "bottom" }}> <GlobeIcon size={16} /> </PromptInputButton>

<PromptInputSubmit />

Prop类型默认值说明
statusChatStatus-当前聊天状态,决定按钮图标(submitted、streaming、error 等)
...propsReact.ComponentProps<typeof Button>-其余 props 透传到底层 shadcn/ui Button

模型选择:Select系列

组件透传的底层组件说明
<PromptInputSelect />Select模型选择下拉根组件
<PromptInputSelectTrigger />SelectTrigger触发按钮
<PromptInputSelectContent />SelectContent下拉内容面板
<PromptInputSelectItem />SelectItem单个模型项(value为模型 id)
<PromptInputSelectValue />SelectValue当前选中值展示

动作菜单:ActionMenu系列

组件透传的底层组件说明
<PromptInputActionMenu />DropdownMenu动作菜单根
<PromptInputActionMenuTrigger />Button菜单触发按钮(如「+」)
<PromptInputActionMenuContent />DropdownMenuContent菜单内容面板
<PromptInputActionMenuItem />DropdownMenuItem菜单项
<PromptInputActionAddAttachments />DropdownMenuItem,另有label?: string添加附件的菜单项
<PromptInputActionAddScreenshot />DropdownMenuItem,另有label?: string截图抓取的菜单项

悬停面板:HoverCard系列

组件Prop说明
<PromptInputHoverCard />openDelay: number(默认0)、closeDelay: number(默认0),其余透传HoverCard悬停卡片容器,毫秒级开关延迟
<PromptInputHoverCardTrigger />透传HoverCardTrigger触发元素
<PromptInputHoverCardContent />align: unknown,其余透传HoverCardContent卡片内容

标签组:Tab系列

组件透传目标说明
<PromptInputTabsList />div标签列表容器
<PromptInputTab />div单个标签页
<PromptInputTabLabel />h3标签标题(如 "Active Tabs")
<PromptInputTabBody />div标签内容主体
<PromptInputTabItem />div标签内条目(如文件路径行)

命令面板:Command系列

组件透传目标说明
<PromptInputCommand />Command命令面板根(用于文件/文档搜索)
<PromptInputCommandInput />CommandInput命令搜索输入框
<PromptInputCommandList />CommandList命令结果列表
<PromptInputCommandEmpty />CommandEmpty空结果提示
<PromptInputCommandGroup />CommandGroup结果分组(如 "Added"、"Other Files")
<PromptInputCommandItem />CommandItem单个结果项
<PromptInputCommandSeparator />CommandSeparator分组分隔线

<PromptInputProvider />

Prop类型默认值说明
initialInputstring-初始文本输入值
childrenReact.ReactNode-可访问 Provider 上下文的子组件

可选的全局 Provider,把 PromptInput 的状态提升到组件外部。使用后,你可以在 Provider 树内的任意位置访问并控制输入状态(文本与附件);不使用它时,PromptInput完全自管理。这一设计与源码中的PromptInputController/ProviderAttachmentsContext上下文一一对应(见 prompt-input.tsx),同时支持通过__registerFileInput把隐藏文件输入注册到 Provider 以便外部触发文件对话框。

Hooks

usePromptInputAttachments

在 PromptInput 上下文中访问和管理文件附件:

const attachments = usePromptInputAttachments(); // 可用方法: attachments.files; // 当前附件数组(FileUIPart & { id })[] attachments.add(files); // 添加新文件(File[] 或 FileList) attachments.remove(id); // 按 ID 移除附件 attachments.clear(); // 清空全部附件 attachments.openFileDialog(); // 打开文件选择对话框

源码中AttachmentsContext的完整类型定义(含fileInputRef)可在 prompt-input.tsx 查看。

usePromptInputController

从PromptInputProvider获取完整的 PromptInput 控制器(仅在使用了 Provider 时可用):

const controller = usePromptInputController(); // 可用方法: controller.textInput.value; // 当前文本输入值 controller.textInput.setInput(v); // 设置文本输入值 controller.textInput.clear(); // 清空文本输入 controller.attachments; // 与 usePromptInputAttachments 相同

若在未包裹PromptInputProvider时调用,源码会抛出明确的错误提示:"Wrap your component inside <PromptInputProvider> to use usePromptInputController()."。

useProviderAttachments

从PromptInputProvider访问附件上下文(仅在使用了 Provider 时可用):

const attachments = useProviderAttachments(); // 接口与 usePromptInputAttachments 完全一致

usePromptInputReferencedSources

访问 PromptInput 内的引用来源(referenced sources)上下文——即用户在输入框中引用的文档/文件来源,可用于「@ 引用文件」类交互:

const sources = usePromptInputReferencedSources(); // 可用方法: sources.sources; // 当前引用来源数组(SourceDocumentUIPart 等) sources.add(sources); // 添加新来源 sources.remove(id); // 按 ID 移除来源 sources.clear(); // 清空全部来源

该 Hook 在 Cursor 风格示例 中与PromptInputCommand面板配合,实现了「Add files, folders, docs...」的引用来源管理与去重过滤(通过title + filename判断是否已添加)。

关联模块与延伸阅读

  • 附件展示组件(Attachments/Attachment/AttachmentPreview/AttachmentInfo/AttachmentRemove):见 附件组件文档,支持 grid / inline / list 三种布局,以及getMediaCategory、getAttachmentLabel两个工具函数。
  • 会话容器组件(Conversation):见 会话组件文档,负责消息包裹与自动滚动到底部。
  • 消息展示组件(Message):见 消息组件文档,配合MessageContent/MessageResponse渲染文本与各类型 part。
  • 模型选择器(ModelSelector):见 模型选择器文档,提供带厂商 Logo、可搜索的模型选择体验。
  • 组件库总览与排障:见 SKILL.md,包含组件未安装、主题切换失败、@/路径别名缺失等常见问题的排查步骤。

注意事项与前提

  • PromptInput组件及示例代码在仓库中源自 vercel/ai-elements,遵循 Apache-2.0 许可,并在仓库根目录的 THIRD-PARTY-NOTICES.md 中注明来源与许可信息。
  • 文中scripts/下的.tsx示例是技能目录中的组件示例文件(相对技能目录),而非仓库根目录的脚本,它们用于演示组件用法,并不是已安装的应用功能。
  • 使用PromptInput需要 Node.js 18+、Next.js 项目且已安装 AI SDK;若项目已使用 shadcn/ui,安装会更顺畅。
  • 上述 props 表格中的默认值(如openDelay、closeDelay为0)以仓库内文档为准;实际渲染效果请以安装后组件源码为准,因为 AI Elements 的组件以源码形式进入你的项目,你可以直接查看与修改。
  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询