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 sources

Usage

<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. count reads "Used 6 sources".
  • Sources.Content — The list, collapsed until the trigger is pressed.
  • Sources.Source — One citation. Opens href with the platform's own handler, and falls back to the URL's host when there is no title.

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>
)}

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

PropTypeDefaultDescription
classNamestring
openbooleanControlled open state.
defaultOpenbooleanfalseInitial state when uncontrolled. Folded, which is where a citation list belongs.
onOpenChange(open: boolean) => void

Sources.Trigger

PropTypeDefaultDescription
classNamestring
countnumber0How many sources the list holds. Reads "Used 6 sources".

Sources.Content

PropTypeDefaultDescription
classNamestring

Sources.Source

PropTypeDefaultDescription
classNamestring
hrefstringWhere the row goes. Opened with the platform's own handler.
titlestringWhat 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.
iconReactNodeLeading 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.

On this page