Sources
Where an answer came from, folded under a count.
Where an answer came from, folded under a count. A cited answer usually cites more places than anyone wants listed above it, so the list opens from a single line: "Used 6 sources".
Folded is the resting state, unlike Reasoning — a citation list is a thing you go and check rather than a thing you watch arrive.
Installation
Sources ships with the library — no separate install.
import { Sources, Message, Avatar } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add sourcesUsage
<Sources>
<Sources.Trigger count={sources.length} />
<Sources.Content>
{sources.map((source) => (
<Sources.Source
key={source.url}
href={source.url}
title={source.title}
/>
))}
</Sources.Content>
</Sources>Composition
<Sources>
<Sources.Trigger>…</Sources.Trigger>
<Sources.Content>…</Sources.Content>
<Sources.Source>…</Sources.Source>
</Sources>Sources.Trigger— The line that opens the list.countreads "Used 6 sources".Sources.Content— The list, collapsed until the trigger is pressed.Sources.Source— One citation. Openshrefwith the platform's own handler, and falls back to the URL's host when there is notitle.
Examples
A list of citations
title is what the row says and href is where it goes. With no title the host stands in — which is a better label than a hundred characters of path, and models often send no title at all.
<Sources>
<Sources.Trigger count={3} />
<Sources.Content>
<Sources.Source href="https://expo.dev/changelog" title="Expo SDK 57" />
<Sources.Source href="https://reactnative.dev/blog" title="New Architecture" />
<Sources.Source href="https://docs.swmansion.com/react-native-reanimated/" />
</Sources.Content>
</Sources>Reading message parts
With the AI SDK these are the source-url parts of an assistant message. Filter once for the count and map the same array, so the number on the trigger cannot disagree with the list under it.
const sources = message.parts.filter(
(part) => part.type === 'source-url'
);
{sources.length > 0 && (
<Sources>
<Sources.Trigger count={sources.length} />
<Sources.Content>
{sources.map((part) => (
<Sources.Source
key={part.sourceId}
href={part.url}
title={part.title}
/>
))}
</Sources.Content>
</Sources>
)}Favicons instead of the link glyph
icon replaces the leading glyph on a row. A favicon is worth the request in a real citation list: it is what a reader recognises a source by before they have read the title.
<Sources.Source
href={part.url}
title={part.title}
icon={
<Avatar size="xs" className="h-4 w-4">
<Avatar.Image
source={{ uri: `https://icons.duckduckgo.com/ip3/${host}.ico` }}
/>
</Avatar>
}
/>Under a turn
Below the bubble rather than above it: the citations are what the answer rests on, and they are read after it.
<Message align="start">
<Message.Content>
<Message.Bubble>
<Message.BubbleContent>{answer}</Message.BubbleContent>
</Message.Bubble>
<Sources>
<Sources.Trigger count={sources.length} />
<Sources.Content>
{sources.map((source) => (
<Sources.Source key={source.url} href={source.url} title={source.title} />
))}
</Sources.Content>
</Sources>
</Message.Content>
</Message>API Reference
Sources
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial state when uncontrolled. Folded, which is where a citation list belongs. |
onOpenChange | (open: boolean) => void | — |
Sources.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
count | number | 0 | How many sources the list holds. Reads "Used 6 sources". |
Sources.Content
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Sources.Source
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
href | string | — | Where the row goes. Opened with the platform's own handler. |
title | string | — | What the row says. Falls back to the URL's host, which is a better label than a hundred characters of path — and models often send no title at all. |
icon | ReactNode | — | Leading glyph. A link by default; a favicon suits a real citation list. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
A row is a link, and says so. Each carries the link role and its title as the accessibility label, so a screen reader announces where it goes rather than reading the URL out character by character.
The host is parsed with a regex, not URL. URL exists in Hermes but throws on anything malformed, and a model's citation is not a thing to trust with an exception. A label is worth having even when the string it came from is not a URL at all.
Linking.openURL is called without a canOpenURL guard. That check needs the scheme declared up front on iOS, and a source is always http(s), which is always openable — the guard would only add a way to fail.
Nothing here depends on the AI SDK. href and title are strings; they happen to be the shape of a source-url part.