-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
216 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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={props.handleSubmit} | ||
className="flex w-full items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl" | ||
onSubmit={onSubmit} | ||
className="rounded-xl bg-white p-4 shadow-xl space-y-4" | ||
> | ||
<Input | ||
autoFocus | ||
name="message" | ||
placeholder="Type a message" | ||
className="flex-1" | ||
value={props.input} | ||
onChange={props.handleInputChange} | ||
/> | ||
<Button type="submit" disabled={props.isLoading}> | ||
Send message | ||
</Button> | ||
{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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>; | ||
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<HTMLInputElement>) => { | ||
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 ( | ||
<div className="self-stretch"> | ||
<input | ||
type="file" | ||
id={inputId} | ||
style={{ display: "none" }} | ||
onChange={onFileChange} | ||
accept={allowedExtensions?.join(",")} | ||
disabled={config?.disabled || uploading} | ||
/> | ||
<label | ||
htmlFor={inputId} | ||
className={cn( | ||
buttonVariants({ variant: "secondary", size: "icon" }), | ||
"cursor-pointer", | ||
uploading && "opacity-50", | ||
)} | ||
> | ||
{uploading ? ( | ||
<Loader2 className="h-4 w-4 animate-spin" /> | ||
) : ( | ||
<Paperclip className="-rotate-45 w-4 h-4" /> | ||
)} | ||
</label> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className="relative w-20 h-20 group"> | ||
<Image | ||
src={url} | ||
alt="Uploaded image" | ||
fill | ||
className="object-cover w-full h-full rounded-xl hover:brightness-75" | ||
/> | ||
<div | ||
className={cn( | ||
"absolute -top-2 -right-2 w-6 h-6 z-10 bg-gray-500 text-white rounded-full hidden group-hover:block", | ||
)} | ||
> | ||
<XCircleIcon | ||
className="w-6 h-6 bg-gray-500 text-white rounded-full" | ||
onClick={onRemove} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
} |