diff --git a/src/components/OptionsButton.tsx b/src/components/OptionsButton.tsx index 0de16737..f0b880e1 100644 --- a/src/components/OptionsButton.tsx +++ b/src/components/OptionsButton.tsx @@ -19,6 +19,7 @@ import { useCopyToClipboard } from "react-use"; import { ChatCraftChat } from "../lib/ChatCraftChat"; import { useUser } from "../hooks/use-user"; import { useAlert } from "../hooks/use-alert"; +import { useSettings } from "../hooks/use-settings"; import ShareModal from "./ShareModal"; import { download, compressImageToBase64 } from "../lib/utils"; import theme from "../theme"; @@ -91,6 +92,7 @@ function OptionsButton({ const { info, error } = useAlert(); const [, copyToClipboard] = useCopyToClipboard(); const fileInputRef = useRef(null); + const { settings } = useSettings(); const handleFileChange = useCallback( (event: React.ChangeEvent) => { @@ -105,7 +107,11 @@ function OptionsButton({ const file = files[i]; if (file.type.startsWith("image/")) { onFileSelected(""); - compressImageToBase64(file) + compressImageToBase64(file, { + compressionFactor: settings.compressionFactor, + maxSizeMB: settings.maxCompressedFileSizeMB, + maxWidthOrHeight: settings.maxImageDimension, + }) .then((base64) => onFileSelected(base64)) .catch((err) => { console.error(err); @@ -123,7 +129,13 @@ function OptionsButton({ event.target.value = ""; } }, - [error, onFileSelected] + [ + error, + onFileSelected, + settings.compressionFactor, + settings.maxCompressedFileSizeMB, + settings.maxImageDimension, + ] ); const handleAttachFiles = useCallback(() => { diff --git a/src/components/PreferencesModal.tsx b/src/components/PreferencesModal.tsx index b211efd1..14e7bf49 100644 --- a/src/components/PreferencesModal.tsx +++ b/src/components/PreferencesModal.tsx @@ -455,6 +455,93 @@ function PreferencesModal({ isOpen, onClose, finalFocusRef }: PreferencesModalPr + + Image Compression + + + + + Maximum file size after compression: {settings.maxCompressedFileSizeMB} (MB) + + + setSettings({ ...settings, maxCompressedFileSizeMB: value }) + } + min={1} + max={20} + step={1} + > + + + + + + + Maximum file size must be between 1 and 20 MB. + + + After compression, each attached image will be under your chosen maximum + file size (1-20 MB). + + + + + + + Maximum image dimension: {settings.maxImageDimension} (px) + + setSettings({ ...settings, maxImageDimension: value })} + min={16} + max={2048} + step={16} + > + + + + + + + Maximum Image dimension must be between 16 and 2048 + + + Your compressed image's maximum width or height will be within the + dimension you choose (16-2048 pixels). + + + + + + Compression factor: {settings.compressionFactor} + setSettings({ ...settings, compressionFactor: value })} + min={0.1} + max={1} + step={0.1} + > + + + + + + + Compression factor must be between 0.1 and 1.0 + + + Set the maximum file size based on the original size multiplied by the + factor you choose (0.1-1.0). + + + + + + When writing a prompt, press Enter to... diff --git a/src/components/PromptForm/DesktopPromptForm.tsx b/src/components/PromptForm/DesktopPromptForm.tsx index 0c5951da..697e4451 100644 --- a/src/components/PromptForm/DesktopPromptForm.tsx +++ b/src/components/PromptForm/DesktopPromptForm.tsx @@ -227,8 +227,15 @@ function DesktopPromptForm({ const processImages = (imageFiles: File[]) => { setInputImageUrls((prevImageUrls) => [...prevImageUrls, ...imageFiles.map(() => "")]); - - Promise.all(imageFiles.map((file) => compressImageToBase64(file))) + Promise.all( + imageFiles.map((file) => + compressImageToBase64(file, { + compressionFactor: settings.compressionFactor, + maxSizeMB: settings.maxCompressedFileSizeMB, + maxWidthOrHeight: settings.maxImageDimension, + }) + ) + ) .then((base64Strings) => { setInputImageUrls((prevImageUrls) => { const newImageUrls = [...prevImageUrls]; diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 71d15e62..9c6ca496 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -39,6 +39,9 @@ export type Settings = { textToSpeech: TextToSpeechSettings; providers: ProviderData; currentProvider: ChatCraftProvider; + compressionFactor: number; + maxCompressedFileSizeMB: number; + maxImageDimension: number; }; export const defaults: Settings = { @@ -56,6 +59,9 @@ export const defaults: Settings = { }, providers: {}, currentProvider: new FreeModelProvider(), + compressionFactor: 1, + maxCompressedFileSizeMB: 20, + maxImageDimension: 2048, }; export const key = "settings"; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8fc23ab4..f4388772 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -84,18 +84,28 @@ export const screenshotElement = (element: HTMLElement): Promise => { ); }; +interface ImageCompressionOptions { + compressionFactor?: number; + maxSizeMB?: number; + maxWidthOrHeight?: number; +} // Make sure image's size is within 20MB and 2048x2048 resolution // https://platform.openai.com/docs/guides/vision/is-there-a-limit-to-the-size-of-the-image-i-can-upload -export const compressImageToBase64 = (file: File): Promise => { - const imageCompressionOptions = { - maxSizeMB: 20, - maxWidthOrHeight: 2048, +export const compressImageToBase64 = ( + file: File, + options: ImageCompressionOptions = {} +): Promise => { + const { compressionFactor = 1, maxSizeMB = 20, maxWidthOrHeight = 2048 } = options; + + const libOptions = { + maxSizeMB: Math.min((file.size / 1024 / 1024) * compressionFactor, maxSizeMB, 20), + maxWidthOrHeight: Math.min(maxWidthOrHeight, 2048), }; return import("browser-image-compression") .then((imageCompressionModule) => { const imageCompression = imageCompressionModule.default; - return imageCompression(file, imageCompressionOptions); + return imageCompression(file, libOptions); }) .then((compressedFile: File) => { return new Promise((resolve, reject) => {