Initial commit
This commit is contained in:
388
docs/components/inline-citation.md
Normal file
388
docs/components/inline-citation.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# 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">',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
Reference in New Issue
Block a user