- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
导读
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 | - | 表单提交时回调,携带消息文本与文件 |
accept | string | - | 可接受的文件类型(如image/*) |
multiple | boolean | - | 是否允许多文件选择 |
globalDrop | boolean | - | 为 true 时接受 document 任意位置的文件拖放 |
syncHiddenInput | boolean | - | 渲染带指定 name 的隐藏 input,用于原生表单提交 |
maxFiles | number | - | 允许的最大文件数 |
maxFileSize | number | - | 最大文件大小(字节) |
onError | (err: { code: ... }) => void | - | 文件校验错误处理器 |
...props | React.HTMLAttributes<HTMLFormElement> | - | 其余 props 透传到根 form 元素 |
从 源码实现 可以看到,附件经convertBlobUrlToDataUrl转换为 Data URL 后再进入提交链路;文件校验错误(数量、大小、类型)由onError统一上报,供上层做 toast 等提示。
<PromptInputTextarea />
<PromptInputTextarea placeholder="Plan, search, build anything" />| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
...props | React.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 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tooltip | string \| { content: ReactNode; shortcut?: string; side?: ... } | - | 悬停 tooltip,可为字符串或含 content/shortcut/side 的对象 |
...props | React.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 | 类型 | 默认值 | 说明 |
|---|---|---|---|
status | ChatStatus | - | 当前聊天状态,决定按钮图标(submitted、streaming、error 等) |
...props | React.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 | 类型 | 默认值 | 说明 |
|---|---|---|---|
initialInput | string | - | 初始文本输入值 |
children | React.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 与运行时源码。
相关推荐
Comp AI CRM 前端工程中的 ai-elements PromptInput 组件:构建带附件、模型选择与语音输入的 AI 聊天输入框
Comp AI CRM 前端工程中的 ai elements PromptInput 组件:构建带附件、模型选择与语音输入的 AI 聊天输入框 PromptIn
后端前端CRM人工智能AI AgentZCode 中的 PromptInput 组件:基于 ai-elements 构建 AI 聊天输入框的完整指南
ZCode 中的 PromptInput 组件:基于 ai elements 构建 AI 聊天输入框的完整指南 PromptInput 是 ai element
在 ZCode 中构建 AI 模型选择器:ModelSelector 命令面板组件完全指南
在 ZCode 中构建 AI 模型选择器:ModelSelector 命令面板组件完全指南 ModelSelector 是一个基于 cmdk 构建的可搜索 AI
人工智能大模型代码智能体AI Agent桌面应用后端前端CLI插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考