mirror of
https://github.com/run-llama/chat-ui.git
synced 2026-07-21 11:25:22 -04:00
docs: add more chatui examples (#128)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'web': patch
|
||||
'@llamaindex/chat-ui': patch
|
||||
'@llamaindex/chat-ui-docs': patch
|
||||
---
|
||||
|
||||
docs: add more chatui examples
|
||||
@@ -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 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
// Chat components
|
||||
export * from './chat/chat.interface'
|
||||
export * from './chat/annotations'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
// Other useful components
|
||||
export * from './chat-agent-events'
|
||||
export * from './chat-events'
|
||||
|
||||
Reference in New Issue
Block a user