Skip to content
Snippets Groups Projects
Commit a9930be7 authored by Marcus Schiesser's avatar Marcus Schiesser
Browse files

refactor: swapped html and shadcn

parent 6f48831d
No related branches found
No related tags found
No related merge requests found
Showing
with 259 additions and 236 deletions
import { User2 } from "lucide-react"; "use client";
import Image from "next/image"; import Image from "next/image";
import { Message } from "./chat-messages";
export default function ChatAvatar({ role }: { role: string }) { export default function ChatAvatar(message: Message) {
if (role === "user") { if (message.role === "user") {
return ( return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow"> <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
<User2 className="h-4 w-4" /> <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> </div>
); );
} }
return ( return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow"> <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
<Image <Image
className="rounded-md" className="rounded-md"
src="/llama.png" src="/llama.png"
......
"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>
</>
);
}
"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>
);
}
import ChatInput from "./chat-input"; import ChatInput from "./chat-input";
import ChatMessages from "./chat-messages"; 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 }; export { ChatInput, ChatMessages };
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>
);
}
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>
);
}
...@@ -162,7 +162,7 @@ const installTSTemplate = async ({ ...@@ -162,7 +162,7 @@ const installTSTemplate = async ({
/** /**
* Copy the selected UI files to the target directory and reference it. * 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"); console.log("\nUsing UI:", ui, "\n");
const uiPath = path.join(compPath, "ui", ui); const uiPath = path.join(compPath, "ui", ui);
const destUiPath = path.join(root, "app", "components", "ui"); const destUiPath = path.join(root, "app", "components", "ui");
...@@ -227,26 +227,26 @@ const installTSTemplate = async ({ ...@@ -227,26 +227,26 @@ const installTSTemplate = async ({
}; };
} }
if (framework === "nextjs" && ui === "shadcn") { if (framework === "nextjs" && ui === "html") {
// add shadcn dependencies to package.json // remove shadcn dependencies if html ui is selected
packageJson.dependencies = { packageJson.dependencies = {
...packageJson.dependencies, ...packageJson.dependencies,
"tailwind-merge": "^2", "tailwind-merge": undefined,
"@radix-ui/react-slot": "^1", "@radix-ui/react-slot": undefined,
"class-variance-authority": "^0.7", "class-variance-authority": undefined,
clsx: "^1.2.1", clsx: undefined,
"lucide-react": "^0.291", "lucide-react": undefined,
remark: "^14.0.3", remark: undefined,
"remark-code-import": "^1.2.0", "remark-code-import": undefined,
"remark-gfm": "^3.0.1", "remark-gfm": undefined,
"remark-math": "^5.1.1", "remark-math": undefined,
"react-markdown": "^8.0.7", "react-markdown": undefined,
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": undefined,
}; };
packageJson.devDependencies = { packageJson.devDependencies = {
...packageJson.devDependencies, ...packageJson.devDependencies,
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": undefined,
}; };
} }
......
"use client"; import { User2 } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { Message } from "./chat-messages";
export default function ChatAvatar(message: Message) { export default function ChatAvatar({ role }: { role: string }) {
if (message.role === "user") { if (role === "user") {
return ( return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background"> <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow">
<svg <User2 className="h-4 w-4" />
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> </div>
); );
} }
return ( 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 <Image
className="rounded-md" className="rounded-md"
src="/llama.png" src="/llama.png"
......
"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 { export default function ChatInput(
/** The current value of the input */ props: Pick<
input?: string; ChatHandler,
/** An input/textarea-ready onChange handler to control the value of the input */ | "isLoading"
handleInputChange?: ( | "input"
e: | "onFileUpload"
| React.ChangeEvent<HTMLInputElement> | "onFileError"
| React.ChangeEvent<HTMLTextAreaElement>, | "handleSubmit"
) => void; | "handleInputChange"
/** Form submission handler to automatically reset input and append a user message */ > & {
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; multiModal?: boolean;
isLoading: boolean; },
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 ( return (
<> <form
<form onSubmit={onSubmit}
onSubmit={props.handleSubmit} className="rounded-xl bg-white p-4 shadow-xl space-y-4"
className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4" >
> {imageUrl && (
<input <UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
)}
<div className="flex w-full items-start justify-between gap-4 ">
<Input
autoFocus autoFocus
name="message" name="message"
placeholder="Type a message" placeholder="Type a message"
className="w-full p-4 rounded-xl shadow-inner flex-1" className="flex-1"
value={props.input} value={props.input}
onChange={props.handleInputChange} onChange={props.handleInputChange}
/> />
<button <FileUploader
disabled={props.isLoading} onFileUpload={handleUploadFile}
type="submit" onFileError={props.onFileError}
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" />
> <Button type="submit" disabled={props.isLoading}>
Send message Send message
</button> </Button>
</form> </div>
</> </form>
); );
} }
"use client"; import { Loader2 } from "lucide-react";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import ChatItem from "./chat-item";
export interface Message { import ChatActions from "./chat-actions";
id: string; import ChatMessage from "./chat-message";
content: string; import { ChatHandler } from "./chat.interface";
role: string;
}
export default function ChatMessages({ export default function ChatMessages(
messages, props: Pick<ChatHandler, "messages" | "isLoading" | "reload" | "stop">,
isLoading, ) {
reload,
stop,
}: {
messages: Message[];
isLoading?: boolean;
stop?: () => void;
reload?: () => void;
}) {
const scrollableChatContainerRef = useRef<HTMLDivElement>(null); const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
const messageLength = props.messages.length;
const lastMessage = props.messages[messageLength - 1];
const scrollToBottom = () => { const scrollToBottom = () => {
if (scrollableChatContainerRef.current) { if (scrollableChatContainerRef.current) {
...@@ -29,19 +19,43 @@ export default function ChatMessages({ ...@@ -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(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, [messages.length]); }, [messageLength, lastMessage]);
return ( 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 <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} ref={scrollableChatContainerRef}
> >
{messages.map((m: Message) => ( {props.messages.map((m) => (
<ChatItem key={m.id} {...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>
</div> </div>
); );
......
import ChatInput from "./chat-input"; import ChatInput from "./chat-input";
import ChatMessages from "./chat-messages"; import ChatMessages from "./chat-messages";
export type { ChatInputProps } from "./chat-input"; export { type ChatHandler, type Message } from "./chat.interface";
export type { Message } from "./chat-messages";
export { ChatInput, ChatMessages }; export { ChatInput, ChatMessages };
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment