This commit is contained in:
Qing 2023-11-23 16:28:47 +08:00
parent 7463a599a9
commit 43433c50eb
21 changed files with 776 additions and 524 deletions

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { nanoid } from "nanoid" import { nanoid } from "nanoid"
import { useRecoilState, useSetRecoilState } from "recoil" import { useSetRecoilState } from "recoil"
import { fileState, serverConfigState } from "@/lib/store" import { serverConfigState } from "@/lib/store"
import useInputImage from "@/hooks/useInputImage" import useInputImage from "@/hooks/useInputImage"
import { keepGUIAlive } from "@/lib/utils" import { keepGUIAlive } from "@/lib/utils"
import { getServerConfig, isDesktop } from "@/lib/api" import { getServerConfig, isDesktop } from "@/lib/api"
@ -9,6 +9,7 @@ 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"
const SUPPORTED_FILE_TYPE = [ const SUPPORTED_FILE_TYPE = [
"image/jpeg", "image/jpeg",
@ -18,13 +19,15 @@ const SUPPORTED_FILE_TYPE = [
"image/tiff", "image/tiff",
] ]
function Home() { function Home() {
const [file, setFile] = useRecoilState(fileState) const [file, setFile] = useStore((state) => [state.file, state.setFile])
const userInputImage = useInputImage() const userInputImage = useInputImage()
const setServerConfigState = useSetRecoilState(serverConfigState) const setServerConfigState = useSetRecoilState(serverConfigState)
// Set Input Image
useEffect(() => { useEffect(() => {
if (userInputImage) {
setFile(userInputImage) setFile(userInputImage)
}
}, [userInputImage, setFile]) }, [userInputImage, setFile])
// Keeping GUI Window Open // Keeping GUI Window Open

View File

@ -1,15 +1,6 @@
import React, { import { SyntheticEvent, useCallback, useEffect, useRef, useState } from "react"
SyntheticEvent, import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
useCallback, import { useToast } from "@/components/ui/use-toast"
useEffect,
useRef,
useState,
} from "react"
import {
CursorArrowRaysIcon,
ArrowsPointingOutIcon,
ArrowDownTrayIcon,
} from "@heroicons/react/24/outline"
import { import {
ReactZoomPanPinchRef, ReactZoomPanPinchRef,
TransformComponent, TransformComponent,
@ -19,7 +10,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"
import { useWindowSize } from "react-use" import { useWindowSize } from "react-use"
// import { useWindowSize, useKey, useKeyPressEvent } from "@uidotdev/usehooks" // import { useWindowSize, useKey, useKeyPressEvent } from "@uidotdev/usehooks"
import inpaint, { downloadToOutput, runPlugin } from "@/lib/api" import inpaint, { downloadToOutput, runPlugin } from "@/lib/api"
import { Button, IconButton } from "@/components/ui/button" import { IconButton } from "@/components/ui/button"
import { import {
askWritePermission, askWritePermission,
copyCanvasImage, copyCanvasImage,
@ -29,30 +20,22 @@ import {
loadImage, loadImage,
srcToFile, srcToFile,
} from "@/lib/utils" } from "@/lib/utils"
import { Eraser, Eye, Redo, Undo } from "lucide-react" import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react"
import { import {
appState,
brushSizeState,
croperState, croperState,
enableFileManagerState, enableFileManagerState,
fileState,
imageHeightState,
imageWidthState,
interactiveSegClicksState, interactiveSegClicksState,
isDiffusionModelsState, isDiffusionModelsState,
isEnableAutoSavingState, isEnableAutoSavingState,
isInpaintingState,
isInteractiveSegRunningState, isInteractiveSegRunningState,
isInteractiveSegState, isInteractiveSegState,
isPix2PixState, isPix2PixState,
isPluginRunningState, isPluginRunningState,
isProcessingState, isProcessingState,
negativePropmtState, negativePropmtState,
propmtState,
runManuallyState, runManuallyState,
seedState, seedState,
settingState, settingState,
toastState,
} from "@/lib/store" } from "@/lib/store"
// import Croper from "../Croper/Croper" // import Croper from "../Croper/Croper"
import emitter, { import emitter, {
@ -71,8 +54,9 @@ import { Slider } from "./ui/slider"
// import InteractiveSegReplaceModal from "../InteractiveSeg/ReplaceModal" // import InteractiveSegReplaceModal from "../InteractiveSeg/ReplaceModal"
import { PluginName } from "@/lib/types" import { PluginName } from "@/lib/types"
import { useHotkeys } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook"
import { useStore } from "@/lib/states"
const TOOLBAR_SIZE = 200 const TOOLBAR_HEIGHT = 200
const MIN_BRUSH_SIZE = 10 const MIN_BRUSH_SIZE = 10
const MAX_BRUSH_SIZE = 200 const MAX_BRUSH_SIZE = 200
const BRUSH_COLOR = "#ffcc00bb" const BRUSH_COLOR = "#ffcc00bb"
@ -110,15 +94,45 @@ function mouseXY(ev: SyntheticEvent) {
return { x: mouseEvent.offsetX, y: mouseEvent.offsetY } return { x: mouseEvent.offsetX, y: mouseEvent.offsetY }
} }
export default function Editor() { interface EditorProps {
const [file, setFile] = useRecoilState(fileState) file: File
const promptVal = useRecoilValue(propmtState) }
export default function Editor(props: EditorProps) {
const { file } = props
const { toast } = useToast()
const [
isInpainting,
imageWidth,
imageHeight,
baseBrushSize,
brushScale,
promptVal,
setImageSize,
setBrushSize,
setIsInpainting,
] = useStore((state) => [
state.isInpainting,
state.imageWidth,
state.imageHeight,
state.brushSize,
state.brushSizeScale,
state.prompt,
state.setImageSize,
state.setBrushSize,
state.setIsInpainting,
])
const brushSize = baseBrushSize * brushScale
// 纯 local state
const [showOriginal, setShowOriginal] = useState(false)
//
const negativePromptVal = useRecoilValue(negativePropmtState) const negativePromptVal = useRecoilValue(negativePropmtState)
const settings = useRecoilValue(settingState) const settings = useRecoilValue(settingState)
const [seedVal, setSeed] = useRecoilState(seedState) const [seedVal, setSeed] = useRecoilState(seedState)
const croperRect = useRecoilValue(croperState) const croperRect = useRecoilValue(croperState)
const setToastState = useSetRecoilState(toastState)
const [isInpainting, setIsInpainting] = useRecoilState(isInpaintingState)
const setIsPluginRunning = useSetRecoilState(isPluginRunningState) const setIsPluginRunning = useSetRecoilState(isPluginRunningState)
const isProcessing = useRecoilValue(isProcessingState) const isProcessing = useRecoilValue(isProcessingState)
const runMannually = useRecoilValue(runManuallyState) const runMannually = useRecoilValue(runManuallyState)
@ -152,8 +166,6 @@ export default function Editor() {
const [clicks, setClicks] = useRecoilState(interactiveSegClicksState) const [clicks, setClicks] = useRecoilState(interactiveSegClicksState)
const [brushSize, setBrushSize] = useRecoilState(brushSizeState)
const [original, isOriginalLoaded] = useImage(file) const [original, isOriginalLoaded] = useImage(file)
const [renders, setRenders] = useState<HTMLImageElement[]>([]) const [renders, setRenders] = useState<HTMLImageElement[]>([])
const [context, setContext] = useState<CanvasRenderingContext2D>() const [context, setContext] = useState<CanvasRenderingContext2D>()
@ -175,7 +187,6 @@ export default function Editor() {
brushSize: 20, brushSize: 20,
}) })
const [showOriginal, setShowOriginal] = useState(false)
const [scale, setScale] = useState<number>(1) const [scale, setScale] = useState<number>(1)
const [panned, setPanned] = useState<boolean>(false) const [panned, setPanned] = useState<boolean>(false)
const [minScale, setMinScale] = useState<number>(1.0) const [minScale, setMinScale] = useState<number>(1.0)
@ -198,10 +209,6 @@ export default function Editor() {
const enableFileManager = useRecoilValue(enableFileManagerState) const enableFileManager = useRecoilValue(enableFileManagerState)
const isEnableAutoSaving = useRecoilValue(isEnableAutoSavingState) const isEnableAutoSaving = useRecoilValue(isEnableAutoSavingState)
const [imageWidth, setImageWidth] = useRecoilState(imageWidthState)
const [imageHeight, setImageHeight] = useRecoilState(imageHeightState)
const app = useRecoilValue(appState)
const draw = useCallback( const draw = useCallback(
(render: HTMLImageElement, lineGroup: LineGroup) => { (render: HTMLImageElement, lineGroup: LineGroup) => {
if (!context) { if (!context) {
@ -422,11 +429,10 @@ export default function Editor() {
// clear redo stack // clear redo stack
resetRedoState() resetRedoState()
} catch (e: any) { } catch (e: any) {
setToastState({ toast({
open: true, variant: "destructive",
desc: e.message ? e.message : e.toString(), title: "Uh oh! Something went wrong.",
state: "error", description: e.message ? e.message : e.toString(),
duration: 4000,
}) })
drawOnCurrentRender([]) drawOnCurrentRender([])
} }
@ -464,11 +470,9 @@ export default function Editor() {
} else if (isPix2Pix) { } else if (isPix2Pix) {
runInpainting(false, undefined, null) runInpainting(false, undefined, null)
} else { } else {
setToastState({ toast({
open: true, variant: "destructive",
desc: "Please draw mask on picture", description: "Please draw mask on picture.",
state: "error",
duration: 1500,
}) })
} }
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE) emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
@ -554,11 +558,9 @@ export default function Editor() {
// 使用上一次 IS 的 mask 生成 // 使用上一次 IS 的 mask 生成
runInpainting(false, undefined, prevInteractiveSegMask, data.image) runInpainting(false, undefined, prevInteractiveSegMask, data.image)
} else { } else {
setToastState({ toast({
open: true, variant: "destructive",
desc: "Please draw mask on picture", description: "Please draw mask on picture.",
state: "error",
duration: 1500,
}) })
} }
}) })
@ -577,11 +579,9 @@ export default function Editor() {
// 使用上一次 IS 的 mask 生成 // 使用上一次 IS 的 mask 生成
runInpainting(false, undefined, prevInteractiveSegMask) runInpainting(false, undefined, prevInteractiveSegMask)
} else { } else {
setToastState({ toast({
open: true, variant: "destructive",
desc: "No mask to reuse", description: "No mask to reuse",
state: "error",
duration: 1500,
}) })
} }
}) })
@ -628,8 +628,7 @@ export default function Editor() {
const { blob } = res const { blob } = res
const newRender = new Image() const newRender = new Image()
await loadImage(newRender, blob) await loadImage(newRender, blob)
setImageHeight(newRender.height) setImageSize(newRender.height, newRender.width)
setImageWidth(newRender.width)
const newRenders = [...renders, newRender] const newRenders = [...renders, newRender]
setRenders(newRenders) setRenders(newRenders)
const newLineGroups = [...lineGroups, []] const newLineGroups = [...lineGroups, []]
@ -638,15 +637,12 @@ export default function Editor() {
const end = new Date() const end = new Date()
const time = end.getTime() - start.getTime() const time = end.getTime() - start.getTime()
setToastState({ toast({
open: true, description: `Run ${name} successfully in ${time / 1000}s`,
desc: `Run ${name} successfully in ${time / 1000}s`,
state: "success",
duration: 3000,
}) })
const rW = windowSize.width / newRender.width const rW = windowSize.width / newRender.width
const rH = (windowSize.height - TOOLBAR_SIZE) / newRender.height const rH = (windowSize.height - TOOLBAR_HEIGHT) / newRender.height
let s = 1.0 let s = 1.0
if (rW < 1 || rH < 1) { if (rW < 1 || rH < 1) {
s = Math.min(rW, rH) s = Math.min(rW, rH)
@ -655,11 +651,9 @@ export default function Editor() {
setScale(s) setScale(s)
viewportRef.current?.centerView(s, 1) viewportRef.current?.centerView(s, 1)
} catch (e: any) { } catch (e: any) {
setToastState({ toast({
open: true, variant: "destructive",
desc: e.message ? e.message : e.toString(), description: e.message ? e.message : e.toString(),
state: "error",
duration: 3000,
}) })
} finally { } finally {
setIsPluginRunning(false) setIsPluginRunning(false)
@ -671,8 +665,7 @@ export default function Editor() {
getCurrentRender, getCurrentRender,
setIsPluginRunning, setIsPluginRunning,
isProcessing, isProcessing,
setImageHeight, setImageSize,
setImageWidth,
lineGroups, lineGroups,
viewportRef, viewportRef,
windowSize, windowSize,
@ -753,11 +746,10 @@ export default function Editor() {
} }
const [width, height] = getCurrentWidthHeight() const [width, height] = getCurrentWidthHeight()
setImageWidth(width) setImageSize(width, height)
setImageHeight(height)
const rW = windowSize.width / width const rW = windowSize.width / width
const rH = (windowSize.height - TOOLBAR_SIZE) / height const rH = (windowSize.height - TOOLBAR_HEIGHT) / height
let s = 1.0 let s = 1.0
if (rW < 1 || rH < 1) { if (rW < 1 || rH < 1) {
@ -950,11 +942,9 @@ export default function Editor() {
} }
img.src = blob img.src = blob
} catch (e: any) { } catch (e: any) {
setToastState({ toast({
open: true, variant: "destructive",
desc: e.message ? e.message : e.toString(), description: e.message ? e.message : e.toString(),
state: "error",
duration: 4000,
}) })
} }
setIsInteractiveSegRunning(false) setIsInteractiveSegRunning(false)
@ -1280,18 +1270,14 @@ export default function Editor() {
if ((enableFileManager || isEnableAutoSaving) && renders.length > 0) { if ((enableFileManager || isEnableAutoSaving) && renders.length > 0) {
try { try {
downloadToOutput(renders[renders.length - 1], file.name, file.type) downloadToOutput(renders[renders.length - 1], file.name, file.type)
setToastState({ toast({
open: true, description: "Save image success",
desc: `Save image success`,
state: "success",
duration: 2000,
}) })
} catch (e: any) { } catch (e: any) {
setToastState({ toast({
open: true, variant: "destructive",
desc: e.message ? e.message : e.toString(), title: "Uh oh! Something went wrong.",
state: "error", description: e.message ? e.message : e.toString(),
duration: 2000,
}) })
} }
return return
@ -1439,6 +1425,15 @@ export default function Editor() {
} }
} }
const renderBrush = (style: any) => {
return (
<div
className="absolute rounded-[50%] border-[1px] border-[solid] border-[#ffcc00] pointer-events-none bg-[#ffcc00bb]"
style={style}
/>
)
}
const handleSliderChange = (value: number) => { const handleSliderChange = (value: number) => {
setBrushSize(value) setBrushSize(value)
@ -1591,24 +1586,16 @@ export default function Editor() {
{showBrush && {showBrush &&
!isInpainting && !isInpainting &&
!isPanning && !isPanning &&
(isInteractiveSeg ? ( (isInteractiveSeg
renderInteractiveSegCursor() ? renderInteractiveSegCursor()
) : ( : renderBrush(
<div getBrushStyle(
className="absolute rounded-[50%] border-[1px] border-[solid] pointer-events-none"
style={getBrushStyle(
isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.x : x, isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.x : x,
isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.y : y isChangingBrushSizeByMouse ? changeBrushSizeByMouseInit.y : y
)} )
/>
))} ))}
{showRefBrush && ( {showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
<div
className="absolute rounded-[50%] border-[1px] border-[solid] pointer-events-none"
style={getBrushStyle(windowCenterX, windowCenterY)}
/>
)}
<div className="fixed flex bottom-10 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md"> <div className="fixed flex bottom-10 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md">
<Slider <Slider
@ -1617,7 +1604,7 @@ export default function Editor() {
min={MIN_BRUSH_SIZE} min={MIN_BRUSH_SIZE}
max={MAX_BRUSH_SIZE} max={MAX_BRUSH_SIZE}
step={1} step={1}
value={[brushSize]} value={[baseBrushSize]}
onValueChange={(vals) => handleSliderChange(vals[0])} onValueChange={(vals) => handleSliderChange(vals[0])}
onClick={() => setShowRefBrush(false)} onClick={() => setShowRefBrush(false)}
/> />
@ -1627,7 +1614,7 @@ export default function Editor() {
disabled={scale === minScale && panned === false} disabled={scale === minScale && panned === false}
onClick={resetZoom} onClick={resetZoom}
> >
<ArrowsPointingOutIcon /> <Expand />
</IconButton> </IconButton>
<IconButton tooltip="Undo" onClick={undo} disabled={disableUndo()}> <IconButton tooltip="Undo" onClick={undo} disabled={disableUndo()}>
<Undo /> <Undo />
@ -1662,10 +1649,9 @@ export default function Editor() {
disabled={!renders.length} disabled={!renders.length}
onClick={download} onClick={download}
> >
<ArrowDownTrayIcon /> <Download />
</IconButton> </IconButton>
{settings.runInpaintingManually && !isDiffusionModels && (
<IconButton <IconButton
tooltip="Run Inpainting" tooltip="Run Inpainting"
disabled={ disabled={
@ -1679,7 +1665,6 @@ export default function Editor() {
> >
<Eraser /> <Eraser />
</IconButton> </IconButton>
)}
</div> </div>
</div> </div>
{/* <InteractiveSegReplaceModal {/* <InteractiveSegReplaceModal

View File

@ -7,28 +7,16 @@ import {
FormEvent, FormEvent,
} from "react" } from "react"
import _ from "lodash" import _ from "lodash"
import { useRecoilState } from "recoil"
import PhotoAlbum from "react-photo-album" import PhotoAlbum from "react-photo-album"
import { import { BarsArrowDownIcon, BarsArrowUpIcon } from "@heroicons/react/24/outline"
BarsArrowDownIcon,
BarsArrowUpIcon,
FolderIcon,
} from "@heroicons/react/24/outline"
import { import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
ViewHorizontalIcon, ViewHorizontalIcon,
ViewGridIcon, ViewGridIcon,
} from "@radix-ui/react-icons" } from "@radix-ui/react-icons"
import { useDebounce, useToggle } from "react-use" import { useToggle } from "react-use"
import { useDebounce } from "@uidotdev/usehooks"
import FlexSearch from "flexsearch/dist/flexsearch.bundle.js" import FlexSearch from "flexsearch/dist/flexsearch.bundle.js"
import {
fileManagerLayout,
fileManagerSearchText,
fileManagerSortBy,
fileManagerSortOrder,
SortBy,
SortOrder,
} from "@/lib/store"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { API_ENDPOINT, getMedias } from "@/lib/api" import { API_ENDPOINT, getMedias } from "@/lib/api"
import { IconButton } from "./ui/button" import { IconButton } from "./ui/button"
@ -45,6 +33,9 @@ import {
import { ScrollArea } from "./ui/scroll-area" import { ScrollArea } from "./ui/scroll-area"
import { DialogTrigger } from "@radix-ui/react-dialog" import { DialogTrigger } from "@radix-ui/react-dialog"
import { useHotkeys } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook"
import { useStore } from "@/lib/states"
import { SortBy, SortOrder } from "@/lib/types"
import { FolderClosed } from "lucide-react"
interface Photo { interface Photo {
src: string src: string
@ -83,6 +74,20 @@ export default function FileManager(props: Props) {
const { onPhotoClick, photoWidth } = props const { onPhotoClick, photoWidth } = props
const [open, toggleOpen] = useToggle(false) const [open, toggleOpen] = useToggle(false)
const [
fileManagerState,
setFileManagerLayout,
setFileManagerSortBy,
setFileManagerSortOrder,
setFileManagerSearchText,
] = useStore((state) => [
state.fileManagerState,
state.setFileManagerLayout,
state.setFileManagerSortBy,
state.setFileManagerSortOrder,
state.setFileManagerSearchText,
])
useHotkeys("f", () => { useHotkeys("f", () => {
toggleOpen() toggleOpen()
}) })
@ -91,25 +96,11 @@ export default function FileManager(props: Props) {
const [scrollTop, setScrollTop] = useState(0) const [scrollTop, setScrollTop] = useState(0)
const [closeScrollTop, setCloseScrollTop] = useState(0) const [closeScrollTop, setCloseScrollTop] = useState(0)
const [sortBy, setSortBy] = useRecoilState<SortBy>(fileManagerSortBy)
const [sortOrder, setSortOrder] = useRecoilState(fileManagerSortOrder)
const [layout, setLayout] = useRecoilState(fileManagerLayout)
const [debouncedSearchText, setDebouncedSearchText] = useRecoilState(
fileManagerSearchText
)
const ref = useRef(null) const ref = useRef(null)
const [searchText, setSearchText] = useState(debouncedSearchText) const debouncedSearchText = useDebounce(fileManagerState.searchText, 300)
const [tab, setTab] = useState(IMAGE_TAB) const [tab, setTab] = useState(IMAGE_TAB)
const [photos, setPhotos] = useState<Photo[]>([]) const [photos, setPhotos] = useState<Photo[]>([])
const [, cancel] = useDebounce(
() => {
setDebouncedSearchText(searchText)
},
300,
[searchText]
)
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setCloseScrollTop(scrollTop) setCloseScrollTop(scrollTop)
@ -153,7 +144,11 @@ export default function FileManager(props: Props) {
) )
} }
filteredFilenames = _.orderBy(filteredFilenames, sortBy, sortOrder) filteredFilenames = _.orderBy(
filteredFilenames,
fileManagerState.sortBy,
fileManagerState.sortOrder
)
const newPhotos = filteredFilenames.map((filename: Filename) => { const newPhotos = filteredFilenames.map((filename: Filename) => {
const width = photoWidth const width = photoWidth
@ -171,7 +166,7 @@ export default function FileManager(props: Props) {
} }
} }
fetchData() fetchData()
}, [tab, debouncedSearchText, sortBy, sortOrder, photoWidth, open]) }, [tab, debouncedSearchText, fileManagerState, photoWidth, open])
const onScroll = (event: SyntheticEvent) => { const onScroll = (event: SyntheticEvent) => {
setScrollTop(event.currentTarget.scrollTop) setScrollTop(event.currentTarget.scrollTop)
@ -190,19 +185,21 @@ export default function FileManager(props: Props) {
<IconButton <IconButton
tooltip="Rows layout" tooltip="Rows layout"
onClick={() => { onClick={() => {
setLayout("rows") setFileManagerLayout("rows")
}} }}
> >
<ViewHorizontalIcon <ViewHorizontalIcon
className={layout !== "rows" ? "opacity-50" : ""} className={fileManagerState.layout !== "rows" ? "opacity-50" : ""}
/> />
</IconButton> </IconButton>
<IconButton <IconButton
tooltip="Grid layout" tooltip="Grid layout"
onClick={() => { onClick={() => {
setLayout("masonry") setFileManagerLayout("masonry")
}} }}
className={layout !== "masonry" ? "opacity-50" : ""} className={
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
}
> >
<ViewGridIcon /> <ViewGridIcon />
</IconButton> </IconButton>
@ -213,9 +210,9 @@ export default function FileManager(props: Props) {
return ( return (
<Dialog open={open} onOpenChange={toggleOpen}> <Dialog open={open} onOpenChange={toggleOpen}>
<DialogTrigger> <DialogTrigger asChild>
<IconButton tooltip="File Manager"> <IconButton tooltip="File Manager">
<FolderIcon /> <FolderClosed />
</IconButton> </IconButton>
</DialogTrigger> </DialogTrigger>
<DialogContent className="h-4/5 max-w-6xl"> <DialogContent className="h-4/5 max-w-6xl">
@ -225,14 +222,14 @@ export default function FileManager(props: Props) {
<MagnifyingGlassIcon className="absolute left-[8px]" /> <MagnifyingGlassIcon className="absolute left-[8px]" />
<Input <Input
ref={ref} ref={ref}
value={searchText} value={fileManagerState.searchText}
className="w-[250px] pl-[30px]" className="w-[250px] pl-[30px]"
tabIndex={-1} tabIndex={-1}
onInput={(evt: FormEvent<HTMLInputElement>) => { onInput={(evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault() evt.preventDefault()
evt.stopPropagation() evt.stopPropagation()
const target = evt.target as HTMLInputElement const target = evt.target as HTMLInputElement
setSearchText(target.value) setFileManagerSearchText(target.value)
}} }}
placeholder="Search by file name" placeholder="Search by file name"
/> />
@ -248,17 +245,17 @@ export default function FileManager(props: Props) {
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex gap-1"> <div className="flex gap-1">
<Select <Select
value={SortByMap[sortBy]} value={SortByMap[fileManagerState.sortBy]}
onValueChange={(val) => { onValueChange={(val) => {
switch (val) { switch (val) {
case SORT_BY_NAME: case SORT_BY_NAME:
setSortBy(SortBy.NAME) setFileManagerSortBy(SortBy.NAME)
break break
case SORT_BY_CREATED_TIME: case SORT_BY_CREATED_TIME:
setSortBy(SortBy.CTIME) setFileManagerSortBy(SortBy.CTIME)
break break
case SORT_BY_MODIFIED_TIME: case SORT_BY_MODIFIED_TIME:
setSortBy(SortBy.MTIME) setFileManagerSortBy(SortBy.MTIME)
break break
default: default:
break break
@ -279,11 +276,11 @@ export default function FileManager(props: Props) {
</SelectContent> </SelectContent>
</Select> </Select>
{sortOrder === SortOrder.DESCENDING ? ( {fileManagerState.sortOrder === SortOrder.DESCENDING ? (
<IconButton <IconButton
tooltip="Descending Order" tooltip="Descending Order"
onClick={() => { onClick={() => {
setSortOrder(SortOrder.ASCENDING) setFileManagerSortOrder(SortOrder.ASCENDING)
}} }}
> >
<BarsArrowDownIcon /> <BarsArrowDownIcon />
@ -292,7 +289,7 @@ export default function FileManager(props: Props) {
<IconButton <IconButton
tooltip="Ascending Order" tooltip="Ascending Order"
onClick={() => { onClick={() => {
setSortOrder(SortOrder.DESCENDING) setFileManagerSortOrder(SortOrder.DESCENDING)
}} }}
> >
<BarsArrowUpIcon /> <BarsArrowUpIcon />
@ -308,7 +305,7 @@ export default function FileManager(props: Props) {
ref={onRefChange} ref={onRefChange}
> >
<PhotoAlbum <PhotoAlbum
layout={layout} layout={fileManagerState.layout}
photos={photos} photos={photos}
spacing={12} spacing={12}
padding={0} padding={0}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react" import { useState } from "react"
import useResolution from "@/hooks/useResolution" import useResolution from "@/hooks/useResolution"
type FileSelectProps = { type FileSelectProps = {
@ -34,10 +34,10 @@ export default function FileSelect(props: FileSelectProps) {
} }
return ( return (
<div className="absolute flex w-screen h-screen justify-center items-center "> <div className="absolute flex w-screen h-screen justify-center items-center pointer-events-none">
<label <label
htmlFor={uploadElemId} htmlFor={uploadElemId}
className="grid cursor-pointer border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:cursor-pointer hover:bg-primary hover:text-primary-foreground" className="grid border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:bg-primary hover:text-primary-foreground pointer-events-auto"
> >
<div <div
className="grid p-16 w-full h-full" className="grid p-16 w-full h-full"

View File

@ -1,19 +1,15 @@
import { FolderIcon, PhotoIcon } from "@heroicons/react/24/outline"
import { PlayIcon } from "@radix-ui/react-icons" import { PlayIcon } from "@radix-ui/react-icons"
import React, { useCallback, useState } from "react" import React, { useCallback, useState } from "react"
import { useRecoilState, useRecoilValue } from "recoil" import { useRecoilState, useRecoilValue } from "recoil"
import { useHotkeys } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook"
import { import {
enableFileManagerState, enableFileManagerState,
fileState,
isInpaintingState,
isPix2PixState, isPix2PixState,
isSDState, isSDState,
maskState, maskState,
runManuallyState, runManuallyState,
showFileManagerState,
} from "@/lib/store" } from "@/lib/store"
import { Button, IconButton, ImageUploadButton } from "@/components/ui/button" import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts" import Shortcuts from "@/components/Shortcuts"
// import SettingIcon from "../Settings/SettingIcon" // import SettingIcon from "../Settings/SettingIcon"
// import PromptInput from "./PromptInput" // import PromptInput from "./PromptInput"
@ -28,15 +24,19 @@ import { useImage } from "@/hooks/useImage"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
import PromptInput from "./PromptInput" import PromptInput from "./PromptInput"
import { RotateCw } from "lucide-react" import { RotateCw, Image } from "lucide-react"
import FileManager from "./FileManager" import FileManager from "./FileManager"
import { getMediaFile } from "@/lib/api" import { getMediaFile } from "@/lib/api"
import { useStore } from "@/lib/states"
const Header = () => { const Header = () => {
const isInpainting = useRecoilValue(isInpaintingState) const [file, isInpainting, setFile] = useStore((state) => [
const [file, setFile] = useRecoilState(fileState) state.file,
state.isInpainting,
state.setFile,
])
const [mask, setMask] = useRecoilState(maskState) const [mask, setMask] = useRecoilState(maskState)
const [maskImage, maskImageLoaded] = useImage(mask) // const [maskImage, maskImageLoaded] = useImage(mask)
const isSD = useRecoilValue(isSDState) const isSD = useRecoilValue(isSDState)
const isPix2Pix = useRecoilValue(isPix2PixState) const isPix2Pix = useRecoilValue(isPix2PixState)
const runManually = useRecoilValue(runManuallyState) const runManually = useRecoilValue(runManuallyState)
@ -88,15 +88,13 @@ const Header = () => {
setFile(file) setFile(file)
}} }}
> >
<PhotoIcon /> <Image />
</ImageUploadButton> </ImageUploadButton>
<div <div
className="flex items-center"
style={{ style={{
visibility: file ? "visible" : "hidden", visibility: file ? "visible" : "hidden",
display: "flex",
justifyContent: "center",
alignItems: "center",
}} }}
> >
<ImageUploadButton <ImageUploadButton
@ -133,13 +131,13 @@ const Header = () => {
<PlayIcon /> <PlayIcon />
</IconButton> </IconButton>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> {/* <PopoverContent>
{maskImageLoaded ? ( {maskImageLoaded ? (
<img src={maskImage.src} alt="Custom mask" /> <img src={maskImage.src} alt="Custom mask" />
) : ( ) : (
<></> <></>
)} )}
</PopoverContent> </PopoverContent> */}
</Popover> </Popover>
) : ( ) : (
<></> <></>
@ -159,13 +157,11 @@ const Header = () => {
{isSD ? <PromptInput /> : <></>} {isSD ? <PromptInput /> : <></>}
<div className="header-icons-wrapper">
{/* <CoffeeIcon /> */} {/* <CoffeeIcon /> */}
<div className="header-icons"> <div>
<Shortcuts /> <Shortcuts />
{/* <SettingIcon /> */} {/* <SettingIcon /> */}
</div> </div>
</div>
</header> </header>
) )
} }

View File

@ -0,0 +1,20 @@
import { useStore } from "@/lib/states"
const ImageSize = () => {
const [imageWidth, imageHeight] = useStore((state) => [
state.imageWidth,
state.imageHeight,
])
if (!imageWidth || !imageHeight) {
return null
}
return (
<div className="border rounded-lg px-2 py-[6px] z-10">
{imageWidth}x{imageHeight}
</div>
)
}
export default ImageSize

View File

@ -0,0 +1,131 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { Button } from "./ui/button"
import { Fullscreen, MousePointerClick, Slice, Smile } from "lucide-react"
import { MixIcon } from "@radix-ui/react-icons"
export enum PluginName {
RemoveBG = "RemoveBG",
AnimeSeg = "AnimeSeg",
RealESRGAN = "RealESRGAN",
GFPGAN = "GFPGAN",
RestoreFormer = "RestoreFormer",
InteractiveSeg = "InteractiveSeg",
}
const pluginMap = {
[PluginName.RemoveBG]: {
IconClass: Slice,
showName: "RemoveBG",
},
[PluginName.AnimeSeg]: {
IconClass: Slice,
showName: "Anime Segmentation",
},
[PluginName.RealESRGAN]: {
IconClass: Fullscreen,
showName: "RealESRGAN 4x",
},
[PluginName.GFPGAN]: {
IconClass: Smile,
showName: "GFPGAN",
},
[PluginName.RestoreFormer]: {
IconClass: Smile,
showName: "RestoreFormer",
},
[PluginName.InteractiveSeg]: {
IconClass: MousePointerClick,
showName: "Interactive Segmentation",
},
}
const Plugins = () => {
// const [open, toggleOpen] = useToggle(true)
// const serverConfig = useRecoilValue(serverConfigState)
// const isProcessing = useRecoilValue(isProcessingState)
const plugins = [
PluginName.RemoveBG,
PluginName.AnimeSeg,
PluginName.RealESRGAN,
PluginName.GFPGAN,
PluginName.RestoreFormer,
PluginName.InteractiveSeg,
]
if (plugins.length === 0) {
return null
}
const onPluginClick = (pluginName: string) => {
// if (!disabled) {
// emitter.emit(pluginName)
// }
}
const onRealESRGANClick = (upscale: number) => {
// if (!disabled) {
// emitter.emit(PluginName.RealESRGAN, { upscale })
// }
}
const renderRealESRGANPlugin = () => {
return (
<DropdownMenuSub key="RealESRGAN">
<DropdownMenuSubTrigger>
<div className="flex gap-2 items-center">
<Fullscreen />
RealESRGAN
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => onRealESRGANClick(2)}>
upscale 2x
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onRealESRGANClick(4)}>
upscale 4x
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
const renderPlugins = () => {
return plugins.map((plugin: PluginName) => {
const { IconClass, showName } = pluginMap[plugin]
if (plugin === PluginName.RealESRGAN) {
return renderRealESRGANPlugin()
}
return (
<DropdownMenuItem key={plugin} onClick={() => onPluginClick(plugin)}>
<div className="flex gap-2 items-center">
<IconClass className="p-1" />
{showName}
</div>
</DropdownMenuItem>
)
})
}
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="border rounded-lg z-10">
<Button variant="ghost" size="icon" asChild>
<MixIcon className="p-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start">
{renderPlugins()}
</DropdownMenuContent>
</DropdownMenu>
)
}
export default Plugins

View File

@ -1,18 +1,19 @@
import React, { FormEvent } from "react" import React, { FormEvent } from "react"
import { useRecoilState, useRecoilValue } from "recoil"
import emitter, { import emitter, {
DREAM_BUTTON_MOUSE_ENTER, DREAM_BUTTON_MOUSE_ENTER,
DREAM_BUTTON_MOUSE_LEAVE, DREAM_BUTTON_MOUSE_LEAVE,
EVENT_PROMPT, EVENT_PROMPT,
} from "@/lib/event" } from "@/lib/event"
import { appState, isInpaintingState, propmtState } from "@/lib/store"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import { Input } from "./ui/input" import { Input } from "./ui/input"
import { useStore } from "@/lib/states"
const PromptInput = () => { const PromptInput = () => {
const app = useRecoilValue(appState) const [isInpainting, prompt, setPrompt] = useStore((state) => [
const [prompt, setPrompt] = useRecoilState(propmtState) state.isInpainting,
const isInpainting = useRecoilValue(isInpaintingState) state.prompt,
state.setPrompt,
])
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => { const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault() evt.preventDefault()
@ -22,7 +23,7 @@ const PromptInput = () => {
} }
const handleRepaintClick = () => { const handleRepaintClick = () => {
if (prompt.length !== 0 && !app.isInpainting) { if (prompt.length !== 0 && isInpainting) {
emitter.emit(EVENT_PROMPT) emitter.emit(EVENT_PROMPT)
} }
} }
@ -53,7 +54,7 @@ const PromptInput = () => {
<Button <Button
size="sm" size="sm"
onClick={handleRepaintClick} onClick={handleRepaintClick}
disabled={prompt.length === 0 || app.isInpainting} disabled={prompt.length === 0 || isInpainting}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >

View File

@ -1,5 +1,4 @@
import { Keyboard } from "lucide-react" import { Keyboard } from "lucide-react"
import useHotKey from "@/hooks/useHotkey"
import { IconButton } from "@/components/ui/button" import { IconButton } from "@/components/ui/button"
import { useToggle } from "@uidotdev/usehooks" import { useToggle } from "@uidotdev/usehooks"
import { import {
@ -10,6 +9,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "./ui/dialog" } from "./ui/dialog"
import { useHotkeys } from "react-hotkeys-hook"
interface ShortcutProps { interface ShortcutProps {
content: string content: string
@ -21,7 +21,7 @@ function ShortCut(props: ShortcutProps) {
return ( return (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="shortcut-description">{content}</div> <div>{content}</div>
<div className="flex gap-[8px]"> <div className="flex gap-[8px]">
{keys.map((k) => ( {keys.map((k) => (
// TODO: 优化快捷键显示 // TODO: 优化快捷键显示
@ -49,13 +49,13 @@ const CmdOrCtrl = () => {
export function Shortcuts() { export function Shortcuts() {
const [open, toggleOpen] = useToggle(false) const [open, toggleOpen] = useToggle(false)
useHotKey("h", () => { useHotkeys("h", () => {
toggleOpen() toggleOpen()
}) })
return ( return (
<Dialog open={open} onOpenChange={toggleOpen}> <Dialog open={open} onOpenChange={toggleOpen}>
<DialogTrigger> <DialogTrigger asChild>
<IconButton tooltip="Hotkeys"> <IconButton tooltip="Hotkeys">
<Keyboard /> <Keyboard />
</IconButton> </IconButton>
@ -63,7 +63,7 @@ export function Shortcuts() {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Hotkeys</DialogTitle> <DialogTitle>Hotkeys</DialogTitle>
<DialogDescription className="flex gap-2 flex-col pt-4"> <div className="flex gap-2 flex-col pt-4">
<ShortCut content="Pan" keys={["Space + Drag"]} /> <ShortCut content="Pan" keys={["Space + Drag"]} />
<ShortCut content="Reset Zoom/Pan" keys={["Esc"]} /> <ShortCut content="Reset Zoom/Pan" keys={["Esc"]} />
<ShortCut content="Decrease Brush Size" keys={["["]} /> <ShortCut content="Decrease Brush Size" keys={["["]} />
@ -87,7 +87,7 @@ export function Shortcuts() {
<ShortCut content="Toggle Hotkeys Dialog" keys={["H"]} /> <ShortCut content="Toggle Hotkeys Dialog" keys={["H"]} />
<ShortCut content="Toggle Settings Dialog" keys={["S"]} /> <ShortCut content="Toggle Settings Dialog" keys={["S"]} />
<ShortCut content="Toggle File Manager" keys={["F"]} /> <ShortCut content="Toggle File Manager" keys={["F"]} />
</DialogDescription> </div>
</DialogHeader> </DialogHeader>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,36 +1,28 @@
import React, { useEffect } from "react" import { useEffect } from "react"
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil" import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"
import Editor from "./Editor" import Editor from "./Editor"
// import SettingModal from "./Settings/SettingsModal" // import SettingModal from "./Settings/SettingsModal"
// import Toast from "./shared/Toast"
import { import {
AIModel, AIModel,
fileState,
isPaintByExampleState, isPaintByExampleState,
isPix2PixState, isPix2PixState,
isSDState, isSDState,
settingState, settingState,
showFileManagerState,
toastState,
} from "@/lib/store" } from "@/lib/store"
import { import { currentModel, modelDownloaded, switchModel } from "@/lib/api"
currentModel, import { useStore } from "@/lib/states"
getMediaFile, import ImageSize from "./ImageSize"
modelDownloaded, import Plugins from "./Plugins"
switchModel,
} from "@/lib/api"
// import SidePanel from "./SidePanel/SidePanel" // import SidePanel from "./SidePanel/SidePanel"
// import PESidePanel from "./SidePanel/PESidePanel" // import PESidePanel from "./SidePanel/PESidePanel"
// import FileManager from "./FileManager/FileManager"
// import P2PSidePanel from "./SidePanel/P2PSidePanel" // import P2PSidePanel from "./SidePanel/P2PSidePanel"
// import Plugins from "./Plugins/Plugins" // import Plugins from "./Plugins/Plugins"
// import Flex from "./shared/Layout" // import Flex from "./shared/Layout"
// import ImageSize from "./ImageSize/ImageSize" // import ImageSize from "./ImageSize/ImageSize"
const Workspace = () => { const Workspace = () => {
const setFile = useSetRecoilState(fileState) const file = useStore((state) => state.file)
const [settings, setSettingState] = useRecoilState(settingState) const [settings, setSettingState] = useRecoilState(settingState)
const [toastVal, setToastState] = useRecoilState(toastState)
const isSD = useRecoilValue(isSDState) const isSD = useRecoilValue(isSDState)
const isPaintByExample = useRecoilValue(isPaintByExampleState) const isPaintByExample = useRecoilValue(isPaintByExampleState)
const isPix2Pix = useRecoilValue(isPix2PixState) const isPix2Pix = useRecoilValue(isPix2PixState)
@ -53,33 +45,34 @@ const Workspace = () => {
loadingDuration = 9999999999 loadingDuration = 9999999999
} }
setToastState({ // TODO 修改成 Modal
open: true, // setToastState({
desc: loadingMessage, // open: true,
state: "loading", // desc: loadingMessage,
duration: loadingDuration, // state: "loading",
}) // duration: loadingDuration,
// })
switchModel(model) switchModel(model)
.then((res) => { .then((res) => {
if (res.ok) { if (res.ok) {
setToastState({ // setToastState({
open: true, // open: true,
desc: `Switch to ${model} model success`, // desc: `Switch to ${model} model success`,
state: "success", // state: "success",
duration: 3000, // duration: 3000,
}) // })
} else { } else {
throw new Error("Server error") throw new Error("Server error")
} }
}) })
.catch(() => { .catch(() => {
setToastState({ // setToastState({
open: true, // open: true,
desc: `Switch to ${model} model failed`, // desc: `Switch to ${model} model failed`,
state: "error", // state: "error",
duration: 3000, // duration: 3000,
}) // })
setSettingState((old) => { setSettingState((old) => {
return { ...old, model: curModel as AIModel } return { ...old, model: curModel as AIModel }
}) })
@ -101,12 +94,12 @@ const Workspace = () => {
{/* {isSD ? <SidePanel /> : <></>} {/* {isSD ? <SidePanel /> : <></>}
{isPaintByExample ? <PESidePanel /> : <></>} {isPaintByExample ? <PESidePanel /> : <></>}
{isPix2Pix ? <P2PSidePanel /> : <></>} {isPix2Pix ? <P2PSidePanel /> : <></>}
<Flex style={{ position: "absolute", top: 68, left: 24, gap: 12 }}> {/* <SettingModal onClose={onSettingClose} /> */}
<div className="flex gap-3 absolute top-[68px] left-[24px] items-center">
<Plugins /> <Plugins />
<ImageSize /> <ImageSize />
</Flex> </div>
{/* <SettingModal onClose={onSettingClose} /> */} {file ? <Editor file={file} /> : <></>}
<Editor />
</> </>
) )
} }

View File

@ -65,15 +65,22 @@ export interface IconButtonProps extends ButtonProps {
tooltip: string tooltip: string
} }
const IconButton = (props: IconButtonProps) => { const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
const { tooltip, children, ...rest } = props ({ tooltip, children, ...rest }, ref) => {
return ( return (
<> <>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="icon" {...rest} asChild> <Button
<div className="p-[8px]">{children}</div> variant="ghost"
size="icon"
{...rest}
ref={ref}
tabIndex={-1}
className="cursor-default"
>
<div className="icon-button-icon-wrapper">{children}</div>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@ -84,6 +91,7 @@ const IconButton = (props: IconButtonProps) => {
</> </>
) )
} }
)
export interface UploadButtonProps extends IconButtonProps { export interface UploadButtonProps extends IconButtonProps {
onFileUpload: (file: File) => void onFileUpload: (file: File) => void
@ -106,7 +114,9 @@ const ImageUploadButton = (props: UploadButtonProps) => {
return ( return (
<> <>
<label htmlFor={uploadElemId}> <label htmlFor={uploadElemId}>
<IconButton {...rest}>{children}</IconButton> <IconButton {...rest} asChild>
{children}
</IconButton>
</label> </label>
<Input <Input
style={{ display: "none" }} style={{ display: "none" }}

View File

@ -39,6 +39,7 @@ const DialogContent = React.forwardRef<
"fixed left-[50%] top-[50%] z-50 flex flex-col w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 flex flex-col w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
onCloseAutoFocus={(event) => event.preventDefault()}
{...props} {...props}
> >
{children} {children}

View File

@ -0,0 +1,203 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -26,6 +26,11 @@
transform: scale(1.03); transform: scale(1.03);
} }
.icon-button-icon-wrapper svg {
stroke-width: 1px;
}
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;

View File

@ -1,22 +0,0 @@
import { Options, useHotkeys } from "react-hotkeys-hook"
import { useRecoilValue } from "recoil"
import { appState } from "@/lib/store"
const useHotKey = (
keys: string,
callback: any,
options?: Options,
deps?: any[]
) => {
const app = useRecoilValue(appState)
const ref = useHotkeys(
keys,
callback,
{ ...options, enabled: !app.disableShortCuts },
deps
)
return ref
}
export default useHotKey

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
function useImage(file?: File): [HTMLImageElement, boolean] { function useImage(file: File | null): [HTMLImageElement, boolean] {
const [image] = useState(new Image()) const [image] = useState(new Image())
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => { useEffect(() => {
if (file === undefined) { if (!file) {
return return
} }
image.onload = () => { image.onload = () => {

View File

@ -2,7 +2,7 @@ import { API_ENDPOINT } from "@/lib/api"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
export default function useInputImage() { export default function useInputImage() {
const [inputImage, setInputImage] = useState<File>() const [inputImage, setInputImage] = useState<File | null>(null)
const fetchInputImage = useCallback(() => { const fetchInputImage = useCallback(() => {
const headers = new Headers() const headers = new Headers()

2
web_app/src/lib/const.ts Normal file
View File

@ -0,0 +1,2 @@
export const ACCENT_COLOR = "#ffcc00bb"
export const DEFAULT_BRUSH_SIZE = 40

118
web_app/src/lib/states.ts Normal file
View File

@ -0,0 +1,118 @@
import { create, StoreApi, UseBoundStore } from "zustand"
import { persist } from "zustand/middleware"
import { immer } from "zustand/middleware/immer"
import { SortBy, SortOrder } from "./types"
import { DEFAULT_BRUSH_SIZE } from "./const"
type FileManagerState = {
sortBy: SortBy
sortOrder: SortOrder
layout: "rows" | "masonry"
searchText: string
}
type AppState = {
file: File | null
imageHeight: number
imageWidth: number
brushSize: number
brushSizeScale: number
isInpainting: boolean
isInteractiveSeg: boolean // 是否正处于 sam 状态
isInteractiveSegRunning: boolean
interactiveSegClicks: number[][]
prompt: string
fileManagerState: FileManagerState
}
type AppAction = {
setFile: (file: File) => void
setIsInpainting: (newValue: boolean) => void
setBrushSize: (newValue: number) => void
setImageSize: (width: number, height: number) => void
setFileManagerSortBy: (newValue: SortBy) => void
setFileManagerSortOrder: (newValue: SortOrder) => void
setFileManagerLayout: (
newValue: AppState["fileManagerState"]["layout"]
) => void
setFileManagerSearchText: (newValue: string) => void
setPrompt: (newValue: string) => void
}
export const useStore = create<AppState & AppAction>()(
immer(
persist(
(set, get) => ({
file: null,
imageHeight: 0,
imageWidth: 0,
brushSize: DEFAULT_BRUSH_SIZE,
brushSizeScale: 1,
isInpainting: false,
isInteractiveSeg: false,
isInteractiveSegRunning: false,
interactiveSegClicks: [],
prompt: "",
fileManagerState: {
sortBy: SortBy.CTIME,
sortOrder: SortOrder.DESCENDING,
layout: "masonry",
searchText: "",
},
setIsInpainting: (newValue: boolean) =>
set((state: AppState) => {
state.isInpainting = newValue
}),
setFile: (file: File) =>
set((state: AppState) => {
// TODO: 清空各种状态
state.file = file
}),
setBrushSize: (newValue: number) =>
set((state: AppState) => {
state.brushSize = newValue
}),
setImageSize: (width: number, height: number) => {
// 根据图片尺寸调整 brushSize 的 scale
set((state: AppState) => {
state.imageWidth = width
state.imageHeight = height
state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512
})
},
setPrompt: (newValue: string) =>
set((state: AppState) => {
state.prompt = newValue
}),
setFileManagerSortBy: (newValue: SortBy) =>
set((state: AppState) => {
state.fileManagerState.sortBy = newValue
}),
setFileManagerSortOrder: (newValue: SortOrder) =>
set((state: AppState) => {
state.fileManagerState.sortOrder = newValue
}),
setFileManagerLayout: (newValue: "rows" | "masonry") =>
set((state: AppState) => {
state.fileManagerState.layout = newValue
}),
setFileManagerSearchText: (newValue: string) =>
set((state: AppState) => {
state.fileManagerState.searchText = newValue
}),
}),
{
name: "ZUSTAND_STATE", // name of the item in the storage (must be unique)
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
["fileManagerState", "prompt"].includes(key)
)
),
}
)
)
)

View File

@ -1,23 +1,6 @@
import { atom, selector } from "recoil" import { atom, selector } from "recoil"
import _ from "lodash" import _ from "lodash"
import { CV2Flag, HDStrategy, LDMSampler, ModelsHDSettings } from "./types"
export enum HDStrategy {
ORIGINAL = "Original",
RESIZE = "Resize",
CROP = "Crop",
}
export enum LDMSampler {
ddim = "ddim",
plms = "plms",
}
function strEnum<T extends string>(o: Array<T>): { [K in T]: K } {
return o.reduce((res, key) => {
res[key] = key
return res
}, Object.create(null))
}
export enum AIModel { export enum AIModel {
LAMA = "lama", LAMA = "lama",
@ -75,18 +58,12 @@ export interface Rect {
} }
interface AppState { interface AppState {
file: File | undefined
imageHeight: number
imageWidth: number
disableShortCuts: boolean
isInpainting: boolean
isDisableModelSwitch: boolean isDisableModelSwitch: boolean
isEnableAutoSaving: boolean isEnableAutoSaving: boolean
isInteractiveSeg: boolean isInteractiveSeg: boolean
isInteractiveSegRunning: boolean isInteractiveSegRunning: boolean
interactiveSegClicks: number[][] interactiveSegClicks: number[][]
enableFileManager: boolean enableFileManager: boolean
brushSize: number
isControlNet: boolean isControlNet: boolean
controlNetMethod: string controlNetMethod: string
plugins: string[] plugins: string[]
@ -96,18 +73,12 @@ interface AppState {
export const appState = atom<AppState>({ export const appState = atom<AppState>({
key: "appState", key: "appState",
default: { default: {
file: undefined,
imageHeight: 0,
imageWidth: 0,
disableShortCuts: false,
isInpainting: false,
isDisableModelSwitch: false, isDisableModelSwitch: false,
isEnableAutoSaving: false, isEnableAutoSaving: false,
isInteractiveSeg: false, isInteractiveSeg: false,
isInteractiveSegRunning: false, isInteractiveSegRunning: false,
interactiveSegClicks: [], interactiveSegClicks: [],
enableFileManager: false, enableFileManager: false,
brushSize: 40,
isControlNet: false, isControlNet: false,
controlNetMethod: ControlNetMethod.canny, controlNetMethod: ControlNetMethod.canny,
plugins: [], plugins: [],
@ -115,28 +86,11 @@ export const appState = atom<AppState>({
}, },
}) })
export const propmtState = atom<string>({
key: "promptState",
default: "",
})
export const negativePropmtState = atom<string>({ export const negativePropmtState = atom<string>({
key: "negativePromptState", key: "negativePromptState",
default: "", default: "",
}) })
export const isInpaintingState = selector({
key: "isInpainting",
get: ({ get }) => {
const app = get(appState)
return app.isInpainting
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, { ...app, isInpainting: newValue })
},
})
export const isPluginRunningState = selector({ export const isPluginRunningState = selector({
key: "isPluginRunningState", key: "isPluginRunningState",
get: ({ get }) => { get: ({ get }) => {
@ -175,42 +129,6 @@ export const serverConfigState = selector({
}, },
}) })
export const brushSizeState = selector({
key: "brushSizeState",
get: ({ get }) => {
const app = get(appState)
return app.brushSize
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, { ...app, brushSize: newValue })
},
})
export const imageHeightState = selector({
key: "imageHeightState",
get: ({ get }) => {
const app = get(appState)
return app.imageHeight
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, { ...app, imageHeight: newValue })
},
})
export const imageWidthState = selector({
key: "imageWidthState",
get: ({ get }) => {
const app = get(appState)
return app.imageWidth
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, { ...app, imageWidth: newValue })
},
})
export const enableFileManagerState = selector({ export const enableFileManagerState = selector({
key: "enableFileManagerState", key: "enableFileManagerState",
get: ({ get }) => { get: ({ get }) => {
@ -223,30 +141,6 @@ export const enableFileManagerState = selector({
}, },
}) })
export const fileState = selector({
key: "fileState",
get: ({ get }) => {
const app = get(appState)
return app.file
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, {
...app,
file: newValue,
interactiveSegClicks: [],
isInteractiveSeg: false,
isInteractiveSegRunning: false,
})
const setting = get(settingState)
set(settingState, {
...setting,
sdScale: 100,
})
},
})
export const isInteractiveSegState = selector({ export const isInteractiveSegState = selector({
key: "isInteractiveSegState", key: "isInteractiveSegState",
get: ({ get }) => { get: ({ get }) => {
@ -275,9 +169,7 @@ export const isProcessingState = selector({
key: "isProcessingState", key: "isProcessingState",
get: ({ get }) => { get: ({ get }) => {
const app = get(appState) const app = get(appState)
return ( return app.isInteractiveSegRunning || app.isPluginRunning
app.isInteractiveSegRunning || app.isPluginRunning || app.isInpainting
)
}, },
}) })
@ -339,20 +231,6 @@ export const croperState = atom<Rect>({
}, },
}) })
export const SIDE_PANEL_TAB = strEnum(["inpainting", "outpainting"])
export type SIDE_PANEL_TAB_TYPE = keyof typeof SIDE_PANEL_TAB
export interface SidePanelState {
tab: SIDE_PANEL_TAB_TYPE
}
export const sidePanelTabState = atom<SidePanelState>({
key: "sidePanelTabState",
default: {
tab: SIDE_PANEL_TAB.inpainting,
},
})
export const croperX = selector({ export const croperX = selector({
key: "croperX", key: "croperX",
get: ({ get }) => get(croperState).x, get: ({ get }) => get(croperState).x,
@ -435,43 +313,6 @@ export const extenderWidth = selector({
}, },
}) })
interface ToastAtomState {
open: boolean
desc: string
state: ToastState
duration: number
}
export const toastState = atom<ToastAtomState>({
key: "toastState",
default: {
open: false,
desc: "",
state: "default",
duration: 3000,
},
})
export const shortcutsState = atom<boolean>({
key: "shortcutsState",
default: false,
})
export interface HDSettings {
hdStrategy: HDStrategy
hdStrategyResizeLimit: number
hdStrategyCropTrigerSize: number
hdStrategyCropMargin: number
enabled: boolean
}
type ModelsHDSettings = { [key in AIModel]: HDSettings }
export enum CV2Flag {
INPAINT_NS = "INPAINT_NS",
INPAINT_TELEA = "INPAINT_TELEA",
}
export interface Settings { export interface Settings {
show: boolean show: boolean
showCroper: boolean showCroper: boolean
@ -490,7 +331,6 @@ export interface Settings {
// For SD // For SD
sdMaskBlur: number sdMaskBlur: number
sdMode: SDMode
sdStrength: number sdStrength: number
sdSteps: number sdSteps: number
sdGuidanceScale: number sdGuidanceScale: number
@ -656,7 +496,6 @@ export const settingStateDefault: Settings = {
// SD // SD
sdMaskBlur: 5, sdMaskBlur: 5,
sdMode: SDMode.inpainting,
sdStrength: 0.75, sdStrength: 0.75,
sdSteps: 50, sdSteps: 50,
sdGuidanceScale: 7.5, sdGuidanceScale: 7.5,
@ -819,70 +658,3 @@ export const isDiffusionModelsState = selector({
return isSD || isPaintByExample || isPix2Pix return isSD || isPaintByExample || isPix2Pix
}, },
}) })
export enum SortBy {
NAME = "name",
CTIME = "ctime",
MTIME = "mtime",
}
export enum SortOrder {
DESCENDING = "desc",
ASCENDING = "asc",
}
interface FileManagerState {
sortBy: SortBy
sortOrder: SortOrder
layout: "rows" | "masonry"
searchText: string
}
const FILE_MANAGER_STATE_KEY = "fileManagerState"
export const fileManagerState = atom<FileManagerState>({
key: FILE_MANAGER_STATE_KEY,
default: {
sortBy: SortBy.CTIME,
sortOrder: SortOrder.DESCENDING,
layout: "masonry",
searchText: "",
},
effects: [localStorageEffect(FILE_MANAGER_STATE_KEY)],
})
export const fileManagerSortBy = selector({
key: "fileManagerSortBy",
get: ({ get }) => get(fileManagerState).sortBy,
set: ({ get, set }, newValue: any) => {
const val = get(fileManagerState)
set(fileManagerState, { ...val, sortBy: newValue })
},
})
export const fileManagerSortOrder = selector({
key: "fileManagerSortOrder",
get: ({ get }) => get(fileManagerState).sortOrder,
set: ({ get, set }, newValue: any) => {
const val = get(fileManagerState)
set(fileManagerState, { ...val, sortOrder: newValue })
},
})
export const fileManagerLayout = selector({
key: "fileManagerLayout",
get: ({ get }) => get(fileManagerState).layout,
set: ({ get, set }, newValue: any) => {
const val = get(fileManagerState)
set(fileManagerState, { ...val, layout: newValue })
},
})
export const fileManagerSearchText = selector({
key: "fileManagerSearchText",
get: ({ get }) => get(fileManagerState).searchText,
set: ({ get, set }, newValue: any) => {
const val = get(fileManagerState)
set(fileManagerState, { ...val, searchText: newValue })
},
})

View File

@ -6,3 +6,40 @@ export enum PluginName {
RestoreFormer = "RestoreFormer", RestoreFormer = "RestoreFormer",
InteractiveSeg = "InteractiveSeg", InteractiveSeg = "InteractiveSeg",
} }
export enum SortBy {
NAME = "name",
CTIME = "ctime",
MTIME = "mtime",
}
export enum SortOrder {
DESCENDING = "desc",
ASCENDING = "asc",
}
export enum HDStrategy {
ORIGINAL = "Original",
RESIZE = "Resize",
CROP = "Crop",
}
export enum LDMSampler {
ddim = "ddim",
plms = "plms",
}
export enum CV2Flag {
INPAINT_NS = "INPAINT_NS",
INPAINT_TELEA = "INPAINT_TELEA",
}
export interface HDSettings {
hdStrategy: HDStrategy
hdStrategyResizeLimit: number
hdStrategyCropTrigerSize: number
hdStrategyCropMargin: number
enabled: boolean
}
export type ModelsHDSettings = { [key in AIModel]: HDSettings }