Skip to content

Commit

Permalink
feat: deploy web app (#52)
Browse files Browse the repository at this point in the history
* refactor: chat-section -> chat

* fake stream text when openapi key not set

* clean before build registry

* update registry link in readme

* enhance styling

* enhance brand styling

* fix: lint
  • Loading branch information
thucpn authored Feb 27, 2025
1 parent 9203f2e commit 4dee8c4
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 119 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Chat UI components for LLM apps
You can quickly add a chatbot to your project by using Shadcn CLI command:

```sh
npx shadcn@latest add https://raw.githubusercontent.com/run-llama/chat-ui/refs/heads/tp/build-registry/apps/web/public/r/chat-section.json
npx shadcn@latest add https://ui.llamaindex.ai/r/chat.json
```

## Manual Installation
Expand Down
1 change: 1 addition & 0 deletions apps/web/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ module.exports = {
'no-await-in-loop': 'off',
'no-promise-executor-return': 'off',
'@typescript-eslint/no-loop-func': 'off',
'turbo/no-undeclared-env-vars': 'off',
},
}
12 changes: 12 additions & 0 deletions apps/web/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SimpleChatEngine,
} from 'llamaindex'
import { NextResponse, type NextRequest } from 'next/server'
import { fakeStreamText } from '@/app/utils'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
Expand All @@ -24,6 +25,17 @@ export async function POST(request: NextRequest) {
const lastMessage = messages[messages.length - 1]

const vercelStreamData = new StreamData()

if (!process.env.OPENAI_API_KEY) {
// Return fake stream if API key is not set
return new Response(fakeStreamText(), {
headers: {
'Content-Type': 'text/plain',
Connection: 'keep-alive',
},
})
}

const chatEngine = new SimpleChatEngine()

const response = await chatEngine.chat({
Expand Down
190 changes: 103 additions & 87 deletions apps/web/app/custom-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import {
ChatMessage,
ChatMessages,
ChatSection,
getCustomAnnotation,
JSONValue,
useChatMessage,
useChatUI,
useFile,
} from '@llamaindex/chat-ui'
Expand All @@ -24,65 +21,90 @@ import {
ChatSection,
Markdown,
useChatUI,
useFile,
} from '@llamaindex/chat-ui'
import { useChat } from 'ai/react'
import { User2 } from 'lucide-react'
export function CustomChat() {
const handler = useChat({ initialMessages })
const { imageUrl, getAnnotations, uploadFile, reset } = useFile({
uploadAPI: '/chat/upload',
})
const annotations = getAnnotations()
const handleUpload = async (file: File) => {
try {
await uploadFile(file)
} catch (error) {
console.error(error)
}
}
return (
<ChatSection handler={handler}>
<ChatMessages className="rounded-xl shadow-xl">
<CustomChatMessagesList />
<ChatMessages.Actions />
</ChatMessages>
<ChatInput className="rounded-xl shadow-xl" />
</ChatSection>
<div className="overflow-hidden rounded-xl shadow-xl">
<ChatSection handler={handler} className="max-h-[72vh]">
<ChatMessages className="rounded-xl shadow-xl">
<CustomChatMessagesList />
<ChatMessages.Actions />
</ChatMessages>
<ChatInput
className="rounded-xl shadow-xl"
annotations={annotations}
resetUploadedFiles={reset}
>
<div>
{imageUrl ? (
<img
className="max-h-[100px] object-contain"
src={imageUrl}
alt="uploaded"
/>
) : null}
</div>
<ChatInput.Form>
<ChatInput.Field />
<ChatInput.Upload onUpload={handleUpload} />
<ChatInput.Submit />
</ChatInput.Form>
</ChatInput>
</ChatSection>
</div>
)
}
function CustomChatMessagesList() {
const { messages } = useChatUI()
const { messages, isLoading, append } = useChatUI()
return (
<ChatMessages.List>
{messages.map((message, index) => (
<ChatMessage key={index} message={message} className="items-start">
<ChatMessage
key={index}
message={message}
isLast={index === messages.length - 1}
className="items-start"
>
<ChatMessage.Avatar>
{message.role === 'user' ? (
<User2 className="h-4 w-4" />
) : (
<img alt="LlamaIndex" src="/llama.png" />
)}
</ChatMessage.Avatar>
<ChatMessage.Content>
<Markdown content={message.content} />
<Annotation annotations={message.annotations} />
<ChatMessage.Content
className="items-start"
isLoading={isLoading}
append={append}
>
<ChatMessage.Content.Image />
<ChatMessage.Content.Markdown />
<ChatMessage.Content.DocumentFile />
</ChatMessage.Content>
<ChatMessage.Actions />
</ChatMessage>
))}
</ChatMessages.List>
)
}
function Annotation({ annotations }: { annotations: any }) {
if (annotations && Array.isArray(annotations)) {
return annotations.map((annotation: any) => {
if (annotation.type === 'image' && annotation.url) {
return (
<img
alt="annotation"
className="h-[200px] object-contain"
key={annotation.url}
src={annotation.url}
/>
)
}
return null
})
}
return null
}
`

const initialMessages: Message[] = [
Expand All @@ -94,11 +116,14 @@ const initialMessages: Message[] = [
{
id: '2',
role: 'assistant',
content: 'Got it! Here is the logo for LlamaIndex',
content:
'Got it! Here is the logo for LlamaIndex. The logo features a friendly llama mascot that represents our AI-powered document indexing and chat capabilities.',
annotations: [
{
type: 'image',
url: '/llama.png',
data: {
url: '/llama.png',
},
},
],
},
Expand All @@ -110,7 +135,8 @@ const initialMessages: Message[] = [
{
id: '4',
role: 'assistant',
content: 'Got it! Here is the pdf file',
content:
'Got it! Here is a sample PDF file that demonstrates PDF handling capabilities. This PDF contains some basic text and formatting examples that you can use to test PDF viewing functionality.',
annotations: [
{
type: 'document_file',
Expand All @@ -128,24 +154,6 @@ const initialMessages: Message[] = [
},
]

function Annotation() {
const { message } = useChatMessage()
const annotations = getCustomAnnotation<{ type: string; url: string }>(
message.annotations as JSONValue[],
a => a.type === 'image'
)

if (!annotations.length) return null
return annotations.map(annotation => (
<img
alt="annotation"
className="h-[200px] object-contain"
key={annotation.url}
src={annotation.url}
/>
))
}

function CustomChatMessagesList() {
const { messages, isLoading, append } = useChatUI()
return (
Expand All @@ -169,9 +177,9 @@ function CustomChatMessagesList() {
isLoading={isLoading}
append={append}
>
<Annotation />
<ChatMessage.Content.DocumentFile />
<ChatMessage.Content.Image />
<ChatMessage.Content.Markdown />
<ChatMessage.Content.DocumentFile />
</ChatMessage.Content>
<ChatMessage.Actions />
</ChatMessage>
Expand All @@ -194,44 +202,52 @@ export function CustomChat() {
}
}
return (
<ChatSection handler={handler}>
<ChatMessages className="rounded-xl shadow-xl">
<CustomChatMessagesList />
<ChatMessages.Actions />
</ChatMessages>
<ChatInput
className="rounded-xl shadow-xl"
annotations={annotations}
resetUploadedFiles={reset}
>
<div>
{imageUrl ? (
<img
className="max-h-[100px] object-contain"
src={imageUrl}
alt="uploaded"
/>
) : null}
</div>
<ChatInput.Form>
<ChatInput.Field />
<ChatInput.Upload onUpload={handleUpload} />
<ChatInput.Submit />
</ChatInput.Form>
</ChatInput>
</ChatSection>
<div className="overflow-hidden rounded-xl shadow-xl">
<ChatSection handler={handler} className="max-h-[72vh]">
<ChatMessages className="rounded-xl shadow-xl">
<CustomChatMessagesList />
<ChatMessages.Actions />
</ChatMessages>
<ChatInput
className="rounded-xl shadow-xl"
annotations={annotations}
resetUploadedFiles={reset}
>
<div>
{imageUrl ? (
<img
className="max-h-[100px] object-contain"
src={imageUrl}
alt="uploaded"
/>
) : null}
</div>
<ChatInput.Form>
<ChatInput.Field />
<ChatInput.Upload onUpload={handleUpload} />
<ChatInput.Submit />
</ChatInput.Form>
</ChatInput>
</ChatSection>
</div>
)
}

export function CustomChatSection() {
return (
<div className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Custom Chat Demo</h2>
<Tabs defaultValue="preview" className="w-[800px]">
<TabsList>
<div className="flex flex-col gap-6">
<div className="space-y-2">
<h2 className="text-2xl font-bold text-white">Custom Chat Demo</h2>
<p className="text-zinc-400">
A more advanced implementation with custom components and file uploads
</p>
</div>
<Tabs defaultValue="preview" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="preview">Preview</TabsTrigger>
<TabsTrigger value="code">Code</TabsTrigger>
</TabsList>

<TabsContent value="preview">
<CustomChat />
</TabsContent>
Expand Down
Loading

0 comments on commit 4dee8c4

Please sign in to comment.