389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# Inline Citation
|
|
|
|
URL: /components/inline-citation
|
|
|
|
---
|
|
|
|
title: Inline Citation
|
|
description: A hoverable citation component that displays source information and quotes inline with text, perfect for AI-generated content with references.
|
|
path: elements/components/inline-citation
|
|
|
|
---
|
|
|
|
The `InlineCitation` component provides a way to display citations inline with text content, similar to academic papers or research documents. It consists of a citation pill that shows detailed source information on hover, making it perfect for AI-generated content that needs to reference sources.
|
|
|
|
<Preview path="inline-citation" />
|
|
|
|
## Installation
|
|
|
|
<ElementsInstaller path="inline-citation" />
|
|
|
|
## Usage
|
|
|
|
```tsx
|
|
import {
|
|
InlineCitation,
|
|
InlineCitationCard,
|
|
InlineCitationCardBody,
|
|
InlineCitationCardTrigger,
|
|
InlineCitationCarousel,
|
|
InlineCitationCarouselContent,
|
|
InlineCitationCarouselItem,
|
|
InlineCitationCarouselHeader,
|
|
InlineCitationCarouselIndex,
|
|
InlineCitationSource,
|
|
InlineCitationText,
|
|
} from "@/components/ai-elements/inline-citation";
|
|
```
|
|
|
|
```tsx
|
|
<InlineCitation>
|
|
<InlineCitationText>{citation.text}</InlineCitationText>
|
|
<InlineCitationCard>
|
|
<InlineCitationCardTrigger sources={citation.sources.map((source) => source.url)} />
|
|
<InlineCitationCardBody>
|
|
<InlineCitationCarousel>
|
|
<InlineCitationCarouselHeader>
|
|
<InlineCitationCarouselIndex />
|
|
</InlineCitationCarouselHeader>
|
|
<InlineCitationCarouselContent>
|
|
<InlineCitationCarouselItem>
|
|
<InlineCitationSource title="AI SDK" url="https://ai-sdk.dev" description="The AI Toolkit for TypeScript" />
|
|
</InlineCitationCarouselItem>
|
|
</InlineCitationCarouselContent>
|
|
</InlineCitationCarousel>
|
|
</InlineCitationCardBody>
|
|
</InlineCitationCard>
|
|
</InlineCitation>
|
|
```
|
|
|
|
## Usage with AI SDK
|
|
|
|
Build citations for AI-generated content using [`experimental_generateObject`](/docs/reference/ai-sdk-ui/use-object).
|
|
|
|
Add the following component to your frontend:
|
|
|
|
```tsx title="app/page.tsx"
|
|
"use client";
|
|
|
|
import { experimental_useObject as useObject } from "@ai-sdk/react";
|
|
import {
|
|
InlineCitation,
|
|
InlineCitationText,
|
|
InlineCitationCard,
|
|
InlineCitationCardTrigger,
|
|
InlineCitationCardBody,
|
|
InlineCitationCarousel,
|
|
InlineCitationCarouselContent,
|
|
InlineCitationCarouselItem,
|
|
InlineCitationCarouselHeader,
|
|
InlineCitationCarouselIndex,
|
|
InlineCitationCarouselPrev,
|
|
InlineCitationCarouselNext,
|
|
InlineCitationSource,
|
|
InlineCitationQuote,
|
|
} from "@/components/ai-elements/inline-citation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { citationSchema } from "@/app/api/citation/route";
|
|
|
|
const CitationDemo = () => {
|
|
const { object, submit, isLoading } = useObject({
|
|
api: "/api/citation",
|
|
schema: citationSchema,
|
|
});
|
|
|
|
const handleSubmit = (topic: string) => {
|
|
submit({ prompt: topic });
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
|
<div className="flex gap-2 mb-6">
|
|
<Button onClick={() => handleSubmit("artificial intelligence")} disabled={isLoading} variant="outline">
|
|
Generate AI Content
|
|
</Button>
|
|
<Button onClick={() => handleSubmit("climate change")} disabled={isLoading} variant="outline">
|
|
Generate Climate Content
|
|
</Button>
|
|
</div>
|
|
|
|
{isLoading && !object && <div className="text-muted-foreground">Generating content with citations...</div>}
|
|
|
|
{object?.content && (
|
|
<div className="prose prose-sm max-w-none">
|
|
<p className="leading-relaxed">
|
|
{object.content.split(/(\[\d+\])/).map((part, index) => {
|
|
const citationMatch = part.match(/\[(\d+)\]/);
|
|
if (citationMatch) {
|
|
const citationNumber = citationMatch[1];
|
|
const citation = object.citations?.find((c: any) => c.number === citationNumber);
|
|
|
|
if (citation) {
|
|
return (
|
|
<InlineCitation key={index}>
|
|
<InlineCitationCard>
|
|
<InlineCitationCardTrigger sources={[citation.url]} />
|
|
<InlineCitationCardBody>
|
|
<InlineCitationCarousel>
|
|
<InlineCitationCarouselHeader>
|
|
<InlineCitationCarouselPrev />
|
|
<InlineCitationCarouselNext />
|
|
<InlineCitationCarouselIndex />
|
|
</InlineCitationCarouselHeader>
|
|
<InlineCitationCarouselContent>
|
|
<InlineCitationCarouselItem>
|
|
<InlineCitationSource
|
|
title={citation.title}
|
|
url={citation.url}
|
|
description={citation.description}
|
|
/>
|
|
{citation.quote && <InlineCitationQuote>{citation.quote}</InlineCitationQuote>}
|
|
</InlineCitationCarouselItem>
|
|
</InlineCitationCarouselContent>
|
|
</InlineCitationCarousel>
|
|
</InlineCitationCardBody>
|
|
</InlineCitationCard>
|
|
</InlineCitation>
|
|
);
|
|
}
|
|
}
|
|
return part;
|
|
})}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CitationDemo;
|
|
```
|
|
|
|
Add the following route to your backend:
|
|
|
|
```ts title="app/api/citation/route.ts"
|
|
import { streamObject } from "ai";
|
|
import { z } from "zod";
|
|
|
|
export const citationSchema = z.object({
|
|
content: z.string(),
|
|
citations: z.array(
|
|
z.object({
|
|
number: z.string(),
|
|
title: z.string(),
|
|
url: z.string(),
|
|
description: z.string().optional(),
|
|
quote: z.string().optional(),
|
|
})
|
|
),
|
|
});
|
|
|
|
// Allow streaming responses up to 30 seconds
|
|
export const maxDuration = 30;
|
|
|
|
export async function POST(req: Request) {
|
|
const { prompt } = await req.json();
|
|
|
|
const result = streamObject({
|
|
model: "openai/gpt-4o",
|
|
schema: citationSchema,
|
|
prompt: `Generate a well-researched paragraph about ${prompt} with proper citations.
|
|
|
|
Include:
|
|
- A comprehensive paragraph with inline citations marked as [1], [2], etc.
|
|
- 2-3 citations with realistic source information
|
|
- Each citation should have a title, URL, and optional description/quote
|
|
- Make the content informative and the sources credible
|
|
|
|
Format citations as numbered references within the text.`,
|
|
});
|
|
|
|
return result.toTextStreamResponse();
|
|
}
|
|
```
|
|
|
|
## Features
|
|
|
|
- Hover interaction to reveal detailed citation information
|
|
- **Carousel navigation** for multiple citations with prev/next controls
|
|
- **Live index tracking** showing current slide position (e.g., "1/5")
|
|
- Support for source titles, URLs, and descriptions
|
|
- Optional quote blocks for relevant excerpts
|
|
- Composable architecture for flexible citation formats
|
|
- Accessible design with proper keyboard navigation
|
|
- Seamless integration with AI-generated content
|
|
- Clean visual design that doesn't disrupt reading flow
|
|
- Smart badge display showing source hostname and count
|
|
|
|
## Props
|
|
|
|
### `<InlineCitation />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the root span element.',
|
|
type: 'React.ComponentProps<"span">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationText />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying span element.',
|
|
type: 'React.ComponentProps<"span">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCard />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the HoverCard component.',
|
|
type: 'React.ComponentProps<"span">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCardTrigger />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
sources: {
|
|
description: 'Array of source URLs. The length determines the number displayed in the badge.',
|
|
type: 'string[]',
|
|
},
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying button element.',
|
|
type: 'React.ComponentProps<"button">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCardBody />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying div.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarousel />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying Carousel component.',
|
|
type: 'React.ComponentProps<typeof Carousel>',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselContent />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying CarouselContent component.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselItem />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying div.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselHeader />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying div.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselIndex />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying div. Children will override the default index display.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselPrev />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying CarouselPrevious component.',
|
|
type: 'React.ComponentProps<typeof CarouselPrevious>',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationCarouselNext />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying CarouselNext component.',
|
|
type: 'React.ComponentProps<typeof CarouselNext>',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationSource />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
title: {
|
|
description: 'The title of the source.',
|
|
type: 'string',
|
|
},
|
|
url: {
|
|
description: 'The URL of the source.',
|
|
type: 'string',
|
|
},
|
|
description: {
|
|
description: 'A brief description of the source.',
|
|
type: 'string',
|
|
},
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying div.',
|
|
type: 'React.ComponentProps<"div">',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
### `<InlineCitationQuote />`
|
|
|
|
<TypeTable
|
|
type={{
|
|
'...props': {
|
|
description: 'Any other props are spread to the underlying blockquote element.',
|
|
type: 'React.ComponentProps<"blockquote">',
|
|
},
|
|
}}
|
|
/>
|