Skip to content

Commit 78ccde7

Browse files
feat: integrate llamaindex chat-ui (#399)
--------- Co-authored-by: Marcus Schiesser <[email protected]>
1 parent 0251070 commit 78ccde7

34 files changed

+290
-2019
lines changed

.changeset/nice-garlics-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-llama": patch
3+
---
4+
5+
feat: use llamaindex chat-ui for nextjs frontend
Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,26 @@
11
"use client";
22

3+
import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
4+
import "@llamaindex/chat-ui/styles/code.css";
5+
import "@llamaindex/chat-ui/styles/katex.css";
36
import { useChat } from "ai/react";
4-
import { useState } from "react";
5-
import { ChatInput, ChatMessages } from "./ui/chat";
7+
import CustomChatInput from "./ui/chat/chat-input";
8+
import CustomChatMessages from "./ui/chat/chat-messages";
69
import { useClientConfig } from "./ui/chat/hooks/use-config";
710

811
export default function ChatSection() {
912
const { backend } = useClientConfig();
10-
const [requestData, setRequestData] = useState<any>();
11-
const {
12-
messages,
13-
input,
14-
isLoading,
15-
handleSubmit,
16-
handleInputChange,
17-
reload,
18-
stop,
19-
append,
20-
setInput,
21-
} = useChat({
22-
body: { data: requestData },
13+
const handler = useChat({
2314
api: `${backend}/api/chat`,
24-
headers: {
25-
"Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
26-
},
2715
onError: (error: unknown) => {
2816
if (!(error instanceof Error)) throw error;
29-
const message = JSON.parse(error.message);
30-
alert(message.detail);
17+
alert(JSON.parse(error.message).detail);
3118
},
32-
sendExtraMessageFields: true,
3319
});
34-
3520
return (
36-
<div className="space-y-4 w-full h-full flex flex-col">
37-
<ChatMessages
38-
messages={messages}
39-
isLoading={isLoading}
40-
reload={reload}
41-
stop={stop}
42-
append={append}
43-
/>
44-
<ChatInput
45-
input={input}
46-
handleSubmit={handleSubmit}
47-
handleInputChange={handleInputChange}
48-
isLoading={isLoading}
49-
messages={messages}
50-
append={append}
51-
setInput={setInput}
52-
requestParams={{ params: requestData }}
53-
setRequestData={setRequestData}
54-
/>
55-
</div>
21+
<ChatSectionUI handler={handler} className="w-full h-full">
22+
<CustomChatMessages />
23+
<CustomChatInput />
24+
</ChatSectionUI>
5625
);
5726
}

templates/types/streaming/nextjs/app/components/ui/README-template.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-avatar.tsx renamed to templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { useChatMessage } from "@llamaindex/chat-ui";
12
import { User2 } from "lucide-react";
23
import Image from "next/image";
34

4-
export default function ChatAvatar({ role }: { role: string }) {
5-
if (role === "user") {
5+
export function ChatMessageAvatar() {
6+
const { message } = useChatMessage();
7+
if (message.role === "user") {
68
return (
79
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow">
810
<User2 className="h-4 w-4" />
Lines changed: 52 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,13 @@
1-
import { JSONValue } from "ai";
2-
import React from "react";
3-
import { DocumentFile } from ".";
4-
import { Button } from "../button";
5-
import { DocumentPreview } from "../document-preview";
6-
import FileUploader from "../file-uploader";
7-
import { Textarea } from "../textarea";
8-
import UploadImagePreview from "../upload-image-preview";
9-
import { ChatHandler } from "./chat.interface";
10-
import { useFile } from "./hooks/use-file";
11-
import { LlamaCloudSelector } from "./widgets/LlamaCloudSelector";
1+
"use client";
122

13-
const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"];
3+
import { ChatInput, useChatUI, useFile } from "@llamaindex/chat-ui";
4+
import { DocumentPreview, ImagePreview } from "@llamaindex/chat-ui/widgets";
5+
import { LlamaCloudSelector } from "./custom/llama-cloud-selector";
6+
import { useClientConfig } from "./hooks/use-config";
147

15-
export default function ChatInput(
16-
props: Pick<
17-
ChatHandler,
18-
| "isLoading"
19-
| "input"
20-
| "onFileUpload"
21-
| "onFileError"
22-
| "handleSubmit"
23-
| "handleInputChange"
24-
| "messages"
25-
| "setInput"
26-
| "append"
27-
> & {
28-
requestParams?: any;
29-
setRequestData?: React.Dispatch<any>;
30-
},
31-
) {
8+
export default function CustomChatInput() {
9+
const { requestData, isLoading, input } = useChatUI();
10+
const { backend } = useClientConfig();
3211
const {
3312
imageUrl,
3413
setImageUrl,
@@ -37,107 +16,65 @@ export default function ChatInput(
3716
removeDoc,
3817
reset,
3918
getAnnotations,
40-
} = useFile();
41-
42-
// default submit function does not handle including annotations in the message
43-
// so we need to use append function to submit new message with annotations
44-
const handleSubmitWithAnnotations = (
45-
e: React.FormEvent<HTMLFormElement>,
46-
annotations: JSONValue[] | undefined,
47-
) => {
48-
e.preventDefault();
49-
props.append!({
50-
content: props.input,
51-
role: "user",
52-
createdAt: new Date(),
53-
annotations,
54-
});
55-
props.setInput!("");
56-
};
57-
58-
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
59-
e.preventDefault();
60-
const annotations = getAnnotations();
61-
if (annotations.length) {
62-
handleSubmitWithAnnotations(e, annotations);
63-
return reset();
64-
}
65-
props.handleSubmit(e);
66-
};
19+
} = useFile({ uploadAPI: `${backend}/api/chat/upload` });
6720

21+
/**
22+
* Handles file uploads. Overwrite to hook into the file upload behavior.
23+
* @param file The file to upload
24+
*/
6825
const handleUploadFile = async (file: File) => {
26+
// There's already an image uploaded, only allow one image at a time
6927
if (imageUrl) {
7028
alert("You can only upload one image at a time.");
7129
return;
7230
}
31+
7332
try {
74-
await uploadFile(file, props.requestParams);
75-
props.onFileUpload?.(file);
33+
// Upload the file and send with it the current request data
34+
await uploadFile(file, requestData);
7635
} catch (error: any) {
77-
const onFileUploadError = props.onFileError || window.alert;
78-
onFileUploadError(error.message);
36+
// Show error message if upload fails
37+
alert(error.message);
7938
}
8039
};
8140

82-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
83-
if (e.key === "Enter" && !e.shiftKey) {
84-
e.preventDefault();
85-
onSubmit(e as unknown as React.FormEvent<HTMLFormElement>);
86-
}
87-
};
41+
// Get references to the upload files in message annotations format, see https://github.com/run-llama/chat-ui/blob/main/packages/chat-ui/src/hook/use-file.tsx#L56
42+
const annotations = getAnnotations();
8843

8944
return (
90-
<form
91-
onSubmit={onSubmit}
92-
className="rounded-xl bg-white p-4 shadow-xl space-y-4 shrink-0"
45+
<ChatInput
46+
className="shadow-xl rounded-xl"
47+
resetUploadedFiles={reset}
48+
annotations={annotations}
9349
>
94-
{imageUrl && (
95-
<UploadImagePreview url={imageUrl} onRemove={() => setImageUrl(null)} />
96-
)}
97-
{files.length > 0 && (
98-
<div className="flex gap-4 w-full overflow-auto py-2">
99-
{files.map((file: DocumentFile) => (
100-
<DocumentPreview
101-
key={file.id}
102-
file={file}
103-
onRemove={() => removeDoc(file)}
104-
/>
105-
))}
106-
</div>
107-
)}
108-
<div className="flex w-full items-start justify-between gap-4 ">
109-
<Textarea
110-
id="chat-input"
111-
autoFocus
112-
name="message"
113-
placeholder="Type a message"
114-
className="flex-1 min-h-0 h-[40px]"
115-
value={props.input}
116-
onChange={props.handleInputChange}
117-
onKeyDown={handleKeyDown}
118-
/>
119-
<FileUploader
120-
onFileUpload={handleUploadFile}
121-
onFileError={props.onFileError}
122-
config={{
123-
allowedExtensions: ALLOWED_EXTENSIONS,
124-
disabled: props.isLoading,
125-
multiple: true,
126-
}}
127-
/>
128-
{process.env.NEXT_PUBLIC_USE_LLAMACLOUD === "true" &&
129-
props.setRequestData && (
130-
<LlamaCloudSelector setRequestData={props.setRequestData} />
131-
)}
132-
<Button
133-
type="submit"
50+
<div>
51+
{/* Image preview section */}
52+
{imageUrl && (
53+
<ImagePreview url={imageUrl} onRemove={() => setImageUrl(null)} />
54+
)}
55+
{/* Document previews section */}
56+
{files.length > 0 && (
57+
<div className="flex gap-4 w-full overflow-auto py-2">
58+
{files.map((file) => (
59+
<DocumentPreview
60+
key={file.id}
61+
file={file}
62+
onRemove={() => removeDoc(file)}
63+
/>
64+
))}
65+
</div>
66+
)}
67+
</div>
68+
<ChatInput.Form>
69+
<ChatInput.Field />
70+
<ChatInput.Upload onUpload={handleUploadFile} />
71+
<LlamaCloudSelector />
72+
<ChatInput.Submit
13473
disabled={
135-
props.isLoading || (!props.input.trim() && files.length === 0)
74+
isLoading || (!input.trim() && files.length === 0 && !imageUrl)
13675
}
137-
>
138-
Send message
139-
</Button>
140-
</div>
141-
</form>
76+
/>
77+
</ChatInput.Form>
78+
</ChatInput>
14279
);
14380
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
ChatMessage,
3+
ContentPosition,
4+
getSourceAnnotationData,
5+
useChatMessage,
6+
} from "@llamaindex/chat-ui";
7+
import { Markdown } from "./custom/markdown";
8+
import { ToolAnnotations } from "./tools/chat-tools";
9+
10+
export function ChatMessageContent() {
11+
const { message } = useChatMessage();
12+
const customContent = [
13+
{
14+
// override the default markdown component
15+
position: ContentPosition.MARKDOWN,
16+
component: (
17+
<Markdown
18+
content={message.content}
19+
sources={getSourceAnnotationData(message.annotations)?.[0]}
20+
/>
21+
),
22+
},
23+
{
24+
// add the tool annotations after events
25+
position: ContentPosition.AFTER_EVENTS,
26+
component: <ToolAnnotations message={message} />,
27+
},
28+
];
29+
return <ChatMessage.Content content={customContent} />;
30+
}

0 commit comments

Comments
 (0)