Sources
Collapsible list of the sources a response used.
import { Source, Sources, SourcesContent, SourcesTrigger,} from "@/components/ai/sources";
const sources = [ { title: "Nvidia Q3 results", href: "https://www.reuters.com/technology/nvidia-q3", }, { title: "Form 10-Q", href: "https://www.sec.gov/cgi-bin/browse-edgar" }, { title: "NVDA quote", href: "https://www.nasdaq.com/market-activity/stocks/nvda", },];
export default function Demo() { return ( <div className="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger count={sources.length} /> <SourcesContent> {sources.map((s) => ( <Source key={s.href} href={s.href} title={s.title} /> ))} </SourcesContent> </Sources> <Sources defaultOpen> <SourcesTrigger count={sources.length} /> <SourcesContent> {sources.map((s) => ( <Source key={s.href} href={s.href} title={s.title} /> ))} </SourcesContent> </Sources> </div> );}<script setup lang="ts">import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai/sources";
const sources = [ { title: "Nvidia Q3 results", href: "https://www.reuters.com/technology/nvidia-q3" }, { title: "Form 10-Q", href: "https://www.sec.gov/cgi-bin/browse-edgar" }, { title: "NVDA quote", href: "https://www.nasdaq.com/market-activity/stocks/nvda" },];</script>
<template> <div class="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger :count="sources.length" /> <SourcesContent> <Source v-for="s in sources" :key="s.href" :href="s.href" :title="s.title" /> </SourcesContent> </Sources> <Sources default-open> <SourcesTrigger :count="sources.length" /> <SourcesContent> <Source v-for="s in sources" :key="s.href" :href="s.href" :title="s.title" /> </SourcesContent> </Sources> </div></template><script lang="ts"> import { Source, Sources, SourcesContent, SourcesTrigger } from "$lib/components/ai/sources";
const sources = [ { title: "Nvidia Q3 results", href: "https://www.reuters.com/technology/nvidia-q3" }, { title: "Form 10-Q", href: "https://www.sec.gov/cgi-bin/browse-edgar" }, { title: "NVDA quote", href: "https://www.nasdaq.com/market-activity/stocks/nvda" }, ];</script>
<div class="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger count={sources.length} /> <SourcesContent> {#each sources as s (s.href)} <Source href={s.href} title={s.title} /> {/each} </SourcesContent> </Sources> <Sources open> <SourcesTrigger count={sources.length} /> <SourcesContent> {#each sources as s (s.href)} <Source href={s.href} title={s.title} /> {/each} </SourcesContent> </Sources></div>Built on collapsible.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-sourcespnpm dlx shadcn@latest add @edmi-ui/ai-sourcesyarn dlx shadcn@latest add @edmi-ui/ai-sourcesbunx shadcn@latest add @edmi-ui/ai-sourcesPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-sourcespnpm dlx shadcn-vue@latest add @edmi-ui/ai-sourcesyarn dlx shadcn-vue@latest add @edmi-ui/ai-sourcesbunx shadcn-vue@latest add @edmi-ui/ai-sourcesPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue reka-uiTerminal window pnpm add @lucide/vue reka-uiTerminal window yarn add @lucide/vue reka-uiTerminal window bun add @lucide/vue reka-uiAdd the Edmi components this one builds on:
collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sources.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sources.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sources.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-sources.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Sources, SourcesContent, SourcesTrigger,} from "@/components/ai/sources";
<div className="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger /> <SourcesContent> </SourcesContent> </Sources> <Sources defaultOpen> <SourcesTrigger /> <SourcesContent> </SourcesContent> </Sources></div><script setup lang="ts">import { Source, Sources, SourcesContent, SourcesTrigger,} from "@/components/ai/sources";</script>
<template> <div class="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger /> <SourcesContent> <Source :key="s.href" :href="s.href" :title="s.title" /> </SourcesContent> </Sources> <Sources default-open> <SourcesTrigger /> <SourcesContent> <Source :key="s.href" :href="s.href" :title="s.title" /> </SourcesContent> </Sources> </div></template><script lang="ts"> import { Source, Sources, SourcesContent, SourcesTrigger, } from "$lib/components/ai/sources";</script>
<div class="flex w-full max-w-lg flex-col gap-6"> <Sources> <SourcesTrigger /> <SourcesContent> {#each sources as s (s.href)} <Source href={s.href} title={s.title} /> {/each} </SourcesContent> </Sources> <Sources open> <SourcesTrigger /> <SourcesContent> {#each sources as s (s.href)} <Source href={s.href} title={s.title} /> {/each} </SourcesContent> </Sources></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { BookmarkIcon, CaretDownIcon, GlobeIcon } from "@phosphor-icons/react";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";
export type SourcesProps = ComponentProps<typeof Collapsible>;
export const Sources = ({ className, ...props }: SourcesProps) => ( <Collapsible data-slot="ai-sources" className={cn("not-prose text-[13.5px]", className)} {...props} />);
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { count: number;};
export const SourcesTrigger = ({ className, count, children, ...props}: SourcesTriggerProps) => ( <CollapsibleTrigger data-slot="ai-sources-trigger" className={cn( "group/sources-trigger flex w-full items-center gap-2 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className, )} {...props} > {children ?? ( <> <BookmarkIcon className="size-4" /> <span> Used {count} {count === 1 ? "source" : "sources"} </span> <CaretDownIcon className="ml-auto size-4 transition-transform group-data-[panel-open]/sources-trigger:rotate-180" /> </> )} </CollapsibleTrigger>);
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
export const SourcesContent = ({ className, children, ...props}: SourcesContentProps) => ( <CollapsibleContent data-slot="ai-sources-content" className={cn( "h-(--collapsible-panel-height) overflow-hidden outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0", className, )} {...props} > <div className="flex flex-col gap-1.5 pt-2 pl-[23px]">{children}</div> </CollapsibleContent>);
export type SourceProps = ComponentProps<"a">;
const hostOf = (href?: string) => { if (!href) return undefined; try { return new URL(href).hostname.replace(/^www\./, ""); } catch { return undefined; }};
export const Source = ({ href, title, className, children, ...props}: SourceProps) => { const host = hostOf(href); return ( <a data-slot="ai-source" className={cn("flex items-center gap-2 text-foreground", className)} href={href} rel="noreferrer" target="_blank" {...props} > {children ?? ( <> <GlobeIcon className="size-3.5 shrink-0 text-muted-foreground" /> <span className="underline underline-offset-[3px]">{title}</span> {host && ( <span className="font-mono text-[11.5px] text-muted-foreground"> {host} </span> )} </> )} </a> );};<script setup lang="ts">import type { HTMLAttributes } from "vue"import { GlobeIcon } from "@lucide/vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ href?: string title?: string class?: HTMLAttributes["class"]}>()
const host = computed(() => { if (!props.href) return undefined try { return new URL(props.href).hostname.replace(/^www\./, "") } catch { return undefined }})</script>
<template> <a data-slot="ai-source" :class="cn('flex items-center gap-2 text-foreground', props.class)" :href="href" rel="noreferrer" target="_blank" > <slot> <GlobeIcon class="size-3.5 shrink-0 text-muted-foreground" /> <span class="underline underline-offset-[3px]">{{ title }}</span> <span v-if="host" class="font-mono text-[11.5px] text-muted-foreground">{{ host }}</span> </slot> </a></template><script setup lang="ts">import type { CollapsibleRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Collapsible } from "@/registry/edmi/ui/collapsible"
interface SourcesProps extends /* @vue-ignore */ CollapsibleRootProps { class?: HTMLAttributes["class"]}
const props = defineProps<SourcesProps>()</script>
<template> <!-- Collapsible props (open, defaultOpen, update:open) fall through as attrs. --> <Collapsible data-slot="ai-sources" :class="cn('not-prose text-[13.5px]', props.class)"> <slot /> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleContent data-slot="ai-sources-content" :class="cn( 'overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class, )" > <div class="flex flex-col gap-1.5 pt-2 pl-[23px]"> <slot /> </div> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { BookmarkIcon, ChevronDownIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ count: number class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleTrigger data-slot="ai-sources-trigger" :class="cn( 'group/sources-trigger flex w-full items-center gap-2 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', props.class, )" > <slot> <BookmarkIcon class="size-4" /> <span>Used {{ count }} {{ count === 1 ? "source" : "sources" }}</span> <ChevronDownIcon class="ml-auto size-4 transition-transform group-data-[state=open]/sources-trigger:rotate-180" /> </slot> </CollapsibleTrigger></template>export { default as Source } from "./Source.vue";export { default as Sources } from "./Sources.vue";export { default as SourcesContent } from "./SourcesContent.vue";export { default as SourcesTrigger } from "./SourcesTrigger.vue";import Source from "./source.svelte";import Root from "./sources.svelte";import Content from "./sources-content.svelte";import Trigger from "./sources-trigger.svelte";
export { Content, Content as SourcesContent, Root, // Root as Sources, Source, Trigger, Trigger as SourcesTrigger,};<script lang="ts"> import GlobeIcon from "phosphor-svelte/lib/Globe"; import { cn } from "$lib/utils.js"; import type { HTMLAnchorAttributes } from "svelte/elements";
let { class: className, href, title, children, ...restProps }: HTMLAnchorAttributes = $props();
const host = $derived.by(() => { if (!href) return undefined; try { return new URL(href).hostname.replace(/^www\./, ""); } catch { return undefined; } });</script>
<a data-slot="ai-source" class={cn("flex items-center gap-2 text-foreground", className)} {href} rel="noreferrer" target="_blank" {...restProps}> {#if children} {@render children()} {:else} <GlobeIcon class="size-3.5 shrink-0 text-muted-foreground" /> <span class="underline underline-offset-[3px]">{title}</span> {#if host} <span class="font-mono text-[11.5px] text-muted-foreground">{host}</span> {/if} {/if}</a><script lang="ts"> import { cn } from "$lib/utils.js"; import { CollapsibleContent } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CollapsibleContent> = $props();</script>
<CollapsibleContent data-slot="ai-sources-content" class={cn( "overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className )} {...restProps}> <div class="flex flex-col gap-1.5 pt-2 pl-[23px]"> {@render children?.()} </div></CollapsibleContent><script lang="ts"> import BookmarkIcon from "phosphor-svelte/lib/Bookmark"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn } from "$lib/utils.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, count, children, ...restProps }: ComponentProps<typeof CollapsibleTrigger> & { count: number } = $props();</script>
<CollapsibleTrigger data-slot="ai-sources-trigger" class={cn( "group/sources-trigger flex w-full items-center gap-2 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className )} {...restProps}> {#if children} {@render children()} {:else} <BookmarkIcon class="size-4" /> <span>Used {count} {count === 1 ? "source" : "sources"}</span> <CaretDownIcon class="ml-auto size-4 transition-transform group-data-[state=open]/sources-trigger:rotate-180" /> {/if}</CollapsibleTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import { Collapsible } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, open = $bindable(false), children, ...restProps }: ComponentProps<typeof Collapsible> = $props();</script>
<Collapsible bind:open data-slot="ai-sources" class={cn("not-prose text-[13.5px]", className)} {...restProps}> {@render children?.()}</Collapsible>| Part | Prop | Type | Default |
|---|---|---|---|
| Sources | open, defaultOpen, onOpenChange |
collapsible state | closed |
| SourcesTrigger | count |
number |
|
| Source | href, title |
link props |
✦ Edmi additions
Section titled “✦ Edmi additions”Sourceshows the host (reuters.com) in mono next to the underlined title, derived fromhref.- The trigger spans the row with the chevron on the right, like the other agent-output disclosures.