【免费下载链接】ZCode
Z.ai's coding agent harness. Powerful, intelligent, extensible.
导读
本文以 ZCode 仓库内置的ai-elements技能中Sources组件的完整参考文档(.agents/skills/ai-elements/references/sources.md)为核心,系统讲解如何在基于 AI SDK 的聊天应用中,为模型生成的回答展示来源与引用(citations)。文章将覆盖Sources、SourcesTrigger、SourcesContent、Source四个组件的安装方式、与 Perplexity 等搜索型模型的端到端接入示例、全部 Props 参数表,并结合 ZCode 仓库中的实际实现源码(packages/ui/src/components/ai-elements/sources.tsx)深入剖析其折叠交互原理与自定义渲染方法,帮助你快速在自己的 AI 前端中落地"引用来源"这一关键可信度功能。
什么是 Sources 组件
Sources是一个允许用户查看生成回答时所使用来源或引用的 UI 组件。在基于检索增强或联网搜索的 AI 应用中,模型会引用多个网页、文档作为回答依据,Sources组件将这些来源以可折叠(collapsible)的形式组织起来:默认只显示一个触发按钮(例如"Used N sources"),用户点击后展开来源列表,每条来源以带图标的超链接形式展示。
从 ZCode 仓库的实现看(packages/ui/src/components/ai-elements/sources.tsx),该组件由四个协作的部分组成,底层依赖 radix-ui 的Collapsible原语(见 packages/ui/src/components/ui/collapsible.tsx):
| 组件 | 职责 |
|---|---|
Sources | 折叠容器根节点,内部渲染Collapsible,持有展开/收起状态 |
SourcesTrigger | 折叠触发器,显示来源数量(默认文案"Used N sources")与箭头图标 |
SourcesContent | 折叠内容区,承载来源列表,带展开/收起动画 |
Source | 单个来源链接,默认渲染书本图标 + 标题,新标签页打开 |
四个组件均支持通过className与展开的 props 深度定制,这与该技能目录(.agents/skills/ai-elements/SKILL.md)所倡导的"尽可能透传原生属性"的扩展性原则一致。
安装
在具备以下前置条件的项目中,通过 AI Elements CLI 即可一键安装Sources组件:
- Node.js 18 及以上版本;
- 已安装 AI SDK 的 Next.js 项目;
- 已安装 shadcn/ui(未安装时,执行安装命令会自动补装)。
安装命令:
npx ai-elements@latest add sources如果你的项目使用 pnpm 或 bun 作为包管理器,请使用对应的运行器:pnpm dlx ai-elements@latest或bunx --bun ai-elements@latest。CLI 会将组件代码(及其依赖)写入你项目中 shadcn 配置的组件目录,默认位置是@/components/ai-elements/,因此安装完成后,代码中引入路径即为:
import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai-elements/sources";组件代码会作为你项目源码的一部分落地(而非封装在不可见的库里),这意味你可以直接打开组件文件查看实现、按需修改样式与逻辑。若引入时报"module not found",请检查tsconfig.json中是否配置了@/路径别名:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./*"] } } }与 AI SDK 集成:构建一个联网搜索问答示例
参考文档给出了一个完整的实战示例(对应的示例源码位于 .agents/skills/ai-elements/scripts/sources.tsx):使用 Perplexity Sonar 模型构建一个简单的网页搜索 Agent,前端展示每次回复引用的来源。
前端组件(app/page.tsx)
"use client"; import { useChat } from "@ai-sdk/react"; import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai-elements/sources"; import { PromptInput, type PromptInputMessage, PromptInputTextarea, PromptInputSubmit, } from "@/components/ai-elements/prompt-input"; import { Conversation, ConversationContent, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; import { useState } from "react"; import { DefaultChatTransport } from "ai"; const SourceDemo = () => { const [input, setInput] = useState(""); const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: "/api/sources", }), }); const handleSubmit = (message: PromptInputMessage) => { if (message.text.trim()) { sendMessage({ text: message.text }); setInput(""); } }; 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"> <div className="flex-1 overflow-auto mb-4"> <Conversation> <ConversationContent> {messages.map((message) => ( <div key={message.id}> {message.role === "assistant" && ( <Sources> <SourcesTrigger count={message.parts.filter((part) => part.type === "source-url").length} /> {message.parts.map((part, i) => { switch (part.type) { case "source-url": return ( <SourcesContent key={`${message.id}-${i}`}> <Source key={`${message.id}-${i}`} href={part.url} title={part.url} /> </SourcesContent> ); } })} </Sources> )} <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> </div> ))} </ConversationContent> <ConversationScrollButton /> </Conversation> </div> <PromptInput onSubmit={handleSubmit} className="mt-4 w-full max-w-2xl mx-auto relative"> <PromptInputTextarea value={input} placeholder="Ask a question and search the..." onChange={(e) => setInput(e.currentTarget.value)} className="pr-12" /> <PromptInputSubmit status={status === "streaming" ? "streaming" : "ready"} disabled={!input.trim()} className="absolute bottom-1 right-1" /> </PromptInput> </div> </div> ); }; export default SourceDemo;这段代码的关键逻辑在于:通过useChat拿到流式消息后,对message.parts中type === "source-url"的 part 逐一映射为Source链接,并用SourcesTrigger的count属性展示来源总数;文本 part 则交给MessageResponse渲染。折叠容器Sources仅包裹 assistant 消息,用户消息不展示来源。
后端路由(api/chat/route.ts)
import { convertToModelMessages, streamText, UIMessage } from "ai"; import { perplexity } from "@ai-sdk/perplexity"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: "perplexity/sonar", system: "You are a helpful assistant. Keep your responses short (< 100 words) unless you are asked for more details. ALWAYS USE SEARCH.", messages: await convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); }后端的关键配置是toUIMessageStreamResponse({ sendSources: true }):开启后,AI SDK 会把模型返回的引用来源编码为source-url类型的 UI message part,前端据此驱动Sources组件渲染。系统提示词中的 "ALWAYS USE SEARCH" 用于引导 Sonar 模型始终执行搜索,从而稳定地产出可展示的来源数据。maxDuration = 30允许流式响应最长 30 秒。
功能特性
Sources组件围绕"来源展示"场景提供了以下能力:
- 可折叠组件:用户可按需展开/收起回答所使用的来源或引用列表;
- 触发器与内容可定制:
SourcesTrigger与SourcesContent均可传入自定义子元素或样式; - 支持自定义来源:
Source组件接受任意href与title,可渲染任意来源/引证链接; - 响应式设计:布局适配移动端,折叠动画与交互在窄屏下同样可用;
- 简洁现代的样式:基于 Tailwind 与 shadcn/ui 主题体系,可通过
className无缝接入既有主题。
自定义渲染
SourcesTrigger与Source均支持 children 覆盖默认内容,这为深度定制提供了入口。参考文档提供了自定义示例(对应源码 .agents/skills/ai-elements/scripts/sources-custom.tsx):
"use client"; import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai-elements/sources"; import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react"; const sources = [ { href: "https://stripe.com/docs/api", title: "Stripe API Documentation" }, { href: "https://docs.github.com/en/rest", title: "GitHub REST API" }, { href: "https://docs.aws.amazon.com/sdk-for-javascript/", title: "AWS SDK for JavaScript", }, ]; const Example = () => ( <div style={{ height: "110px" }}> <Sources> <SourcesTrigger count={sources.length}> <p className="font-medium">Using {sources.length} citations</p> <ChevronDownIcon className="size-4" /> </SourcesTrigger> <SourcesContent> {sources.map((source) => ( <Source href={source.href} key={source.href}> {source.title} <ExternalLinkIcon className="size-4" /> </Source> ))} </SourcesContent> </Sources> </div> ); export default Example;与默认渲染相比,自定义示例做了两处增强:一是触发器文案改为 "Using N citations" 并显式放置下箭头图标;二是每条来源追加ExternalLinkIcon外链图标,语义上提示用户点击后将离开当前页面。这正是该组件"可像自己写的代码一样自由修改"的体现。
结合源码看自定义的实现原理
在 ZCode 的组件实现中(packages/ui/src/components/ai-elements/sources.tsx),SourcesTrigger使用{children ?? (...)}的写法:传入 children 时完全渲染自定义内容,未传入时才回退到默认的 "Used {count} sources" 文案与ChevronDownIcon;Source同样用{children ?? (...)}在无 children 时回退到BookIcon+title的默认布局。由此可推断:任何自定义内容都是通过 children 覆盖实现的,而组件自身仅负责折叠状态与基础 a 标签语义,互不干扰。
Props 参考
以下是四个组件的完整 Props 说明(与参考文档一致,并结合实现源码补充类型来源)。
<Sources />
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
...props | React.HTMLAttributes<HTMLDivElement> | - | 其余所有 props 透传到根 div |
实现中Sources实际渲染为 radix-ui 的Collapsible根组件(见 packages/ui/src/components/ui/collapsible.tsx),因此defaultOpen、open、onOpenChange等折叠控制 props 同样可用。
<SourcesTrigger />
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
count | number | 必填 | 触发器上展示的来源数量 |
...props | React.ComponentProps<typeof CollapsibleTrigger> | - | 其余 props 透传到 CollapsibleTrigger 组件 |
<SourcesContent />
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
...props | React.HTMLAttributes<HTMLDivElement> | - | 其余 props 透传到内容容器 |
实现中SourcesContent渲染为CollapsibleContent,内置了data-[state]驱动的展开/收起动画类(fade-out、slide-in-from-top 等),实际生效样式可参考 packages/ui/src/components/ai-elements/sources.tsx。
<Source />
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
...props | React.AnchorHTMLAttributes<HTMLAnchorElement> | - | 其余 props 透传到 a 元素 |
Source即一个<a>锚点元素,实现中默认带有target="_blank"与rel="noreferrer",保证来源在新标签页打开且不泄露来源页信息;href、title等标准链接属性均可用。
主题与样式注意事项
Sources组件继承 shadcn/ui 的>赞
【免费下载链接】ZCode
Z.ai's coding agent harness. Powerful, intelligent, extensible.
相关推荐
如何用Ventoy打造终极多系统启动U盘:告别反复格式化,一U盘装遍所有操作系统
如何用Ventoy打造终极多系统启动U盘:告别反复格式化,一U盘装遍所有操作系统 还在为每次重装系统都要重新制作启动盘而烦恼吗?还在因为U盘只能存放一个系统镜像
操作系统固件开发工具marimo Accordion 组件详解:用 `mo.accordion` 构建可折叠内容区
marimo Accordion 组件详解:用 mo.accordion 构建可折叠内容区 导读 本文围绕 marimo 的 mo.accordion 布局组件
数据科学前端后端AI 应用推荐开源项目:React响应式折叠组件(React Collapsible)
推荐开源项目:React响应式折叠组件(React Collapsible) 在构建动态和交互式的网页应用时,处理大量信息展示的高效性和条理性至关重要。今天,我