来源
一个允许用户查看用于生成响应的来源或引用的组件。
Sources 组件允许用户查看用于生成响应的来源或引用。
使用 CLI 安装
AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add sources
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/sources.json
手动安装
将以下代码复制并粘贴到同一文件夹中。
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>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Collapsible
:class="cn('not-prose mb-4 text-primary text-xs', props.class)"
>
<slot />
</Collapsible>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { CollapsibleTrigger } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { ChevronDownIcon } from 'lucide-vue-next'
const props = defineProps<{
count: number
class?: HTMLAttributes['class']
}>()
</script>
<template>
<CollapsibleTrigger
:class="cn('flex items-center gap-2', props.class)"
>
<slot>
<p class="font-medium">
Used {{ props.count }} sources
</p>
<ChevronDownIcon class="h-4 w-4" />
</slot>
</CollapsibleTrigger>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { CollapsibleContent } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<CollapsibleContent
:class="
cn(
'mt-3 flex w-fit flex-col gap-2',
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
props.class,
)
"
>
<slot />
</CollapsibleContent>
</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'
与 AI SDK 一起使用
使用 Perplexity Sonar 构建简单的网络搜索代理。
将以下组件添加到你的前端:
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>
将以下路由添加到你的后端:
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,
})
})
特性
- 可折叠组件,允许用户查看用于生成响应的来源或引用
- 可自定义的触发器和内容组件
- 支持自定义来源或引用
- 具有移动友好控件的响应式设计
- 具有可自定义主题的简洁现代样式
示例
自定义渲染
Props
<Source />
hrefstring
''titlestring
''classstring
''<Sources />
classstring
''<SourcesTrigger />
countnumber
0classstring
''<SourcesContent />
classstring
''