update FE

This commit is contained in:
dbdbd9 2025-07-30 09:33:40 +07:00
parent 5d726d51ab
commit ee3809670b
7 changed files with 2709 additions and 2625 deletions

View File

@ -1,14 +1,14 @@
import { useCallback, useEffect, useRef } from "react"
import { useCallback, useEffect, useRef } from "react";
import useInputImage from "@/hooks/useInputImage"
import { keepGUIAlive } from "@/lib/utils"
import { getServerConfig } from "@/lib/api"
import Header from "@/components/Header"
import Workspace from "@/components/Workspace"
import FileSelect from "@/components/FileSelect"
import { Toaster } from "./components/ui/toaster"
import { useStore } from "./lib/states"
import { useWindowSize } from "react-use"
import useInputImage from "@/hooks/useInputImage";
import { keepGUIAlive } from "@/lib/utils";
import { getServerConfig } from "@/lib/api";
import Header from "@/components/Header";
import Workspace from "@/components/Workspace";
import FileSelect from "@/components/FileSelect";
import { Toaster } from "./components/ui/toaster";
import { useStore } from "./lib/states";
import { useWindowSize } from "react-use";
const SUPPORTED_FILE_TYPE = [
"image/jpeg",
@ -16,64 +16,66 @@ const SUPPORTED_FILE_TYPE = [
"image/webp",
"image/bmp",
"image/tiff",
]
];
function Home() {
const [file, updateAppState, setServerConfig, setFile] = useStore((state) => [
const [file, updateAppState, setServerConfig, setFile] = useStore(
(state) => [
state.file,
state.updateAppState,
state.setServerConfig,
state.setFile,
])
],
);
const userInputImage = useInputImage()
const userInputImage = useInputImage();
const windowSize = useWindowSize()
const windowSize = useWindowSize();
useEffect(() => {
if (userInputImage) {
setFile(userInputImage)
setFile(userInputImage);
}
}, [userInputImage, setFile])
}, [userInputImage, setFile]);
useEffect(() => {
updateAppState({ windowSize })
}, [windowSize])
updateAppState({ windowSize });
}, [windowSize]);
useEffect(() => {
const fetchServerConfig = async () => {
const serverConfig = await getServerConfig()
setServerConfig(serverConfig)
const serverConfig = await getServerConfig();
setServerConfig(serverConfig);
if (serverConfig.isDesktop) {
// Keeping GUI Window Open
keepGUIAlive()
keepGUIAlive();
}
}
fetchServerConfig()
}, [])
};
fetchServerConfig();
}, []);
const dragCounter = useRef(0)
const dragCounter = useRef(0);
const handleDrag = useCallback((event: any) => {
event.preventDefault()
event.stopPropagation()
}, [])
event.preventDefault();
event.stopPropagation();
}, []);
const handleDragIn = useCallback((event: any) => {
event.preventDefault()
event.stopPropagation()
dragCounter.current += 1
}, [])
event.preventDefault();
event.stopPropagation();
dragCounter.current += 1;
}, []);
const handleDragOut = useCallback((event: any) => {
event.preventDefault()
event.stopPropagation()
dragCounter.current -= 1
if (dragCounter.current > 0) return
}, [])
event.preventDefault();
event.stopPropagation();
dragCounter.current -= 1;
if (dragCounter.current > 0) return;
}, []);
const handleDrop = useCallback((event: any) => {
event.preventDefault()
event.stopPropagation()
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
if (event.dataTransfer.files.length > 1) {
// setToastState({
@ -83,10 +85,10 @@ function Home() {
// duration: 3000,
// })
} else {
const dragFile = event.dataTransfer.files[0]
const fileType = dragFile.type
const dragFile = event.dataTransfer.files[0];
const fileType = dragFile.type;
if (SUPPORTED_FILE_TYPE.includes(fileType)) {
setFile(dragFile)
setFile(dragFile);
} else {
// setToastState({
// open: true,
@ -96,55 +98,55 @@ function Home() {
// })
}
}
event.dataTransfer.clearData()
event.dataTransfer.clearData();
}
}, [])
}, []);
const onPaste = useCallback((event: any) => {
// TODO: when sd side panel open, ctrl+v not work
// https://htmldom.dev/paste-an-image-from-the-clipboard/
if (!event.clipboardData) {
return
return;
}
const clipboardItems = event.clipboardData.items
const clipboardItems = event.clipboardData.items;
const items: DataTransferItem[] = [].slice
.call(clipboardItems)
.filter((item: DataTransferItem) => {
// Filter the image items only
return item.type.indexOf("image") !== -1
})
return item.type.indexOf("image") !== -1;
});
if (items.length === 0) {
return
return;
}
event.preventDefault()
event.stopPropagation()
event.preventDefault();
event.stopPropagation();
// TODO: add confirm dialog
const item = items[0]
const item = items[0];
// Get the blob of image
const blob = item.getAsFile()
const blob = item.getAsFile();
if (blob) {
setFile(blob)
setFile(blob);
}
}, [])
}, []);
useEffect(() => {
window.addEventListener("dragenter", handleDragIn)
window.addEventListener("dragleave", handleDragOut)
window.addEventListener("dragover", handleDrag)
window.addEventListener("drop", handleDrop)
window.addEventListener("paste", onPaste)
window.addEventListener("dragenter", handleDragIn);
window.addEventListener("dragleave", handleDragOut);
window.addEventListener("dragover", handleDrag);
window.addEventListener("drop", handleDrop);
window.addEventListener("paste", onPaste);
return function cleanUp() {
window.removeEventListener("dragenter", handleDragIn)
window.removeEventListener("dragleave", handleDragOut)
window.removeEventListener("dragover", handleDrag)
window.removeEventListener("drop", handleDrop)
window.removeEventListener("paste", onPaste)
}
})
window.removeEventListener("dragenter", handleDragIn);
window.removeEventListener("dragleave", handleDragOut);
window.removeEventListener("dragover", handleDrag);
window.removeEventListener("drop", handleDrop);
window.removeEventListener("paste", onPaste);
};
});
return (
<main className="flex min-h-screen flex-col items-center justify-between w-full bg-[radial-gradient(circle_at_1px_1px,_#8e8e8e8e_1px,_transparent_0)] [background-size:20px_20px] bg-repeat">
@ -154,14 +156,14 @@ function Home() {
{!file ? (
<FileSelect
onSelection={async (f) => {
setFile(f)
setFile(f);
}}
/>
) : (
<></>
)}
</main>
)
);
}
export default Home
export default Home;

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,35 @@
import { useState } from "react"
import useResolution from "@/hooks/useResolution"
import { useState } from "react";
import useResolution from "@/hooks/useResolution";
type FileSelectProps = {
onSelection: (file: File) => void
}
onSelection: (file: File) => void;
};
export default function FileSelect(props: FileSelectProps) {
const { onSelection } = props
const { onSelection } = props;
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`);
const resolution = useResolution()
const resolution = useResolution();
function onFileSelected(file: File) {
if (!file) {
return
return;
}
// Skip non-image files
const isImage = file.type.match("image.*")
const isImage = file.type.match("image.*");
if (!isImage) {
return
return;
}
try {
// Check if file is larger than 20mb
if (file.size > 20 * 1024 * 1024) {
throw new Error("file too large")
throw new Error("file too large");
}
onSelection(file)
onSelection(file);
} catch (e) {
// eslint-disable-next-line
alert(`error: ${(e as any).message}`)
alert(`error: ${(e as any).message}`);
}
}
@ -42,8 +42,8 @@ export default function FileSelect(props: FileSelectProps) {
<div
className="grid p-16 w-full h-full"
onDragOver={(ev) => {
ev.stopPropagation()
ev.preventDefault()
ev.stopPropagation();
ev.preventDefault();
}}
>
<input
@ -52,9 +52,9 @@ export default function FileSelect(props: FileSelectProps) {
name={uploadElemId}
type="file"
onChange={(ev) => {
const file = ev.currentTarget.files?.[0]
const file = ev.currentTarget.files?.[0];
if (file) {
onFileSelected(file)
onFileSelected(file);
}
}}
accept="image/png, image/jpeg"
@ -67,5 +67,5 @@ export default function FileSelect(props: FileSelectProps) {
</div>
</label>
</div>
)
);
}

View File

@ -1,174 +1,53 @@
import { PlayIcon } from "@radix-ui/react-icons"
import { useState } from "react"
import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts"
import { useImage } from "@/hooks/useImage"
import { IconButton, ImageUploadButton } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
import PromptInput from "./PromptInput"
import { RotateCw, Image, Upload } from "lucide-react"
import FileManager from "./FileManager"
import { getMediaFile } from "@/lib/api"
import { useStore } from "@/lib/states"
import SettingsDialog from "./Settings"
import { cn, fileToImage } from "@/lib/utils"
import Coffee from "./Coffee"
import { useToast } from "./ui/use-toast"
import PromptInput from "./PromptInput";
import { RotateCw, Image } from "lucide-react";
import { useStore } from "@/lib/states";
const Header = () => {
const [
file,
customMask,
isInpainting,
serverConfig,
runMannually,
enableUploadMask,
model,
setFile,
setCustomFile,
runInpainting,
showPrevMask,
hidePrevMask,
imageHeight,
imageWidth,
] = useStore((state) => [
state.file,
state.customMask,
state.isInpainting,
state.serverConfig,
state.runMannually(),
state.settings.enableUploadMask,
state.settings.model,
state.setFile,
state.setCustomFile,
state.runInpainting,
state.showPrevMask,
state.hidePrevMask,
state.imageHeight,
state.imageWidth,
])
const { toast } = useToast()
const [maskImage, maskImageLoaded] = useImage(customMask)
const [openMaskPopover, setOpenMaskPopover] = useState(false)
]);
const handleRerunLastMask = () => {
runInpainting()
}
runInpainting();
};
const onRerunMouseEnter = () => {
showPrevMask()
}
showPrevMask();
};
const onRerunMouseLeave = () => {
hidePrevMask()
}
hidePrevMask();
};
return (
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70">
<div className="flex items-center gap-1">
{serverConfig.enableFileManager ? (
<FileManager
photoWidth={512}
onPhotoClick={async (tab: string, filename: string) => {
try {
const newFile = await getMediaFile(tab, filename)
setFile(newFile)
} catch (e: any) {
toast({
variant: "destructive",
description: e.message ? e.message : e.toString(),
})
return
}
}}
/>
) : (
<></>
)}
<ImageUploadButton
disabled={isInpainting}
tooltip="Upload image"
onFileUpload={(file) => {
setFile(file)
setFile(file);
}}
>
<Image />
</ImageUploadButton>
<div
className={cn([
"flex items-center gap-1",
file && enableUploadMask ? "visible" : "hidden",
])}
>
<ImageUploadButton
disabled={isInpainting}
tooltip="Upload custom mask"
onFileUpload={async (file) => {
let newCustomMask: HTMLImageElement | null = null
try {
newCustomMask = await fileToImage(file)
} catch (e: any) {
toast({
variant: "destructive",
description: e.message ? e.message : e.toString(),
})
return
}
if (
newCustomMask.naturalHeight !== imageHeight ||
newCustomMask.naturalWidth !== imageWidth
) {
toast({
variant: "destructive",
description: `The size of the mask must same as image: ${imageWidth}x${imageHeight}`,
})
return
}
setCustomFile(file)
if (!runMannually) {
runInpainting()
}
}}
>
<Upload />
</ImageUploadButton>
{customMask ? (
<Popover open={openMaskPopover}>
<PopoverTrigger
className="btn-primary side-panel-trigger"
onMouseEnter={() => setOpenMaskPopover(true)}
onMouseLeave={() => setOpenMaskPopover(false)}
style={{
visibility: customMask ? "visible" : "hidden",
outline: "none",
}}
onClick={() => {
if (customMask) {
}
}}
>
<IconButton tooltip="Run custom mask">
<PlayIcon />
</IconButton>
</PopoverTrigger>
<PopoverContent>
{maskImageLoaded ? (
<img src={maskImage.src} alt="Custom mask" />
) : (
<></>
)}
</PopoverContent>
</Popover>
) : (
<></>
)}
</div>
{file && !model.need_prompt ? (
<IconButton
disabled={isInpainting}
@ -186,13 +65,9 @@ const Header = () => {
{model.need_prompt ? <PromptInput /> : <></>}
<div className="flex gap-1">
<Coffee />
<Shortcuts />
{serverConfig.disableModelSwitch ? <></> : <SettingsDialog />}
</div>
<div className="flex gap-1"></div>
</header>
)
}
);
};
export default Header
export default Header;

View File

@ -1,39 +1,31 @@
import { useEffect } from "react"
import Editor from "./Editor"
import { currentModel } from "@/lib/api"
import { useStore } from "@/lib/states"
import ImageSize from "./ImageSize"
import Plugins from "./Plugins"
import { InteractiveSeg } from "./InteractiveSeg"
import SidePanel from "./SidePanel"
import DiffusionProgress from "./DiffusionProgress"
import { useEffect } from "react";
import Editor from "./Editor";
import { currentModel } from "@/lib/api";
import { useStore } from "@/lib/states";
import ImageSize from "./ImageSize";
const Workspace = () => {
const [file, updateSettings] = useStore((state) => [
state.file,
state.updateSettings,
])
]);
useEffect(() => {
const fetchCurrentModel = async () => {
const model = await currentModel()
updateSettings({ model })
}
fetchCurrentModel()
}, [])
const model = await currentModel();
updateSettings({ model });
};
fetchCurrentModel();
}, []);
return (
<>
<div className="flex gap-3 absolute top-[68px] left-[24px] items-center">
<Plugins />
<ImageSize />
</div>
<InteractiveSeg />
<DiffusionProgress />
<SidePanel />
{file ? <Editor file={file} /> : <></>}
</>
)
}
);
};
export default Workspace
export default Workspace;

View File

@ -24,7 +24,7 @@ export default async function inpaint(
croperRect: Rect,
extenderState: Rect,
mask: File | Blob,
paintByExampleImage: File | null = null
paintByExampleImage: File | null = null,
) {
const imageBase64 = await convertToBase64(imageFile);
const maskBase64 = await convertToBase64(mask);
@ -106,7 +106,7 @@ export async function switchModel(name: string): Promise<ModelInfo> {
export async function switchPluginModel(
plugin_name: string,
model_name: string
model_name: string,
) {
return api.post(`/switch_plugin_model`, { plugin_name, model_name });
}
@ -121,7 +121,7 @@ export async function runPlugin(
name: string,
imageFile: File,
upscale?: number,
clicks?: number[][]
clicks?: number[][],
) {
const imageBase64 = await convertToBase64(imageFile);
const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image";
@ -148,11 +148,11 @@ export async function runPlugin(
export async function getMediaFile(tab: string, filename: string) {
const res = await fetch(
`${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
filename
filename,
)}`,
{
method: "GET",
}
},
);
if (res.ok) {
const blob = await res.blob();
@ -173,7 +173,7 @@ export async function getMedias(tab: string): Promise<Filename[]> {
export async function downloadToOutput(
image: HTMLImageElement,
filename: string,
mimeType: string
mimeType: string,
) {
const file = await srcToFile(image.src, filename, mimeType);
const fd = new FormData();
@ -208,7 +208,7 @@ export async function getSamplers(): Promise<string[]> {
export async function postAdjustMask(
mask: File | Blob,
operate: "expand" | "shrink" | "reverse",
kernel_size: number
kernel_size: number,
) {
const maskBase64 = await convertToBase64(mask);
const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
@ -229,3 +229,28 @@ export async function postAdjustMask(
const errMsg = await res.json();
throw new Error(errMsg);
}
export async function submitMask(imageFile: File, mask: File | Blob) {
const imageBase64 = await convertToBase64(imageFile);
const maskBase64 = await convertToBase64(mask);
const res = await fetch(`${API_ENDPOINT}/submit-mask`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image: imageBase64,
mask: maskBase64,
}),
});
if (res.ok) {
const blob = await res.blob();
return {
blob: URL.createObjectURL(blob),
seed: res.headers.get("X-Seed"),
};
}
// const errors = await res.json();
throw new Error(`Submit successfull.`);
}

File diff suppressed because it is too large Load Diff