docs: add more chatui examples (#128)

This commit is contained in:
Thuc Pham
2025-06-09 16:52:44 +07:00
committed by GitHub
parent 785f9575a6
commit 23093ed223
17 changed files with 1070 additions and 39 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'web': patch
'@llamaindex/chat-ui': patch
'@llamaindex/chat-ui-docs': patch
---
docs: add more chatui examples
+305
View File
@@ -0,0 +1,305 @@
'use client'
import { useCopyToClipboard } from '@/app/use-copy-to-clipboard'
import {
Artifact,
ChatCanvas,
ChatInput,
ChatMessage,
ChatMessages,
ChatSection,
useChatCanvas,
useChatUI,
} from '@llamaindex/chat-ui'
import { Message, useChat } from 'ai/react'
import { Image } from 'lucide-react'
const code = `
import {
Artifact,
ChatCanvas,
ChatInput,
ChatMessage,
ChatMessages,
ChatSection,
useChatCanvas,
useChatUI,
} from '@llamaindex/chat-ui'
import { useChat } from 'ai/react'
import { Image } from 'lucide-react'
export function CustomChat() {
const handler = useChat({ initialMessages: [] })
return (
<ChatSection
handler={handler}
className="block h-screen flex-row gap-4 p-0 md:flex md:p-5"
>
<div className="md:max-w-1/2 mx-auto flex h-full min-w-0 max-w-full flex-1 flex-col gap-4">
<CustomChatMessages />
<ChatInput />
</div>
<ChatCanvas>
<ChatCanvas.CodeArtifact />
<ChatCanvas.DocumentArtifact />
<ImageArtifactViewer />
</ChatCanvas>
</ChatSection>
)
}
type ImageArtifact = Artifact<
{
imageUrl: string
caption: string
},
'image'
>
function ImageArtifactViewer() {
const { displayedArtifact } = useChatCanvas()
if (displayedArtifact?.type !== 'image') return null
const {
data: { imageUrl, caption },
} = displayedArtifact as ImageArtifact
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex items-center justify-between border-b p-4">
<h3 className="flex items-center gap-3 text-gray-600">
<Image className="size-8 text-blue-500" />
<div className="text font-semibold">{caption}</div>
</h3>
<ChatCanvas.Actions>
<ChatCanvas.Actions.History />
<ChatCanvas.Actions.Close />
</ChatCanvas.Actions>
</div>
<div className="flex flex-1 items-center justify-center p-4">
<img
src={imageUrl}
alt={caption}
className="h-[70%] rounded-2xl shadow-2xl"
/>
</div>
</div>
)
}
function CustomChatMessages() {
const { messages } = useChatUI()
return (
<ChatMessages>
<ChatMessages.List className="px-0 md:px-16">
{messages.map((message, index) => (
<ChatMessage
key={index}
message={message}
isLast={index === messages.length - 1}
className="items-start"
>
<ChatMessage.Avatar />
<ChatMessage.Content>
<ChatMessage.Content.Markdown
annotationRenderers={{
artifact: CustomArtifactCard,
}}
/>
</ChatMessage.Content>
<ChatMessage.Actions />
</ChatMessage>
))}
</ChatMessages.List>
</ChatMessages>
)
}
// custom artifact card for image artifacts
function CustomArtifactCard({ data }: { data: Artifact }) {
return (
<ChatCanvas.Artifact
data={data}
getTitle={artifact => (artifact as ImageArtifact).data.caption}
iconMap={{ image: Image }}
/>
)
}
`
const initialMessages: Message[] = [
{
id: '1',
role: 'user',
content: 'Generate an image of a cat',
},
{
id: '2',
role: 'assistant',
content:
'Here is a cat image named Millie.' +
`\n\`\`\`annotation\n${JSON.stringify({
type: 'artifact',
data: {
type: 'image',
data: {
imageUrl: 'https://placecats.com/millie/700/500',
caption: 'A cute cat image named Millie',
},
created_at: 1745480281756,
},
})}
\n\`\`\`\n`,
},
{
id: '3',
role: 'user',
content: 'Please generate a black cat image',
},
{
id: '4',
role: 'assistant',
content:
'Here is a black cat image named Poppy.' +
`\n\`\`\`annotation\n${JSON.stringify({
type: 'artifact',
data: {
type: 'image',
data: {
imageUrl: 'https://placecats.com/poppy/700/500',
caption: 'A black cat image named Poppy',
},
created_at: 1745480281999,
},
})}
\n\`\`\`\n`,
},
]
export default function Page(): JSX.Element {
return <CustomChat />
}
function CustomChat() {
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 2000 })
const handler = useChat({ initialMessages })
return (
<ChatSection
handler={handler}
className="block h-screen flex-row gap-4 p-0 md:flex md:p-5"
>
<div className="md:max-w-1/2 mx-auto flex h-full min-w-0 max-w-full flex-1 flex-col gap-4">
<div className="flex justify-between gap-2 border-b p-4">
<div>
<h1 className="bg-gradient-to-r from-[#e711dd] to-[#0fc1e0] bg-clip-text text-lg font-bold text-transparent">
LlamaIndex ChatUI - Custom Canvas Demo
</h1>
<p className="text-sm text-zinc-400">
Try click to a version to see how it works
</p>
</div>
<button
type="button"
onClick={() => {
copyToClipboard(code)
}}
className={`flex h-10 items-center gap-2 rounded-lg bg-zinc-700 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-600 ${
isCopied ? 'bg-green-600 hover:bg-green-500' : ''
}`}
>
{isCopied ? 'Copied!' : 'Copy Code'}
</button>
</div>
<CustomChatMessages />
<ChatInput />
</div>
<ChatCanvas>
<ChatCanvas.CodeArtifact />
<ChatCanvas.DocumentArtifact />
<ImageArtifactViewer />
</ChatCanvas>
</ChatSection>
)
}
type ImageArtifact = Artifact<
{
imageUrl: string
caption: string
},
'image'
>
function ImageArtifactViewer() {
const { displayedArtifact } = useChatCanvas()
if (displayedArtifact?.type !== 'image') return null
const {
data: { imageUrl, caption },
} = displayedArtifact as ImageArtifact
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex items-center justify-between border-b p-4">
<h3 className="flex items-center gap-3 text-gray-600">
<Image className="size-8 text-blue-500" />
<div className="text font-semibold">{caption}</div>
</h3>
<ChatCanvas.Actions>
<ChatCanvas.Actions.History />
<ChatCanvas.Actions.Close />
</ChatCanvas.Actions>
</div>
<div className="flex flex-1 items-center justify-center p-4">
<img
src={imageUrl}
alt={caption}
className="h-[70%] rounded-2xl shadow-2xl"
/>
</div>
</div>
)
}
function CustomChatMessages() {
const { messages } = useChatUI()
return (
<ChatMessages>
<ChatMessages.List className="px-0 md:px-16">
{messages.map((message, index) => (
<ChatMessage
key={index}
message={message}
isLast={index === messages.length - 1}
className="items-start"
>
<ChatMessage.Avatar />
<ChatMessage.Content>
<ChatMessage.Content.Markdown
annotationRenderers={{
artifact: CustomArtifactCard,
}}
/>
</ChatMessage.Content>
<ChatMessage.Actions />
</ChatMessage>
))}
</ChatMessages.List>
</ChatMessages>
)
}
// custom artifact card for image artifacts
function CustomArtifactCard({ data }: { data: Artifact }) {
return (
<ChatCanvas.Artifact
data={data}
getTitle={artifact => (artifact as ImageArtifact).data.caption}
iconMap={{ image: Image }}
/>
)
}
+23 -11
View File
@@ -8,6 +8,8 @@ import {
ChatSection,
} from '@llamaindex/chat-ui'
import { Message, useChat } from 'ai/react'
import { ArrowRightIcon } from 'lucide-react'
import Link from 'next/link'
const code = `
import {
@@ -745,17 +747,27 @@ function CustomChat() {
Try click to a version to see how it works
</p>
</div>
<button
type="button"
onClick={() => {
copyToClipboard(code)
}}
className={`flex h-10 items-center gap-2 rounded-lg bg-zinc-700 px-2 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-600 ${
isCopied ? 'bg-green-600 hover:bg-green-500' : ''
}`}
>
{isCopied ? 'Copied!' : 'Copy Code'}
</button>
<div className="flex gap-4">
<button
type="button"
onClick={() => {
copyToClipboard(code)
}}
className={`flex h-10 items-center gap-2 rounded-lg bg-zinc-700 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-600 ${
isCopied ? 'bg-green-600 hover:bg-green-500' : ''
}`}
>
{isCopied ? 'Copied!' : 'Copy Code'}
</button>
<Link href="/demo/canvas/custom">
<button
type="button"
className="flex h-10 items-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500"
>
Custom Viewer <ArrowRightIcon className="h-4 w-4" />
</button>
</Link>
</div>
</div>
<ChatMessages />
<ChatInput />
+153
View File
@@ -386,6 +386,159 @@ function ChatWithCanvas() {
}
```
### Adding Custom Artifact Viewers
You can extend the canvas with custom artifact viewers to handle different types of content. Here's an example of creating a custom image artifact viewer:
```tsx
import {
Artifact,
ChatCanvas,
useChatCanvas
} from '@llamaindex/chat-ui'
import { Image } from 'lucide-react'
// Define your custom artifact type
type ImageArtifact = Artifact<
{
imageUrl: string
caption: string
},
'image'
>
function ImageArtifactViewer() {
const { displayedArtifact } = useChatCanvas()
// Only render for image artifacts
if (displayedArtifact?.type !== 'image') return null
const {
data: { imageUrl, caption },
} = displayedArtifact as ImageArtifact
return (
<div className="flex min-h-0 flex-1 flex-col">
{/* Custom header with icon and title */}
<div className="flex items-center justify-between border-b p-4">
<h3 className="flex items-center gap-3 text-gray-600">
<Image className="size-8 text-blue-500" />
<div className="font-semibold">{caption}</div>
</h3>
{/* Use built-in canvas actions */}
<ChatCanvas.Actions>
<ChatCanvas.Actions.History />
<ChatCanvas.Actions.Close />
</ChatCanvas.Actions>
</div>
{/* Custom content area */}
<div className="flex flex-1 items-center justify-center p-4">
<img
src={imageUrl}
alt={caption}
className="h-[70%] rounded-2xl shadow-2xl"
/>
</div>
</div>
)
}
// Use the custom viewer in your chat
function CustomChat() {
const handler = useChat({ initialMessages: [] })
return (
<ChatSection handler={handler}>
<div className="flex h-full flex-col">
<ChatMessages />
<ChatInput />
</div>
<ChatCanvas>
{/* Built-in artifact viewers */}
<ChatCanvas.CodeArtifact />
<ChatCanvas.DocumentArtifact />
{/* Your custom artifact viewer */}
<ImageArtifactViewer />
</ChatCanvas>
</ChatSection>
)
}
```
You can also custom ArtifactCard for your artifact type.
```tsx
import { Image } from 'lucide-react'
// custom artifact card for image artifacts with title is the caption and icon is the image icon
function CustomArtifactCard({ data }: { data: Artifact }) {
return (
<ChatCanvas.Artifact
data={data}
getTitle={artifact => (artifact as ImageArtifact).data.caption}
iconMap={{ image: Image }}
/>
)
}
// update markdown annotation renderers to use your custom artifact card
<ChatMessage.Content>
<ChatMessage.Content.Markdown
annotationRenderers={{
artifact: CustomArtifactCard,
}}
/>
</ChatMessage.Content>
```
To trigger your custom artifact viewer, the AI response should include an annotation with the matching artifact type:
```tsx
// Example of how to create an artifact in AI response
const response = `Here is your image!
\`\`\`annotation
${JSON.stringify({
type: 'artifact',
data: {
type: 'image', // This matches your viewer's check
data: {
imageUrl: 'https://example.com/image.jpg',
caption: 'A beautiful landscape'
},
created_at: Date.now(),
},
})}
\`\`\`
`
```
You can create multiple custom artifact viewers for different content types:
```tsx
<ChatCanvas>
{/* Built-in viewers */}
<ChatCanvas.CodeArtifact />
<ChatCanvas.DocumentArtifact />
{/* Custom viewers */}
<ImageArtifactViewer /> {/* for image artifacts */}
<PDFArtifactViewer /> {/* for pdf artifacts */}
<ChartArtifactViewer /> {/* for chart artifacts */}
</ChatCanvas>
```
For a complete working example of custom artifact viewers, check out the demo implementation in [`apps/web/app/demo/canvas/custom/page.tsx`](https://github.com/run-llama/chat-ui/blob/main/apps/web/app/demo/canvas/custom/page.tsx). This demo shows:
- Custom `ImageArtifactViewer` implementation
- Integration with existing chat components
- Sample messages with artifact annotations
- Copy-to-clipboard functionality for the code
### Canvas Auto-Show
The canvas automatically appears when artifacts are present:
+2
View File
@@ -6,5 +6,7 @@ module.exports = {
'no-promise-executor-return': 'off',
'@typescript-eslint/no-loop-func': 'off',
'turbo/no-undeclared-env-vars': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-misused-promises': 'off',
},
}
+51 -7
View File
@@ -36,20 +36,64 @@ This is a simple Next.js application demonstrating how to use the `@llamaindex/c
- `app/page.tsx` - Main chat interface
- `app/layout.tsx` - Root layout
- `app/api/chat/route.ts` - Chat API endpoint using LlamaIndex
- `app/rsc/` - React Server Components example with server actions
- `package.json` - Dependencies and scripts
- `next.config.js` - Next.js configuration
- `tsconfig.json` - TypeScript configuration
## How It Works
## Examples
The example uses:
This project includes multiple examples to demonstrate different approaches:
- **@llamaindex/chat-ui** components for the UI
- **LlamaIndex SimpleChatEngine** for AI chat functionality
- **Vercel AI SDK** for streaming responses
- **OpenAI GPT-4o-mini** as the language model
### 1. Default Chat Interface
An OpenAI API key is required for the chat functionality to work.
**URL:** [http://localhost:3000](http://localhost:3000)
The standard implementation using client-side components with the `useChat` hook from Vercel AI SDK
### 2. Rendering Inline Annotations Example
**Enable:** Uncomment the advanced API route in `app/page.tsx`
Demonstrates inline annotations in the chat interface.
### 3. Edge Runtime Example
**Enable:** Uncomment the edge API route in `app/page.tsx`
Showcases running the chat API on Vercel's Edge Runtime for global distribution
### 4. RSC (React Server Components) Example
**URL:** [http://localhost:3000/rsc](http://localhost:3000/rsc)
Demonstrates React Server Components with server actions for chat functionality.
1. **AI Provider Setup** (`ai.ts`):
- Creates an AI provider using `createAI` from Vercel AI SDK
- Defines server state (Message[]) and UI state (Message with ReactNode display)
- Exposes `chatAction` as a server action
2. **Server Actions** (`action.tsx`):
- `chatAction` runs entirely on the server with `'use server'` directive
- Uses `createStreamableUI()` to stream React components to the client
- Processes chat messages and returns React components as responses
- Demonstrates streaming with fake chat data including inline annotations
3. **RSC Chat Hook** (`use-chat-rsc.tsx`):
- Bridges server actions with @llamaindex/chat-ui components
- Uses `useUIState` and `useActions` to manage RSC state
- Provides the same interface as the standard `useChat` hook
- Handles message appending and UI updates seamlessly
4. **Page Component** (`page.tsx`):
- Uses the custom `useChatRSC()` hook instead of `useChat`
- Renders messages with server-generated React components
- Integrates with ChatCanvas for advanced UI features
## Learn More
@@ -0,0 +1,98 @@
/**
* This is an example to demo chat-ui with edge runtime, same functionality as chat/route.ts
* - Text streaming with token-by-token delivery
* - Basic markdown content with code blocks
* - Custom annotations (weather) sent after text completion
* - Standard annotations (sources) sent after text completion
*
*/
import { NextResponse, type NextRequest } from 'next/server'
const TOKEN_DELAY = 30 // 30ms delay between tokens
const TEXT_PREFIX = '0:' // vercel ai text prefix
const ANNOTATION_PREFIX = '8:' // vercel ai annotation prefix
export const runtime = 'edge' // This is the key difference from chat/route.ts
export async function POST(request: NextRequest) {
try {
const { messages } = await request.json()
const lastMessage = messages[messages.length - 1]
const stream = fakeChatStream(`User query: "${lastMessage.content}".\n`)
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'X-Vercel-AI-Data-Stream': 'v1',
Connection: 'keep-alive',
},
})
} catch (error) {
const detail = (error as Error).message
return NextResponse.json({ detail }, { status: 500 })
}
}
const SAMPLE_TEXT = `
Welcome to the demo of @llamaindex/chat-ui. Let me show you the different types of components that can be triggered from the server.
### Markdown with code block
\`\`\`js
const a = 1
const b = 2
const c = a + b
console.log(c)
\`\`\`
### Annotations
`
const SAMPLE_ANNOTATIONS = [
{
type: 'weather',
data: {
location: 'San Francisco, CA',
temperature: 22,
condition: 'sunny',
humidity: 65,
windSpeed: 12,
},
},
{
type: 'sources',
data: {
nodes: [
{ id: '1', url: '/sample.pdf' },
{ id: '2', url: '/sample.pdf' },
],
},
},
]
const fakeChatStream = (query: string): ReadableStream => {
return new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
controller.enqueue(
encoder.encode(`${TEXT_PREFIX}${JSON.stringify(query)}\n`)
)
for (const token of SAMPLE_TEXT.split(' ')) {
await new Promise(resolve => setTimeout(resolve, TOKEN_DELAY))
controller.enqueue(
encoder.encode(`${TEXT_PREFIX}${JSON.stringify(`${token} `)}\n`)
)
}
for (const item of SAMPLE_ANNOTATIONS) {
controller.enqueue(
encoder.encode(`${ANNOTATION_PREFIX}${JSON.stringify([item])}\n`)
)
}
controller.close()
},
})
}
+7 -2
View File
@@ -40,9 +40,14 @@ export default function Page(): JSX.Element {
function ChatExample() {
const handler = useChat({
// api: '/api/chat',
api: '/api/chat',
// uncomment this to try edge runtime example in app/api/chat/edge/route.ts
// api: '/api/chat/edge',
// uncomment this to try advanced example in app/api/chat/advanced/route.ts
api: '/api/chat/advanced',
// api: '/api/chat/advanced',
initialMessages,
})
+194
View File
@@ -0,0 +1,194 @@
'use server'
import { defaultAnnotationRenderers } from '@llamaindex/chat-ui'
import { Markdown } from '@llamaindex/chat-ui/widgets'
import { createStreamableUI } from 'ai/rsc'
import { ReactNode } from 'react'
const TOKEN_DELAY = 30
const ANNOTATION_DELAY = 300
const INLINE_ANNOTATION_KEY = 'annotation'
export async function chatAction(question: string) {
const uiStream = createStreamableUI()
let assistantMsg = ''
const responseStream = fakeChatStream(question)
responseStream
.pipeTo(
new WritableStream({
write: (data: any) => {
if (typeof data === 'string') {
assistantMsg += data
} else {
assistantMsg += toInlineAnnotationCode(data)
}
uiStream.update(
<Markdown
content={assistantMsg}
annotationRenderers={defaultAnnotationRenderers}
/>
)
},
close: () => {
uiStream.done()
},
})
)
.catch(uiStream.error)
return uiStream.value as Promise<ReactNode>
}
const SAMPLE_TEXT = [
`
Welcome to the demo of @llamaindex/chat-ui. Let me show you the different types of components that can be triggered from the server.
### Markdown with code block
\`\`\`js
const a = 1
const b = 2
const c = a + b
console.log(c)
\`\`\`
`,
'\n ### Demo inline annotations \n',
'Here are some steps to create a simple wiki app: \n',
'1. Create package.json file:',
{
type: 'artifact',
data: {
type: 'code',
created_at: 1717334400000,
data: {
file_name: 'package.json',
language: 'json',
code: `{
"name": "wiki-app",
"version": "1.0.0",
"description": "Wiki application",
"main": "wiki.js",
"dependencies": {
"axios": "^1.0.0",
"wiki-api": "^2.1.0"
}
}`,
},
},
},
'2. Check the wiki fetching script:',
{
type: 'artifact',
data: {
created_at: 1717334500000,
type: 'code',
data: {
file_name: 'wiki.js',
language: 'javascript',
code: `async function getWiki(search) {
const response = await fetch("/api/wiki?search=" + search);
const data = await response.json();
return data;
}`,
},
},
},
'3. Run getWiki with the search term:',
{
type: 'artifact',
data: {
created_at: 1717334600000,
type: 'code',
data: {
file_name: 'wiki.js',
language: 'javascript',
code: `getWiki(\`What is \${search}?\`);`,
},
},
},
'#### 🎯 Demo generating a document artifact',
{
type: 'artifact',
data: {
type: 'document',
data: {
title: 'Sample document',
content: `# Getting Started Guide
## Introduction
This comprehensive guide will walk you through everything you need to know to get started with our platform. Whether you're a beginner or an experienced user, you'll find valuable information here.
## Key Features
- **Easy Setup**: Get running in minutes
- **Powerful Tools**: Access advanced capabilities
- **Great Documentation**: Find answers quickly
- **Active Community**: Get help when needed
## Setup Process
1. Install Dependencies
First, ensure you have all required dependencies installed on your system.
2. Configuration
Update your configuration files with the necessary settings:
- API keys
- Environment variables
- User preferences
3. First Steps
Begin with basic operations to familiarize yourself with the platform.
## Best Practices
- Always backup your data
- Follow security guidelines
- Keep your dependencies updated
- Document your changes
## Troubleshooting
If you encounter issues, try these steps:
1. Check logs for errors
2. Verify configurations
3. Update to latest version
4. Contact support if needed
## Additional Resources
- [Documentation](https://docs.example.com)
- [API Reference](https://api.example.com)
- [Community Forums](https://community.example.com)
Feel free to explore and reach out if you need assistance!`,
type: 'markdown',
},
},
},
'\n\n Please feel free to open the document in the canvas and edit it. The document will be saved as a new version',
]
function fakeChatStream(question: string): ReadableStream {
return new ReadableStream({
async start(controller) {
controller.enqueue(`User question: ${question}. \n `)
for (const item of SAMPLE_TEXT) {
if (typeof item === 'string') {
for (const token of item.split(' ')) {
await new Promise(resolve => setTimeout(resolve, TOKEN_DELAY))
controller.enqueue(`${token} `)
}
} else {
await new Promise(resolve => setTimeout(resolve, ANNOTATION_DELAY))
controller.enqueue(item)
}
}
controller.close()
},
})
}
function toInlineAnnotationCode(item: any) {
return `\n\`\`\`${INLINE_ANNOTATION_KEY}\n${JSON.stringify(item)}\n\`\`\`\n`
}
+19
View File
@@ -0,0 +1,19 @@
'use server'
import { createAI } from 'ai/rsc'
import { chatAction } from './action'
import { Message } from 'ai'
import { ReactNode } from 'react'
// define AI state and AI provider for RSC app
export const AI = createAI<
Message[], // server state
(Message & { display: ReactNode })[], // frontend state
{ chatAction: (question: string) => Promise<ReactNode> } // actions
>({
actions: { chatAction },
initialAIState: [],
initialUIState: [],
})
export type AIProvider = typeof AI
+25
View File
@@ -0,0 +1,25 @@
import '../globals.css'
import '@llamaindex/chat-ui/styles/markdown.css'
import '@llamaindex/chat-ui/styles/pdf.css'
import '@llamaindex/chat-ui/styles/editor.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { AI as AIProvider } from './ai'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'LlamaIndex Chat UI - RSC Next.js Example',
description: 'A simple RSC Next.js application using @llamaindex/chat-ui',
}
// for RSC example, we need to wrap the children with the AIProvider
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<AIProvider>{children}</AIProvider>
</body>
</html>
)
}
+81
View File
@@ -0,0 +1,81 @@
'use client'
import {
ChatCanvas,
ChatInput,
ChatMessage,
ChatMessages,
ChatSection,
Message,
useChatUI,
} from '@llamaindex/chat-ui'
import { useChatRSC } from './use-chat-rsc'
import { ReactNode } from 'react'
export default function Page(): JSX.Element {
return (
<div className="flex h-screen flex-col">
<header className="w-full border-b p-4 text-center">
<h1 className="text-2xl font-bold">
LlamaIndex Chat UI - RSC Next.js Example
</h1>
<p className="text-gray-600">
A simple chat interface using @llamaindex/chat-ui and RSC
</p>
</header>
<div className="min-h-0 flex-1">
<ChatExample />
</div>
</div>
)
}
function ChatExample() {
const handler = useChatRSC() // use the RSC hook to create the chat handler
return (
<ChatSection
handler={handler}
className="block h-full flex-row gap-4 p-0 md:flex md:p-5"
>
<div className="md:max-w-1/2 mx-auto flex h-full min-w-0 max-w-full flex-1 flex-col gap-4">
<CustomChatMessages />
<ChatInput />
</div>
<ChatCanvas className="w-full md:w-2/3" />
</ChatSection>
)
}
// custom chat messages to display the UI from the RSC actions
function CustomChatMessages() {
const { messages } = useChatUI()
const frontendMessages = messages.map(message => ({
...message,
display: (
<ChatMessage.Content>
{(message as Message & { display: ReactNode }).display}
</ChatMessage.Content>
),
}))
return (
<ChatMessages>
<ChatMessages.List className="px-4 py-6">
{frontendMessages.map((message, index) => (
<ChatMessage
key={index}
message={message}
isLast={index === messages.length - 1}
>
<ChatMessage.Avatar />
{message.display}
<ChatMessage.Actions />
</ChatMessage>
))}
<ChatMessages.Loading />
</ChatMessages.List>
</ChatMessages>
)
}
+61
View File
@@ -0,0 +1,61 @@
'use client'
import { generateId, Message } from 'ai'
import { useActions, useUIState } from 'ai/rsc'
import { useState } from 'react'
import { AIProvider } from './ai'
import { ChatHandler } from '@llamaindex/chat-ui'
// simple hook to create chat handler from RSC actions
// then we can easily use it with @llamaindex/chat-ui
export function useChatRSC(): ChatHandler {
const [input, setInput] = useState<string>('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const [messages, setMessages] = useUIState<AIProvider>()
const { chatAction } = useActions<AIProvider>()
// similar append function as useChat hook
const append = async (message: Omit<Message, 'id'>) => {
const newMsg: Message = { ...message, id: generateId() }
setIsLoading(true)
try {
setMessages(prev => [
...prev,
{
...newMsg,
display: (
<div className="bg-primary text-primary-foreground ml-auto w-fit max-w-[80%] rounded-xl px-3 py-2">
{message.content}
</div>
),
},
])
const assistantMsg = await chatAction(newMsg.content)
setMessages(prev => [
...prev,
{
id: generateId(),
role: 'assistant',
content: '',
display: assistantMsg,
},
])
} catch (error) {
console.error(error)
}
setIsLoading(false)
setInput('')
return message.content
}
return {
input,
setInput,
isLoading,
append,
messages,
setMessages: setMessages as ChatHandler['setMessages'],
}
}
@@ -1,4 +1,4 @@
import { FileCode, FileText, LucideIcon } from 'lucide-react'
import { FileCode, FileText, LucideIcon, Paperclip } from 'lucide-react'
import { cn } from '../../lib/utils'
import { Badge } from '../../ui/badge'
import { Button } from '../../ui/button'
@@ -15,7 +15,15 @@ const IconMap: Record<Artifact['type'], LucideIcon> = {
document: FileText,
}
export function ArtifactCard({ data }: { data: Artifact }) {
export function ArtifactCard({
data,
getTitle = getCardTitle,
iconMap = IconMap,
}: {
data: Artifact
getTitle?: (data: Artifact) => string
iconMap?: Record<Artifact['type'], LucideIcon>
}) {
const {
openArtifactInCanvas,
getArtifactVersion,
@@ -24,8 +32,8 @@ export function ArtifactCard({ data }: { data: Artifact }) {
} = useChatCanvas()
const { versionNumber, isLatest } = getArtifactVersion(data)
const Icon = IconMap[data.type]
const title = getCardTitle(data)
const Icon = iconMap[data.type] || Paperclip
const title = getTitle(data)
const isDisplayed =
displayedArtifact && isEqualArtifact(data, displayedArtifact)
@@ -41,7 +49,7 @@ export function ArtifactCard({ data }: { data: Artifact }) {
<Icon className="size-7 shrink-0 text-blue-500" />
<div className="flex flex-col">
<div className="text-sm font-semibold">Version {versionNumber}</div>
{title && <div className="text-xs text-gray-600">{title}</div>}
<div className="text-xs text-gray-600">{title}</div>
</div>
</div>
{isLatest ? (
@@ -72,5 +80,5 @@ const getCardTitle = (artifact: Artifact) => {
const { title } = artifact.data as DocumentArtifact['data']
return title
}
return ''
return 'Generated Artifact'
}
+26 -13
View File
@@ -24,19 +24,32 @@ export type CodeArtifactError = {
artifact: CodeArtifact
errors: string[]
}
export type Artifact<T = unknown> = {
/**
* Generic artifact type definition
* @typeParam T - The type of the data payload (e.g., \{ imageUrl: string, caption: string \})
* @typeParam K - The artifact type identifier string (e.g., 'image', 'code', 'document')
*/
export type Artifact<T = unknown, K = string> = {
created_at: number
type: 'code' | 'document'
type: K
data: T
}
export type CodeArtifact = Artifact<{
file_name: string
code: string
language: string
}>
export type DocumentArtifact = Artifact<{
title: string
content: string
type: string
sources?: { id: string }[] // we can add more source info here if needed
}>
export type CodeArtifact = Artifact<
{
file_name: string
code: string
language: string
},
'code'
>
export type DocumentArtifact = Artifact<
{
title: string
content: string
type: string
sources?: { id: string }[] // we can add more source info here if needed
},
'document'
>
+2
View File
@@ -1,3 +1,5 @@
'use client'
// Chat components
export * from './chat/chat.interface'
export * from './chat/annotations'
+2
View File
@@ -1,3 +1,5 @@
'use client'
// Other useful components
export * from './chat-agent-events'
export * from './chat-events'