diff --git a/apps/www/app/examples/chat/page.tsx b/apps/www/app/examples/chat/page.tsx
index 7e39641..db5ff13 100644
--- a/apps/www/app/examples/chat/page.tsx
+++ b/apps/www/app/examples/chat/page.tsx
@@ -27,7 +27,7 @@ export default function ChatPage() {
return (
-
+
)
}
diff --git a/apps/www/registry/default/ui/chat/chat-input.tsx b/apps/www/registry/default/ui/chat/chat-input.tsx
index 1a0cc3e..435637e 100644
--- a/apps/www/registry/default/ui/chat/chat-input.tsx
+++ b/apps/www/registry/default/ui/chat/chat-input.tsx
@@ -1,29 +1,84 @@
+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" | "handleSubmit" | "handleInputChange" | "input"
- >,
+ | "isLoading"
+ | "input"
+ | "onFileUpload"
+ | "onFileError"
+ | "handleSubmit"
+ | "handleInputChange"
+ > & {
+ multiModal?: boolean;
+ },
) {
+ const [imageUrl, setImageUrl] = useState(null);
+
+ const onSubmit = (e: React.FormEvent) => {
+ 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((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 (
);
}
diff --git a/apps/www/registry/default/ui/chat/chat.interface.ts b/apps/www/registry/default/ui/chat/chat.interface.ts
index 3256f7f..584a63f 100644
--- a/apps/www/registry/default/ui/chat/chat.interface.ts
+++ b/apps/www/registry/default/ui/chat/chat.interface.ts
@@ -8,8 +8,15 @@ export interface ChatHandler {
messages: Message[];
input: string;
isLoading: boolean;
- handleSubmit: (e: React.FormEvent) => void;
+ handleSubmit: (
+ e: React.FormEvent,
+ ops?: {
+ data?: any;
+ },
+ ) => void;
handleInputChange: (e: React.ChangeEvent) => void;
reload?: () => void;
stop?: () => void;
+ onFileUpload?: (file: File) => Promise;
+ onFileError?: (errMsg: string) => void;
}
diff --git a/apps/www/registry/default/ui/file-uploader.tsx b/apps/www/registry/default/ui/file-uploader.tsx
new file mode 100644
index 0000000..bab773e
--- /dev/null
+++ b/apps/www/registry/default/ui/file-uploader.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { Loader2, Paperclip } from "lucide-react";
+import { ChangeEvent, useState } from "react";
+import { buttonVariants } from "./button";
+import { cn } from "@/lib/utils"
+
+export interface FileUploaderProps {
+ config?: {
+ inputId?: string;
+ fileSizeLimit?: number;
+ allowedExtensions?: string[];
+ checkExtension?: (extension: string) => string | null;
+ disabled: boolean;
+ };
+ onFileUpload: (file: File) => Promise;
+ onFileError?: (errMsg: string) => void;
+}
+
+const DEFAULT_INPUT_ID = "fileInput";
+const DEFAULT_FILE_SIZE_LIMIT = 1024 * 1024 * 50; // 50 MB
+
+export default function FileUploader({
+ config,
+ onFileUpload,
+ onFileError,
+}: FileUploaderProps) {
+ const [uploading, setUploading] = useState(false);
+
+ const inputId = config?.inputId || DEFAULT_INPUT_ID;
+ const fileSizeLimit = config?.fileSizeLimit || DEFAULT_FILE_SIZE_LIMIT;
+ const allowedExtensions = config?.allowedExtensions;
+ const defaultCheckExtension = (extension: string) => {
+ if (allowedExtensions && !allowedExtensions.includes(extension)) {
+ return `Invalid file type. Please select a file with one of these formats: ${allowedExtensions!.join(
+ ",",
+ )}`;
+ }
+ return null;
+ };
+ const checkExtension = config?.checkExtension ?? defaultCheckExtension;
+
+ const isFileSizeExceeded = (file: File) => {
+ return file.size > fileSizeLimit;
+ };
+
+ const resetInput = () => {
+ const fileInput = document.getElementById(inputId) as HTMLInputElement;
+ fileInput.value = "";
+ };
+
+ const onFileChange = async (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ setUploading(true);
+ await handleUpload(file);
+ resetInput();
+ setUploading(false);
+ };
+
+ const handleUpload = async (file: File) => {
+ const onFileUploadError = onFileError || window.alert;
+ const fileExtension = file.name.split(".").pop() || "";
+ const extensionFileError = checkExtension(fileExtension);
+ if (extensionFileError) {
+ return onFileUploadError(extensionFileError);
+ }
+
+ if (isFileSizeExceeded(file)) {
+ return onFileUploadError(
+ `File size exceeded. Limit is ${fileSizeLimit / 1024 / 1024} MB`,
+ );
+ }
+
+ await onFileUpload(file);
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/www/registry/default/ui/upload-image-preview.tsx b/apps/www/registry/default/ui/upload-image-preview.tsx
new file mode 100644
index 0000000..b809056
--- /dev/null
+++ b/apps/www/registry/default/ui/upload-image-preview.tsx
@@ -0,0 +1,32 @@
+import { XCircleIcon } from "lucide-react";
+import Image from "next/image";
+import { cn } from "@/lib/utils"
+
+export default function UploadImagePreview({
+ url,
+ onRemove,
+}: {
+ url: string;
+ onRemove: () => void;
+}) {
+ return (
+
+ );
+}