Sources

A component that allows a user to view the sources or citations used to generate a response.

The Sources component allows a user to view the sources or citations used to generate a response.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add sources

Install Manually

Copy and paste the following code in the same folder.

Source.vue
Sources.vue
SourcesTrigger.vue
SourcesContent.vue
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { BookIcon } from 'lucide-vue-next'

const props = defineProps<{
  href: string
  title: string
  class?: HTMLAttributes['class']
}>()
</script>

<template>
  <a
    :class="cn('flex items-center gap-2', props.class)"
    :href="props.href"
    rel="noreferrer"
    target="_blank"
  >
    <slot>
      <BookIcon class="h-4 w-4" />
      <span class="block font-medium">{{ props.title }}</span>
    </slot>
  </a>
</template>

Usage with AI SDK

Build a simple web search agent with Perplexity Sonar.

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { ref } from 'vue'

import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation'
import { Message, MessageContent } from '@/components/ai-elements/message'
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'
import { Response } from '@/components/ai-elements/response'
import {
  Source,
  Sources,
  SourcesContent,
  SourcesTrigger,
} from '@/components/ai-elements/sources'

const input = ref('')

const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/sources',
  }),
})

function handleSubmit() {
  if (input.value.trim()) {
    sendMessage({ text: input.value })
    input.value = ''
  }
}

function getSourceParts(message: any) {
  return message.parts.filter((part: any) => part.type === 'source-url')
}

function getTextParts(message: any) {
  return message.parts.filter((part: any) => part.type === 'text')
}
</script>

<template>
  <div
    class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
  >
    <div class="flex flex-col h-full">
      <div class="flex-1 overflow-auto mb-4">
        <Conversation>
          <ConversationContent>
            <div v-for="message in messages" :key="message.id">
              <Sources v-if="message.role === 'assistant'">
                <SourcesTrigger :count="getSourceParts(message).length" />
                <SourcesContent>
                  <template
                    v-for="(part, i) in getSourceParts(message)"
                    :key="`${message.id}-${i}`"
                  >
                    <Source :href="part.url" :title="part.url" />
                  </template>
                </SourcesContent>
              </Sources>

              <Message :from="message.role">
                <MessageContent>
                  <template
                    v-for="(part, i) in getTextParts(message)"
                    :key="`${message.id}-${i}`"
                  >
                    <Response>{{ part.text }}</Response>
                  </template>
                </MessageContent>
              </Message>
            </div>
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>
      </div>

      <PromptInput
        class="mt-4 w-full max-w-2xl mx-auto relative"
        @submit.prevent="handleSubmit"
      >
        <PromptInputTextarea
          v-model="input"
          placeholder="Ask a question and search the..."
          class="pr-12"
        />
        <PromptInputSubmit
          :status="status === 'streaming' ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

Add the following route to your backend:

server/api/chat/route.ts
import type { UIMessage } from 'ai'
import { perplexity } from '@ai-sdk/perplexity'
import { convertToModelMessages, streamText } from 'ai'

export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const body = await readBody<{ messages: UIMessage[] }>(event)

  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: convertToModelMessages(body.messages),
  })

  return result.toUIMessageStreamResponse({
    sendSources: true,
  })
})

Features

  • Collapsible component that allows a user to view the sources or citations used to generate a response
  • Customizable trigger and content components
  • Support for custom sources or citations
  • Responsive design with mobile-friendly controls
  • Clean, modern styling with customizable themes

Examples

Custom Rendering

Props

<Source />

hrefstring
''
The URL of the source.
titlestring
''
The title of the source.
classstring
''
Additional CSS classes to apply to the component.

<Sources />

classstring
''
Additional CSS classes to apply to the component.

<SourcesTrigger />

countnumber
0
The number of sources or citations.
classstring
''
Additional CSS classes to apply to the component.

<SourcesContent />

classstring
''
Additional CSS classes to apply to the component.