ZCode 中的 Sources 组件:为 AI 对话响应构建可折叠引用来源展示
2026/9/23 7:24:55 网站建设 项目流程

【免费下载链接】ZCode

Z.ai's coding agent harness. Powerful, intelligent, extensible.

项目地址:https://gitcode.com/gh_mirrors/zco/ZCode
点击查看免费下载

导读

本文以 ZCode 仓库内置的ai-elements技能中Sources组件的完整参考文档(.agents/skills/ai-elements/references/sources.md)为核心,系统讲解如何在基于 AI SDK 的聊天应用中,为模型生成的回答展示来源与引用(citations)。文章将覆盖SourcesSourcesTriggerSourcesContentSource四个组件的安装方式、与 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@latestbunx --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.partstype === "source-url"的 part 逐一映射为Source链接,并用SourcesTriggercount属性展示来源总数;文本 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组件围绕"来源展示"场景提供了以下能力:

  • 可折叠组件:用户可按需展开/收起回答所使用的来源或引用列表;
  • 触发器与内容可定制:SourcesTriggerSourcesContent均可传入自定义子元素或样式;
  • 支持自定义来源:Source组件接受任意hreftitle,可渲染任意来源/引证链接;
  • 响应式设计:布局适配移动端,折叠动画与交互在窄屏下同样可用;
  • 简洁现代的样式:基于 Tailwind 与 shadcn/ui 主题体系,可通过className无缝接入既有主题。

自定义渲染

SourcesTriggerSource均支持 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" 文案与ChevronDownIconSource同样用{children ?? (...)}在无 children 时回退到BookIcon+title的默认布局。由此可推断:任何自定义内容都是通过 children 覆盖实现的,而组件自身仅负责折叠状态与基础 a 标签语义,互不干扰。

Props 参考

以下是四个组件的完整 Props 说明(与参考文档一致,并结合实现源码补充类型来源)。

<Sources />

Prop类型默认值说明
...propsReact.HTMLAttributes<HTMLDivElement>-其余所有 props 透传到根 div

实现中Sources实际渲染为 radix-ui 的Collapsible根组件(见 packages/ui/src/components/ui/collapsible.tsx),因此defaultOpenopenonOpenChange等折叠控制 props 同样可用。

<SourcesTrigger />

Prop类型默认值说明
countnumber必填触发器上展示的来源数量
...propsReact.ComponentProps<typeof CollapsibleTrigger>-其余 props 透传到 CollapsibleTrigger 组件

<SourcesContent />

Prop类型默认值说明
...propsReact.HTMLAttributes<HTMLDivElement>-其余 props 透传到内容容器

实现中SourcesContent渲染为CollapsibleContent,内置了data-[state]驱动的展开/收起动画类(fade-out、slide-in-from-top 等),实际生效样式可参考 packages/ui/src/components/ai-elements/sources.tsx。

<Source />

Prop类型默认值说明
...propsReact.AnchorHTMLAttributes<HTMLAnchorElement>-其余 props 透传到 a 元素

Source即一个<a>锚点元素,实现中默认带有target="_blank"rel="noreferrer",保证来源在新标签页打开且不泄露来源页信息;hreftitle等标准链接属性均可用。

主题与样式注意事项

Sources组件继承 shadcn/ui 的>

【免费下载链接】ZCode

Z.ai's coding agent harness. Powerful, intelligent, extensible.

项目地址:https://gitcode.com/gh_mirrors/zco/ZCode
点击查看免费下载

相关推荐

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

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

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

立即咨询