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 useInputImage from "@/hooks/useInputImage";
import { keepGUIAlive } from "@/lib/utils" import { keepGUIAlive } from "@/lib/utils";
import { getServerConfig } from "@/lib/api" import { getServerConfig } from "@/lib/api";
import Header from "@/components/Header" import Header from "@/components/Header";
import Workspace from "@/components/Workspace" import Workspace from "@/components/Workspace";
import FileSelect from "@/components/FileSelect" import FileSelect from "@/components/FileSelect";
import { Toaster } from "./components/ui/toaster" import { Toaster } from "./components/ui/toaster";
import { useStore } from "./lib/states" import { useStore } from "./lib/states";
import { useWindowSize } from "react-use" import { useWindowSize } from "react-use";
const SUPPORTED_FILE_TYPE = [ const SUPPORTED_FILE_TYPE = [
"image/jpeg", "image/jpeg",
@ -16,64 +16,66 @@ const SUPPORTED_FILE_TYPE = [
"image/webp", "image/webp",
"image/bmp", "image/bmp",
"image/tiff", "image/tiff",
] ];
function Home() { function Home() {
const [file, updateAppState, setServerConfig, setFile] = useStore((state) => [ const [file, updateAppState, setServerConfig, setFile] = useStore(
(state) => [
state.file, state.file,
state.updateAppState, state.updateAppState,
state.setServerConfig, state.setServerConfig,
state.setFile, state.setFile,
]) ],
);
const userInputImage = useInputImage() const userInputImage = useInputImage();
const windowSize = useWindowSize() const windowSize = useWindowSize();
useEffect(() => { useEffect(() => {
if (userInputImage) { if (userInputImage) {
setFile(userInputImage) setFile(userInputImage);
} }
}, [userInputImage, setFile]) }, [userInputImage, setFile]);
useEffect(() => { useEffect(() => {
updateAppState({ windowSize }) updateAppState({ windowSize });
}, [windowSize]) }, [windowSize]);
useEffect(() => { useEffect(() => {
const fetchServerConfig = async () => { const fetchServerConfig = async () => {
const serverConfig = await getServerConfig() const serverConfig = await getServerConfig();
setServerConfig(serverConfig) setServerConfig(serverConfig);
if (serverConfig.isDesktop) { if (serverConfig.isDesktop) {
// Keeping GUI Window Open // Keeping GUI Window Open
keepGUIAlive() keepGUIAlive();
} }
} };
fetchServerConfig() fetchServerConfig();
}, []) }, []);
const dragCounter = useRef(0) const dragCounter = useRef(0);
const handleDrag = useCallback((event: any) => { const handleDrag = useCallback((event: any) => {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
}, []) }, []);
const handleDragIn = useCallback((event: any) => { const handleDragIn = useCallback((event: any) => {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
dragCounter.current += 1 dragCounter.current += 1;
}, []) }, []);
const handleDragOut = useCallback((event: any) => { const handleDragOut = useCallback((event: any) => {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
dragCounter.current -= 1 dragCounter.current -= 1;
if (dragCounter.current > 0) return if (dragCounter.current > 0) return;
}, []) }, []);
const handleDrop = useCallback((event: any) => { const handleDrop = useCallback((event: any) => {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
if (event.dataTransfer.files.length > 1) { if (event.dataTransfer.files.length > 1) {
// setToastState({ // setToastState({
@ -83,10 +85,10 @@ function Home() {
// duration: 3000, // duration: 3000,
// }) // })
} else { } else {
const dragFile = event.dataTransfer.files[0] const dragFile = event.dataTransfer.files[0];
const fileType = dragFile.type const fileType = dragFile.type;
if (SUPPORTED_FILE_TYPE.includes(fileType)) { if (SUPPORTED_FILE_TYPE.includes(fileType)) {
setFile(dragFile) setFile(dragFile);
} else { } else {
// setToastState({ // setToastState({
// open: true, // open: true,
@ -96,55 +98,55 @@ function Home() {
// }) // })
} }
} }
event.dataTransfer.clearData() event.dataTransfer.clearData();
} }
}, []) }, []);
const onPaste = useCallback((event: any) => { const onPaste = useCallback((event: any) => {
// TODO: when sd side panel open, ctrl+v not work // TODO: when sd side panel open, ctrl+v not work
// https://htmldom.dev/paste-an-image-from-the-clipboard/ // https://htmldom.dev/paste-an-image-from-the-clipboard/
if (!event.clipboardData) { if (!event.clipboardData) {
return return;
} }
const clipboardItems = event.clipboardData.items const clipboardItems = event.clipboardData.items;
const items: DataTransferItem[] = [].slice const items: DataTransferItem[] = [].slice
.call(clipboardItems) .call(clipboardItems)
.filter((item: DataTransferItem) => { .filter((item: DataTransferItem) => {
// Filter the image items only // Filter the image items only
return item.type.indexOf("image") !== -1 return item.type.indexOf("image") !== -1;
}) });
if (items.length === 0) { if (items.length === 0) {
return return;
} }
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
// TODO: add confirm dialog // TODO: add confirm dialog
const item = items[0] const item = items[0];
// Get the blob of image // Get the blob of image
const blob = item.getAsFile() const blob = item.getAsFile();
if (blob) { if (blob) {
setFile(blob) setFile(blob);
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
window.addEventListener("dragenter", handleDragIn) window.addEventListener("dragenter", handleDragIn);
window.addEventListener("dragleave", handleDragOut) window.addEventListener("dragleave", handleDragOut);
window.addEventListener("dragover", handleDrag) window.addEventListener("dragover", handleDrag);
window.addEventListener("drop", handleDrop) window.addEventListener("drop", handleDrop);
window.addEventListener("paste", onPaste) window.addEventListener("paste", onPaste);
return function cleanUp() { return function cleanUp() {
window.removeEventListener("dragenter", handleDragIn) window.removeEventListener("dragenter", handleDragIn);
window.removeEventListener("dragleave", handleDragOut) window.removeEventListener("dragleave", handleDragOut);
window.removeEventListener("dragover", handleDrag) window.removeEventListener("dragover", handleDrag);
window.removeEventListener("drop", handleDrop) window.removeEventListener("drop", handleDrop);
window.removeEventListener("paste", onPaste) window.removeEventListener("paste", onPaste);
} };
}) });
return ( 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"> <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 ? ( {!file ? (
<FileSelect <FileSelect
onSelection={async (f) => { onSelection={async (f) => {
setFile(f) setFile(f);
}} }}
/> />
) : ( ) : (
<></> <></>
)} )}
</main> </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 { useState } from "react";
import useResolution from "@/hooks/useResolution" import useResolution from "@/hooks/useResolution";
type FileSelectProps = { type FileSelectProps = {
onSelection: (file: File) => void onSelection: (file: File) => void;
} };
export default function FileSelect(props: FileSelectProps) { 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) { function onFileSelected(file: File) {
if (!file) { if (!file) {
return return;
} }
// Skip non-image files // Skip non-image files
const isImage = file.type.match("image.*") const isImage = file.type.match("image.*");
if (!isImage) { if (!isImage) {
return return;
} }
try { try {
// Check if file is larger than 20mb // Check if file is larger than 20mb
if (file.size > 20 * 1024 * 1024) { if (file.size > 20 * 1024 * 1024) {
throw new Error("file too large") throw new Error("file too large");
} }
onSelection(file) onSelection(file);
} catch (e) { } catch (e) {
// eslint-disable-next-line // 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 <div
className="grid p-16 w-full h-full" className="grid p-16 w-full h-full"
onDragOver={(ev) => { onDragOver={(ev) => {
ev.stopPropagation() ev.stopPropagation();
ev.preventDefault() ev.preventDefault();
}} }}
> >
<input <input
@ -52,9 +52,9 @@ export default function FileSelect(props: FileSelectProps) {
name={uploadElemId} name={uploadElemId}
type="file" type="file"
onChange={(ev) => { onChange={(ev) => {
const file = ev.currentTarget.files?.[0] const file = ev.currentTarget.files?.[0];
if (file) { if (file) {
onFileSelected(file) onFileSelected(file);
} }
}} }}
accept="image/png, image/jpeg" accept="image/png, image/jpeg"
@ -67,5 +67,5 @@ export default function FileSelect(props: FileSelectProps) {
</div> </div>
</label> </label>
</div> </div>
) );
} }

View File

@ -1,174 +1,53 @@
import { PlayIcon } from "@radix-ui/react-icons" import { IconButton, ImageUploadButton } from "@/components/ui/button";
import { useState } from "react"
import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts"
import { useImage } from "@/hooks/useImage"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" import PromptInput from "./PromptInput";
import PromptInput from "./PromptInput" import { RotateCw, Image } from "lucide-react";
import { RotateCw, Image, Upload } from "lucide-react" import { useStore } from "@/lib/states";
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"
const Header = () => { const Header = () => {
const [ const [
file, file,
customMask,
isInpainting, isInpainting,
serverConfig,
runMannually,
enableUploadMask,
model, model,
setFile, setFile,
setCustomFile,
runInpainting, runInpainting,
showPrevMask, showPrevMask,
hidePrevMask, hidePrevMask,
imageHeight,
imageWidth,
] = useStore((state) => [ ] = useStore((state) => [
state.file, state.file,
state.customMask,
state.isInpainting, state.isInpainting,
state.serverConfig,
state.runMannually(),
state.settings.enableUploadMask,
state.settings.model, state.settings.model,
state.setFile, state.setFile,
state.setCustomFile,
state.runInpainting, state.runInpainting,
state.showPrevMask, state.showPrevMask,
state.hidePrevMask, state.hidePrevMask,
state.imageHeight, ]);
state.imageWidth,
])
const { toast } = useToast()
const [maskImage, maskImageLoaded] = useImage(customMask)
const [openMaskPopover, setOpenMaskPopover] = useState(false)
const handleRerunLastMask = () => { const handleRerunLastMask = () => {
runInpainting() runInpainting();
} };
const onRerunMouseEnter = () => { const onRerunMouseEnter = () => {
showPrevMask() showPrevMask();
} };
const onRerunMouseLeave = () => { const onRerunMouseLeave = () => {
hidePrevMask() hidePrevMask();
} };
return ( 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"> <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"> <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 <ImageUploadButton
disabled={isInpainting} disabled={isInpainting}
tooltip="Upload image" tooltip="Upload image"
onFileUpload={(file) => { onFileUpload={(file) => {
setFile(file) setFile(file);
}} }}
> >
<Image /> <Image />
</ImageUploadButton> </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 ? ( {file && !model.need_prompt ? (
<IconButton <IconButton
disabled={isInpainting} disabled={isInpainting}
@ -186,13 +65,9 @@ const Header = () => {
{model.need_prompt ? <PromptInput /> : <></>} {model.need_prompt ? <PromptInput /> : <></>}
<div className="flex gap-1"> <div className="flex gap-1"></div>
<Coffee />
<Shortcuts />
{serverConfig.disableModelSwitch ? <></> : <SettingsDialog />}
</div>
</header> </header>
) );
} };
export default Header export default Header;

View File

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

View File

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