From a9930be700ebc1a84f379402058ca04422a316f9 Mon Sep 17 00:00:00 2001 From: Marcus Schiesser <mail@marcusschiesser.de> Date: Tue, 5 Dec 2023 11:33:32 +0700 Subject: [PATCH] refactor: swapped html and shadcn --- .../components/ui/html/chat/chat-avatar.tsx | 34 ++++++ .../components/ui/html/chat/chat-input.tsx | 43 ++++++++ .../ui/html}/chat/chat-item.tsx | 0 .../components/ui/html/chat/chat-messages.tsx | 48 ++++++++ .../ui/{shadcn => html}/chat/index.ts | 3 +- .../components/ui/shadcn/chat/chat-avatar.tsx | 25 ----- .../components/ui/shadcn/chat/chat-input.tsx | 84 -------------- .../ui/shadcn/chat/chat-messages.tsx | 64 ----------- templates/index.ts | 30 ++--- .../app/components/ui}/README-template.md | 0 .../nextjs/app/components/ui}/button.tsx | 0 .../app/components/ui}/chat/chat-actions.tsx | 0 .../app/components/ui/chat/chat-avatar.tsx | 21 +--- .../app/components/ui/chat/chat-input.tsx | 103 ++++++++++++------ .../app/components/ui}/chat/chat-message.tsx | 0 .../app/components/ui/chat/chat-messages.tsx | 62 +++++++---- .../app/components/ui}/chat/chat.interface.ts | 0 .../app/components/ui}/chat/codeblock.tsx | 0 .../nextjs/app/components/ui/chat/index.ts | 3 +- .../app/components/ui}/chat/markdown.tsx | 0 .../ui}/chat/use-copy-to-clipboard.tsx | 0 .../app/components/ui}/file-uploader.tsx | 0 .../nextjs/app/components/ui}/input.tsx | 0 .../nextjs/app/components/ui}/lib/utils.ts | 0 .../components/ui}/upload-image-preview.tsx | 0 templates/types/streaming/nextjs/package.json | 19 +++- 26 files changed, 275 insertions(+), 264 deletions(-) create mode 100644 templates/components/ui/html/chat/chat-avatar.tsx create mode 100644 templates/components/ui/html/chat/chat-input.tsx rename templates/{types/streaming/nextjs/app/components/ui => components/ui/html}/chat/chat-item.tsx (100%) create mode 100644 templates/components/ui/html/chat/chat-messages.tsx rename templates/components/ui/{shadcn => html}/chat/index.ts (54%) delete mode 100644 templates/components/ui/shadcn/chat/chat-avatar.tsx delete mode 100644 templates/components/ui/shadcn/chat/chat-input.tsx delete mode 100644 templates/components/ui/shadcn/chat/chat-messages.tsx rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/README-template.md (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/button.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/chat-actions.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/chat-message.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/chat.interface.ts (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/codeblock.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/markdown.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/chat/use-copy-to-clipboard.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/file-uploader.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/input.tsx (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/lib/utils.ts (100%) rename templates/{components/ui/shadcn => types/streaming/nextjs/app/components/ui}/upload-image-preview.tsx (100%) diff --git a/templates/components/ui/html/chat/chat-avatar.tsx b/templates/components/ui/html/chat/chat-avatar.tsx new file mode 100644 index 00000000..cd241104 --- /dev/null +++ b/templates/components/ui/html/chat/chat-avatar.tsx @@ -0,0 +1,34 @@ +"use client"; + +import Image from "next/image"; +import { Message } from "./chat-messages"; + +export default function ChatAvatar(message: Message) { + if (message.role === "user") { + return ( + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 256 256" + fill="currentColor" + className="h-4 w-4" + > + <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z"></path> + </svg> + </div> + ); + } + + return ( + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white"> + <Image + className="rounded-md" + src="/llama.png" + alt="Llama Logo" + width={24} + height={24} + priority + /> + </div> + ); +} diff --git a/templates/components/ui/html/chat/chat-input.tsx b/templates/components/ui/html/chat/chat-input.tsx new file mode 100644 index 00000000..7c3e8728 --- /dev/null +++ b/templates/components/ui/html/chat/chat-input.tsx @@ -0,0 +1,43 @@ +"use client"; + +export interface ChatInputProps { + /** The current value of the input */ + input?: string; + /** An input/textarea-ready onChange handler to control the value of the input */ + handleInputChange?: ( + e: + | React.ChangeEvent<HTMLInputElement> + | React.ChangeEvent<HTMLTextAreaElement>, + ) => void; + /** Form submission handler to automatically reset input and append a user message */ + handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; + isLoading: boolean; + multiModal?: boolean; +} + +export default function ChatInput(props: ChatInputProps) { + return ( + <> + <form + onSubmit={props.handleSubmit} + className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4" + > + <input + autoFocus + name="message" + placeholder="Type a message" + className="w-full p-4 rounded-xl shadow-inner flex-1" + value={props.input} + onChange={props.handleInputChange} + /> + <button + disabled={props.isLoading} + type="submit" + className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed" + > + Send message + </button> + </form> + </> + ); +} diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-item.tsx b/templates/components/ui/html/chat/chat-item.tsx similarity index 100% rename from templates/types/streaming/nextjs/app/components/ui/chat/chat-item.tsx rename to templates/components/ui/html/chat/chat-item.tsx diff --git a/templates/components/ui/html/chat/chat-messages.tsx b/templates/components/ui/html/chat/chat-messages.tsx new file mode 100644 index 00000000..0e978394 --- /dev/null +++ b/templates/components/ui/html/chat/chat-messages.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import ChatItem from "./chat-item"; + +export interface Message { + id: string; + content: string; + role: string; +} + +export default function ChatMessages({ + messages, + isLoading, + reload, + stop, +}: { + messages: Message[]; + isLoading?: boolean; + stop?: () => void; + reload?: () => void; +}) { + const scrollableChatContainerRef = useRef<HTMLDivElement>(null); + + const scrollToBottom = () => { + if (scrollableChatContainerRef.current) { + scrollableChatContainerRef.current.scrollTop = + scrollableChatContainerRef.current.scrollHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [messages.length]); + + return ( + <div className="w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl"> + <div + className="flex flex-col gap-5 divide-y h-[50vh] overflow-auto" + ref={scrollableChatContainerRef} + > + {messages.map((m: Message) => ( + <ChatItem key={m.id} {...m} /> + ))} + </div> + </div> + ); +} diff --git a/templates/components/ui/shadcn/chat/index.ts b/templates/components/ui/html/chat/index.ts similarity index 54% rename from templates/components/ui/shadcn/chat/index.ts rename to templates/components/ui/html/chat/index.ts index c7990f9c..5de7dce4 100644 --- a/templates/components/ui/shadcn/chat/index.ts +++ b/templates/components/ui/html/chat/index.ts @@ -1,5 +1,6 @@ import ChatInput from "./chat-input"; import ChatMessages from "./chat-messages"; -export { type ChatHandler, type Message } from "./chat.interface"; +export type { ChatInputProps } from "./chat-input"; +export type { Message } from "./chat-messages"; export { ChatInput, ChatMessages }; diff --git a/templates/components/ui/shadcn/chat/chat-avatar.tsx b/templates/components/ui/shadcn/chat/chat-avatar.tsx deleted file mode 100644 index ce04e306..00000000 --- a/templates/components/ui/shadcn/chat/chat-avatar.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { User2 } from "lucide-react"; -import Image from "next/image"; - -export default function ChatAvatar({ role }: { role: string }) { - if (role === "user") { - return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow"> - <User2 className="h-4 w-4" /> - </div> - ); - } - - return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow"> - <Image - className="rounded-md" - src="/llama.png" - alt="Llama Logo" - width={24} - height={24} - priority - /> - </div> - ); -} diff --git a/templates/components/ui/shadcn/chat/chat-input.tsx b/templates/components/ui/shadcn/chat/chat-input.tsx deleted file mode 100644 index 435637e5..00000000 --- a/templates/components/ui/shadcn/chat/chat-input.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from "react"; -import { Button } from "../button"; -import FileUploader from "../file-uploader"; -import { Input } from "../input"; -import UploadImagePreview from "../upload-image-preview"; -import { ChatHandler } from "./chat.interface"; - -export default function ChatInput( - props: Pick< - ChatHandler, - | "isLoading" - | "input" - | "onFileUpload" - | "onFileError" - | "handleSubmit" - | "handleInputChange" - > & { - multiModal?: boolean; - }, -) { - const [imageUrl, setImageUrl] = useState<string | null>(null); - - const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { - if (imageUrl) { - props.handleSubmit(e, { - data: { imageUrl: imageUrl }, - }); - setImageUrl(null); - return; - } - props.handleSubmit(e); - }; - - const onRemovePreviewImage = () => setImageUrl(null); - - const handleUploadImageFile = async (file: File) => { - const base64 = await new Promise<string>((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result as string); - reader.onerror = (error) => reject(error); - }); - setImageUrl(base64); - }; - - const handleUploadFile = async (file: File) => { - try { - if (props.multiModal && file.type.startsWith("image/")) { - return await handleUploadImageFile(file); - } - props.onFileUpload?.(file); - } catch (error: any) { - props.onFileError?.(error.message); - } - }; - - return ( - <form - onSubmit={onSubmit} - className="rounded-xl bg-white p-4 shadow-xl space-y-4" - > - {imageUrl && ( - <UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} /> - )} - <div className="flex w-full items-start justify-between gap-4 "> - <Input - autoFocus - name="message" - placeholder="Type a message" - className="flex-1" - value={props.input} - onChange={props.handleInputChange} - /> - <FileUploader - onFileUpload={handleUploadFile} - onFileError={props.onFileError} - /> - <Button type="submit" disabled={props.isLoading}> - Send message - </Button> - </div> - </form> - ); -} diff --git a/templates/components/ui/shadcn/chat/chat-messages.tsx b/templates/components/ui/shadcn/chat/chat-messages.tsx deleted file mode 100644 index ee40cfd6..00000000 --- a/templates/components/ui/shadcn/chat/chat-messages.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useRef } from "react"; -import { Loader2 } from "lucide-react"; - -import ChatActions from "./chat-actions"; -import ChatMessage from "./chat-message"; -import { ChatHandler } from "./chat.interface"; - -export default function ChatMessages( - props: Pick<ChatHandler, "messages" | "isLoading" | "reload" | "stop">, -) { - const scrollableChatContainerRef = useRef<HTMLDivElement>(null); - const messageLength = props.messages.length; - const lastMessage = props.messages[messageLength - 1]; - - const scrollToBottom = () => { - if (scrollableChatContainerRef.current) { - scrollableChatContainerRef.current.scrollTop = - scrollableChatContainerRef.current.scrollHeight; - } - }; - - const isLastMessageFromAssistant = - messageLength > 0 && lastMessage?.role !== "user"; - const showReload = - props.reload && !props.isLoading && isLastMessageFromAssistant; - const showStop = props.stop && props.isLoading; - - // `isPending` indicate - // that stream response is not yet received from the server, - // so we show a loading indicator to give a better UX. - const isPending = props.isLoading && !isLastMessageFromAssistant; - - useEffect(() => { - scrollToBottom(); - }, [messageLength, lastMessage]); - - return ( - <div className="w-full rounded-xl bg-white p-4 shadow-xl pb-0"> - <div - className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4" - ref={scrollableChatContainerRef} - > - {props.messages.map((m) => ( - <ChatMessage key={m.id} {...m} /> - ))} - {isPending && ( - <div - className='flex justify-center items-center pt-10' - > - <Loader2 className="h-4 w-4 animate-spin"/> - </div> - )} - </div> - <div className="flex justify-end py-4"> - <ChatActions - reload={props.reload} - stop={props.stop} - showReload={showReload} - showStop={showStop} - /> - </div> - </div> - ); -} diff --git a/templates/index.ts b/templates/index.ts index 6063e411..cd675448 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -162,7 +162,7 @@ const installTSTemplate = async ({ /** * Copy the selected UI files to the target directory and reference it. */ - if (framework === "nextjs" && ui !== "html") { + if (framework === "nextjs" && ui !== "shadcn") { console.log("\nUsing UI:", ui, "\n"); const uiPath = path.join(compPath, "ui", ui); const destUiPath = path.join(root, "app", "components", "ui"); @@ -227,26 +227,26 @@ const installTSTemplate = async ({ }; } - if (framework === "nextjs" && ui === "shadcn") { - // add shadcn dependencies to package.json + if (framework === "nextjs" && ui === "html") { + // remove shadcn dependencies if html ui is selected packageJson.dependencies = { ...packageJson.dependencies, - "tailwind-merge": "^2", - "@radix-ui/react-slot": "^1", - "class-variance-authority": "^0.7", - clsx: "^1.2.1", - "lucide-react": "^0.291", - remark: "^14.0.3", - "remark-code-import": "^1.2.0", - "remark-gfm": "^3.0.1", - "remark-math": "^5.1.1", - "react-markdown": "^8.0.7", - "react-syntax-highlighter": "^15.5.0", + "tailwind-merge": undefined, + "@radix-ui/react-slot": undefined, + "class-variance-authority": undefined, + clsx: undefined, + "lucide-react": undefined, + remark: undefined, + "remark-code-import": undefined, + "remark-gfm": undefined, + "remark-math": undefined, + "react-markdown": undefined, + "react-syntax-highlighter": undefined, }; packageJson.devDependencies = { ...packageJson.devDependencies, - "@types/react-syntax-highlighter": "^15.5.6", + "@types/react-syntax-highlighter": undefined, }; } diff --git a/templates/components/ui/shadcn/README-template.md b/templates/types/streaming/nextjs/app/components/ui/README-template.md similarity index 100% rename from templates/components/ui/shadcn/README-template.md rename to templates/types/streaming/nextjs/app/components/ui/README-template.md diff --git a/templates/components/ui/shadcn/button.tsx b/templates/types/streaming/nextjs/app/components/ui/button.tsx similarity index 100% rename from templates/components/ui/shadcn/button.tsx rename to templates/types/streaming/nextjs/app/components/ui/button.tsx diff --git a/templates/components/ui/shadcn/chat/chat-actions.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx similarity index 100% rename from templates/components/ui/shadcn/chat/chat-actions.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/chat-actions.tsx diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx index cd241104..ce04e306 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-avatar.tsx @@ -1,26 +1,17 @@ -"use client"; - +import { User2 } from "lucide-react"; import Image from "next/image"; -import { Message } from "./chat-messages"; -export default function ChatAvatar(message: Message) { - if (message.role === "user") { +export default function ChatAvatar({ role }: { role: string }) { + if (role === "user") { return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 256 256" - fill="currentColor" - className="h-4 w-4" - > - <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z"></path> - </svg> + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow"> + <User2 className="h-4 w-4" /> </div> ); } return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white"> + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow"> <Image className="rounded-md" src="/llama.png" 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 7c3e8728..435637e5 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,43 +1,84 @@ -"use client"; +import { useState } from "react"; +import { Button } from "../button"; +import FileUploader from "../file-uploader"; +import { Input } from "../input"; +import UploadImagePreview from "../upload-image-preview"; +import { ChatHandler } from "./chat.interface"; -export interface ChatInputProps { - /** The current value of the input */ - input?: string; - /** An input/textarea-ready onChange handler to control the value of the input */ - handleInputChange?: ( - e: - | React.ChangeEvent<HTMLInputElement> - | React.ChangeEvent<HTMLTextAreaElement>, - ) => void; - /** Form submission handler to automatically reset input and append a user message */ - handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; - isLoading: boolean; - multiModal?: boolean; -} +export default function ChatInput( + props: Pick< + ChatHandler, + | "isLoading" + | "input" + | "onFileUpload" + | "onFileError" + | "handleSubmit" + | "handleInputChange" + > & { + multiModal?: boolean; + }, +) { + const [imageUrl, setImageUrl] = useState<string | null>(null); + + const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { + if (imageUrl) { + props.handleSubmit(e, { + data: { imageUrl: imageUrl }, + }); + setImageUrl(null); + return; + } + props.handleSubmit(e); + }; + + const onRemovePreviewImage = () => setImageUrl(null); + + const handleUploadImageFile = async (file: File) => { + const base64 = await new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + setImageUrl(base64); + }; + + const handleUploadFile = async (file: File) => { + try { + if (props.multiModal && file.type.startsWith("image/")) { + return await handleUploadImageFile(file); + } + props.onFileUpload?.(file); + } catch (error: any) { + props.onFileError?.(error.message); + } + }; -export default function ChatInput(props: ChatInputProps) { return ( - <> - <form - onSubmit={props.handleSubmit} - className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4" - > - <input + <form + onSubmit={onSubmit} + className="rounded-xl bg-white p-4 shadow-xl space-y-4" + > + {imageUrl && ( + <UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} /> + )} + <div className="flex w-full items-start justify-between gap-4 "> + <Input autoFocus name="message" placeholder="Type a message" - className="w-full p-4 rounded-xl shadow-inner flex-1" + className="flex-1" value={props.input} onChange={props.handleInputChange} /> - <button - disabled={props.isLoading} - type="submit" - className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed" - > + <FileUploader + onFileUpload={handleUploadFile} + onFileError={props.onFileError} + /> + <Button type="submit" disabled={props.isLoading}> Send message - </button> - </form> - </> + </Button> + </div> + </form> ); } diff --git a/templates/components/ui/shadcn/chat/chat-message.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-message.tsx similarity index 100% rename from templates/components/ui/shadcn/chat/chat-message.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/chat-message.tsx diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-messages.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-messages.tsx index 0e978394..abc3e52d 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-messages.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-messages.tsx @@ -1,26 +1,16 @@ -"use client"; - +import { Loader2 } from "lucide-react"; import { useEffect, useRef } from "react"; -import ChatItem from "./chat-item"; -export interface Message { - id: string; - content: string; - role: string; -} +import ChatActions from "./chat-actions"; +import ChatMessage from "./chat-message"; +import { ChatHandler } from "./chat.interface"; -export default function ChatMessages({ - messages, - isLoading, - reload, - stop, -}: { - messages: Message[]; - isLoading?: boolean; - stop?: () => void; - reload?: () => void; -}) { +export default function ChatMessages( + props: Pick<ChatHandler, "messages" | "isLoading" | "reload" | "stop">, +) { const scrollableChatContainerRef = useRef<HTMLDivElement>(null); + const messageLength = props.messages.length; + const lastMessage = props.messages[messageLength - 1]; const scrollToBottom = () => { if (scrollableChatContainerRef.current) { @@ -29,19 +19,43 @@ export default function ChatMessages({ } }; + const isLastMessageFromAssistant = + messageLength > 0 && lastMessage?.role !== "user"; + const showReload = + props.reload && !props.isLoading && isLastMessageFromAssistant; + const showStop = props.stop && props.isLoading; + + // `isPending` indicate + // that stream response is not yet received from the server, + // so we show a loading indicator to give a better UX. + const isPending = props.isLoading && !isLastMessageFromAssistant; + useEffect(() => { scrollToBottom(); - }, [messages.length]); + }, [messageLength, lastMessage]); return ( - <div className="w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl"> + <div className="w-full rounded-xl bg-white p-4 shadow-xl pb-0"> <div - className="flex flex-col gap-5 divide-y h-[50vh] overflow-auto" + className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4" ref={scrollableChatContainerRef} > - {messages.map((m: Message) => ( - <ChatItem key={m.id} {...m} /> + {props.messages.map((m) => ( + <ChatMessage key={m.id} {...m} /> ))} + {isPending && ( + <div className="flex justify-center items-center pt-10"> + <Loader2 className="h-4 w-4 animate-spin" /> + </div> + )} + </div> + <div className="flex justify-end py-4"> + <ChatActions + reload={props.reload} + stop={props.stop} + showReload={showReload} + showStop={showStop} + /> </div> </div> ); diff --git a/templates/components/ui/shadcn/chat/chat.interface.ts b/templates/types/streaming/nextjs/app/components/ui/chat/chat.interface.ts similarity index 100% rename from templates/components/ui/shadcn/chat/chat.interface.ts rename to templates/types/streaming/nextjs/app/components/ui/chat/chat.interface.ts diff --git a/templates/components/ui/shadcn/chat/codeblock.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/codeblock.tsx similarity index 100% rename from templates/components/ui/shadcn/chat/codeblock.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/codeblock.tsx diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/index.ts b/templates/types/streaming/nextjs/app/components/ui/chat/index.ts index 5de7dce4..c7990f9c 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/index.ts +++ b/templates/types/streaming/nextjs/app/components/ui/chat/index.ts @@ -1,6 +1,5 @@ import ChatInput from "./chat-input"; import ChatMessages from "./chat-messages"; -export type { ChatInputProps } from "./chat-input"; -export type { Message } from "./chat-messages"; +export { type ChatHandler, type Message } from "./chat.interface"; export { ChatInput, ChatMessages }; diff --git a/templates/components/ui/shadcn/chat/markdown.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/markdown.tsx similarity index 100% rename from templates/components/ui/shadcn/chat/markdown.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/markdown.tsx diff --git a/templates/components/ui/shadcn/chat/use-copy-to-clipboard.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/use-copy-to-clipboard.tsx similarity index 100% rename from templates/components/ui/shadcn/chat/use-copy-to-clipboard.tsx rename to templates/types/streaming/nextjs/app/components/ui/chat/use-copy-to-clipboard.tsx diff --git a/templates/components/ui/shadcn/file-uploader.tsx b/templates/types/streaming/nextjs/app/components/ui/file-uploader.tsx similarity index 100% rename from templates/components/ui/shadcn/file-uploader.tsx rename to templates/types/streaming/nextjs/app/components/ui/file-uploader.tsx diff --git a/templates/components/ui/shadcn/input.tsx b/templates/types/streaming/nextjs/app/components/ui/input.tsx similarity index 100% rename from templates/components/ui/shadcn/input.tsx rename to templates/types/streaming/nextjs/app/components/ui/input.tsx diff --git a/templates/components/ui/shadcn/lib/utils.ts b/templates/types/streaming/nextjs/app/components/ui/lib/utils.ts similarity index 100% rename from templates/components/ui/shadcn/lib/utils.ts rename to templates/types/streaming/nextjs/app/components/ui/lib/utils.ts diff --git a/templates/components/ui/shadcn/upload-image-preview.tsx b/templates/types/streaming/nextjs/app/components/ui/upload-image-preview.tsx similarity index 100% rename from templates/components/ui/shadcn/upload-image-preview.tsx rename to templates/types/streaming/nextjs/app/components/ui/upload-image-preview.tsx diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json index ea58702e..5cddd0f7 100644 --- a/templates/types/streaming/nextjs/package.json +++ b/templates/types/streaming/nextjs/package.json @@ -8,12 +8,24 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-slot": "^1.0.2", "ai": "^2.2.25", - "llamaindex": "0.0.31", + "class-variance-authority": "^0.7.0", + "clsx": "^1.2.1", "dotenv": "^16.3.1", + "llamaindex": "0.0.37", + "lucide-react": "^0.294.0", "next": "^13", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-markdown": "^8.0.7", + "react-syntax-highlighter": "^15.5.0", + "remark": "^14.0.3", + "remark-code-import": "^1.2.0", + "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", + "supports-color": "^9.4.0", + "tailwind-merge": "^2.1.0" }, "devDependencies": { "@types/node": "^20", @@ -24,6 +36,7 @@ "eslint-config-next": "^13", "postcss": "^8", "tailwindcss": "^3.3", - "typescript": "^5" + "typescript": "^5", + "@types/react-syntax-highlighter": "^15.5.6" } } \ No newline at end of file -- GitLab