diff --git a/.changeset/nice-garlics-repeat.md b/.changeset/nice-garlics-repeat.md new file mode 100644 index 000000000..2ce66a1b5 --- /dev/null +++ b/.changeset/nice-garlics-repeat.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +feat: use llamaindex chat-ui for nextjs frontend diff --git a/templates/types/streaming/nextjs/app/components/chat-section.tsx b/templates/types/streaming/nextjs/app/components/chat-section.tsx index e7e489ba1..483ca7bb3 100644 --- a/templates/types/streaming/nextjs/app/components/chat-section.tsx +++ b/templates/types/streaming/nextjs/app/components/chat-section.tsx @@ -1,57 +1,26 @@ "use client"; +import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui"; +import "@llamaindex/chat-ui/styles/code.css"; +import "@llamaindex/chat-ui/styles/katex.css"; import { useChat } from "ai/react"; -import { useState } from "react"; -import { ChatInput, ChatMessages } from "./ui/chat"; +import CustomChatInput from "./ui/chat/chat-input"; +import CustomChatMessages from "./ui/chat/chat-messages"; import { useClientConfig } from "./ui/chat/hooks/use-config"; export default function ChatSection() { const { backend } = useClientConfig(); - const [requestData, setRequestData] = useState(); - const { - messages, - input, - isLoading, - handleSubmit, - handleInputChange, - reload, - stop, - append, - setInput, - } = useChat({ - body: { data: requestData }, + const handler = useChat({ api: `${backend}/api/chat`, - headers: { - "Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26 - }, onError: (error: unknown) => { if (!(error instanceof Error)) throw error; - const message = JSON.parse(error.message); - alert(message.detail); + alert(JSON.parse(error.message).detail); }, - sendExtraMessageFields: true, }); - return ( -
- - -
+ + + + ); } diff --git a/templates/types/streaming/nextjs/app/components/ui/README-template.md b/templates/types/streaming/nextjs/app/components/ui/README-template.md deleted file mode 100644 index ebfcf48c9..000000000 --- a/templates/types/streaming/nextjs/app/components/ui/README-template.md +++ /dev/null @@ -1 +0,0 @@ -Using the chat component from https://github.com/marcusschiesser/ui (based on https://ui.shadcn.com/) diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx deleted file mode 100644 index 151ef61a9..000000000 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { PauseCircle, RefreshCw } from "lucide-react"; - -import { Button } from "../button"; -import { ChatHandler } from "./chat.interface"; - -export default function ChatActions( - props: Pick & { - showReload?: boolean; - showStop?: boolean; - }, -) { - return ( -
- {props.showStop && ( - - )} - {props.showReload && ( - - )} -
- ); -} diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-avatar.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx similarity index 78% rename from templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-avatar.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx index ce04e306a..cfa307cbf 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-avatar.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx @@ -1,8 +1,10 @@ +import { useChatMessage } from "@llamaindex/chat-ui"; import { User2 } from "lucide-react"; import Image from "next/image"; -export default function ChatAvatar({ role }: { role: string }) { - if (role === "user") { +export function ChatMessageAvatar() { + const { message } = useChatMessage(); + if (message.role === "user") { return (
diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-input.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-input.tsx index 0e5c318b3..8800bfef7 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-input.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-input.tsx @@ -1,34 +1,13 @@ -import { JSONValue } from "ai"; -import React from "react"; -import { DocumentFile } from "."; -import { Button } from "../button"; -import { DocumentPreview } from "../document-preview"; -import FileUploader from "../file-uploader"; -import { Textarea } from "../textarea"; -import UploadImagePreview from "../upload-image-preview"; -import { ChatHandler } from "./chat.interface"; -import { useFile } from "./hooks/use-file"; -import { LlamaCloudSelector } from "./widgets/LlamaCloudSelector"; +"use client"; -const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"]; +import { ChatInput, useChatUI, useFile } from "@llamaindex/chat-ui"; +import { DocumentPreview, ImagePreview } from "@llamaindex/chat-ui/widgets"; +import { LlamaCloudSelector } from "./custom/llama-cloud-selector"; +import { useClientConfig } from "./hooks/use-config"; -export default function ChatInput( - props: Pick< - ChatHandler, - | "isLoading" - | "input" - | "onFileUpload" - | "onFileError" - | "handleSubmit" - | "handleInputChange" - | "messages" - | "setInput" - | "append" - > & { - requestParams?: any; - setRequestData?: React.Dispatch; - }, -) { +export default function CustomChatInput() { + const { requestData, isLoading, input } = useChatUI(); + const { backend } = useClientConfig(); const { imageUrl, setImageUrl, @@ -37,107 +16,65 @@ export default function ChatInput( removeDoc, reset, getAnnotations, - } = useFile(); - - // default submit function does not handle including annotations in the message - // so we need to use append function to submit new message with annotations - const handleSubmitWithAnnotations = ( - e: React.FormEvent, - annotations: JSONValue[] | undefined, - ) => { - e.preventDefault(); - props.append!({ - content: props.input, - role: "user", - createdAt: new Date(), - annotations, - }); - props.setInput!(""); - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const annotations = getAnnotations(); - if (annotations.length) { - handleSubmitWithAnnotations(e, annotations); - return reset(); - } - props.handleSubmit(e); - }; + } = useFile({ uploadAPI: `${backend}/api/chat/upload` }); + /** + * Handles file uploads. Overwrite to hook into the file upload behavior. + * @param file The file to upload + */ const handleUploadFile = async (file: File) => { + // There's already an image uploaded, only allow one image at a time if (imageUrl) { alert("You can only upload one image at a time."); return; } + try { - await uploadFile(file, props.requestParams); - props.onFileUpload?.(file); + // Upload the file and send with it the current request data + await uploadFile(file, requestData); } catch (error: any) { - const onFileUploadError = props.onFileError || window.alert; - onFileUploadError(error.message); + // Show error message if upload fails + alert(error.message); } }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSubmit(e as unknown as React.FormEvent); - } - }; + // 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 + const annotations = getAnnotations(); return ( -
- {imageUrl && ( - setImageUrl(null)} /> - )} - {files.length > 0 && ( -
- {files.map((file: DocumentFile) => ( - removeDoc(file)} - /> - ))} -
- )} -
-