update UI
This commit is contained in:
		
							parent
							
								
									f71e9cfb26
								
							
						
					
					
						commit
						4484a27366
					
				| 
						 | 
				
			
			@ -9,4 +9,6 @@ dist/
 | 
			
		|||
IOPaint.egg-info/
 | 
			
		||||
venv/
 | 
			
		||||
tmp/
 | 
			
		||||
iopaint/web_app/
 | 
			
		||||
# iopaint/web_app/
 | 
			
		||||
iopaint/.venv
 | 
			
		||||
iopaint/output_nohup.log
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
2025-05-08 17:06:14.942 | INFO     | iopaint.runtime:setup_model_dir:82 - Model directory: /root/.cache
 | 
			
		||||
- Platform: Linux-6.8.0-55-generic-x86_64-with-glibc2.39
 | 
			
		||||
- Python version: 3.12.3
 | 
			
		||||
- torch: 2.7.0
 | 
			
		||||
- torchvision: 0.22.0
 | 
			
		||||
- Pillow: 9.5.0
 | 
			
		||||
- diffusers: 0.27.2
 | 
			
		||||
- transformers: 4.48.3
 | 
			
		||||
- opencv-python: 4.11.0.86
 | 
			
		||||
- accelerate: 1.6.0
 | 
			
		||||
- iopaint: N/A
 | 
			
		||||
- rembg: 2.0.65
 | 
			
		||||
- realesrgan: N/A
 | 
			
		||||
- gfpgan: N/A
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
torch>=2.0.0
 | 
			
		||||
opencv-python
 | 
			
		||||
diffusers==0.27.2
 | 
			
		||||
huggingface_hub==0.25.2
 | 
			
		||||
accelerate
 | 
			
		||||
peft==0.7.1
 | 
			
		||||
transformers>=4.39.1
 | 
			
		||||
safetensors
 | 
			
		||||
controlnet-aux==0.0.3
 | 
			
		||||
fastapi==0.108.0
 | 
			
		||||
uvicorn
 | 
			
		||||
python-multipart
 | 
			
		||||
python-socketio==5.7.2
 | 
			
		||||
typer
 | 
			
		||||
pydantic>=2.5.2
 | 
			
		||||
rich
 | 
			
		||||
loguru
 | 
			
		||||
yacs
 | 
			
		||||
piexif==1.1.3
 | 
			
		||||
omegaconf
 | 
			
		||||
easydict
 | 
			
		||||
gradio==4.21.0
 | 
			
		||||
typer-config==1.4.0
 | 
			
		||||
 | 
			
		||||
Pillow==9.5.0 # for AnyText
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  env: { browser: true, es2020: true },
 | 
			
		||||
  extends: [
 | 
			
		||||
    'eslint:recommended',
 | 
			
		||||
    'plugin:@typescript-eslint/recommended',
 | 
			
		||||
    'plugin:react-hooks/recommended',
 | 
			
		||||
  ],
 | 
			
		||||
  ignorePatterns: ['dist', '.eslintrc.cjs'],
 | 
			
		||||
  parser: '@typescript-eslint/parser',
 | 
			
		||||
  plugins: ['react-refresh'],
 | 
			
		||||
  rules: {
 | 
			
		||||
    'react-refresh/only-export-components': [
 | 
			
		||||
      'warn',
 | 
			
		||||
      { allowConstantExport: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
# React + TypeScript + Vite
 | 
			
		||||
 | 
			
		||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
 | 
			
		||||
 | 
			
		||||
Currently, two official plugins are available:
 | 
			
		||||
 | 
			
		||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
 | 
			
		||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
 | 
			
		||||
 | 
			
		||||
## Expanding the ESLint configuration
 | 
			
		||||
 | 
			
		||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
 | 
			
		||||
 | 
			
		||||
- Configure the top-level `parserOptions` property like this:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
export default {
 | 
			
		||||
  // other rules...
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
    ecmaVersion: 'latest',
 | 
			
		||||
    sourceType: 'module',
 | 
			
		||||
    project: ['./tsconfig.json', './tsconfig.node.json'],
 | 
			
		||||
    tsconfigRootDir: __dirname,
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
 | 
			
		||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
 | 
			
		||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "new-york",
 | 
			
		||||
  "rsc": false,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "tailwind.config.js",
 | 
			
		||||
    "css": "app/globals.css",
 | 
			
		||||
    "baseColor": "gray",
 | 
			
		||||
    "cssVariables": true
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "@/components",
 | 
			
		||||
    "utils": "@/lib/utils"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>IOPaint</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "web_app",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "tsc && vite build",
 | 
			
		||||
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@heroicons/react": "^2.0.18",
 | 
			
		||||
    "@hookform/resolvers": "^3.3.2",
 | 
			
		||||
    "@radix-ui/react-accordion": "^1.1.2",
 | 
			
		||||
    "@radix-ui/react-alert-dialog": "^1.0.5",
 | 
			
		||||
    "@radix-ui/react-context-menu": "^2.1.5",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.0.5",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
			
		||||
    "@radix-ui/react-icons": "^1.3.0",
 | 
			
		||||
    "@radix-ui/react-label": "^2.0.2",
 | 
			
		||||
    "@radix-ui/react-popover": "^1.0.7",
 | 
			
		||||
    "@radix-ui/react-progress": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-radio-group": "^1.1.3",
 | 
			
		||||
    "@radix-ui/react-scroll-area": "^1.0.5",
 | 
			
		||||
    "@radix-ui/react-select": "^2.0.0",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-slider": "^1.1.2",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.0.2",
 | 
			
		||||
    "@radix-ui/react-switch": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.0.4",
 | 
			
		||||
    "@radix-ui/react-toast": "^1.1.5",
 | 
			
		||||
    "@radix-ui/react-toggle": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.0.7",
 | 
			
		||||
    "@tanstack/react-query": "^5.8.7",
 | 
			
		||||
    "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
    "axios": "^1.6.2",
 | 
			
		||||
    "class-variance-authority": "^0.7.0",
 | 
			
		||||
    "clsx": "^2.0.0",
 | 
			
		||||
    "fuse.js": "^7.0.0",
 | 
			
		||||
    "immer": "^10.0.3",
 | 
			
		||||
    "inter-ui": "^4.0.0",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "lucide-react": "^0.292.0",
 | 
			
		||||
    "mitt": "^3.0.1",
 | 
			
		||||
    "next-themes": "^0.2.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-hook-form": "^7.48.2",
 | 
			
		||||
    "react-hotkeys-hook": "^4.4.1",
 | 
			
		||||
    "react-photo-album": "^2.3.0",
 | 
			
		||||
    "react-use": "^17.4.0",
 | 
			
		||||
    "react-zoom-pan-pinch": "^3.3.0",
 | 
			
		||||
    "recoil": "^0.7.7",
 | 
			
		||||
    "socket.io-client": "^4.7.2",
 | 
			
		||||
    "tailwind-merge": "^2.0.0",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "zod": "^3.22.4",
 | 
			
		||||
    "zundo": "^2.0.0",
 | 
			
		||||
    "zustand": "^4.4.6"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tanstack/eslint-plugin-query": "^5.8.4",
 | 
			
		||||
    "@types/axios": "^0.14.0",
 | 
			
		||||
    "@types/flexsearch": "^0.7.6",
 | 
			
		||||
    "@types/lodash": "^4.14.201",
 | 
			
		||||
    "@types/node": "^20.9.2",
 | 
			
		||||
    "@types/react": "^18.2.37",
 | 
			
		||||
    "@types/react-dom": "^18.2.15",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^6.10.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^6.10.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.2.0",
 | 
			
		||||
    "@vitejs/plugin-react-swc": "^3.5.0",
 | 
			
		||||
    "autoprefixer": "^10.4.16",
 | 
			
		||||
    "eslint": "^8.53.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^4.6.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.4",
 | 
			
		||||
    "postcss": "^8.4.31",
 | 
			
		||||
    "tailwindcss": "^3.3.5",
 | 
			
		||||
    "typescript": "^5.2.2",
 | 
			
		||||
    "vite": "^5.0.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
export default {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
    autoprefixer: {},
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,167 @@
 | 
			
		|||
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"
 | 
			
		||||
 | 
			
		||||
const SUPPORTED_FILE_TYPE = [
 | 
			
		||||
  "image/jpeg",
 | 
			
		||||
  "image/png",
 | 
			
		||||
  "image/webp",
 | 
			
		||||
  "image/bmp",
 | 
			
		||||
  "image/tiff",
 | 
			
		||||
]
 | 
			
		||||
function Home() {
 | 
			
		||||
  const [file, updateAppState, setServerConfig, setFile] = useStore((state) => [
 | 
			
		||||
    state.file,
 | 
			
		||||
    state.updateAppState,
 | 
			
		||||
    state.setServerConfig,
 | 
			
		||||
    state.setFile,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const userInputImage = useInputImage()
 | 
			
		||||
 | 
			
		||||
  const windowSize = useWindowSize()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (userInputImage) {
 | 
			
		||||
      setFile(userInputImage)
 | 
			
		||||
    }
 | 
			
		||||
  }, [userInputImage, setFile])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    updateAppState({ windowSize })
 | 
			
		||||
  }, [windowSize])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchServerConfig = async () => {
 | 
			
		||||
      const serverConfig = await getServerConfig()
 | 
			
		||||
      setServerConfig(serverConfig)
 | 
			
		||||
      if (serverConfig.isDesktop) {
 | 
			
		||||
        // Keeping GUI Window Open
 | 
			
		||||
        keepGUIAlive()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    fetchServerConfig()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const dragCounter = useRef(0)
 | 
			
		||||
 | 
			
		||||
  const handleDrag = useCallback((event: any) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    event.stopPropagation()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleDragIn = useCallback((event: any) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    event.stopPropagation()
 | 
			
		||||
    dragCounter.current += 1
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleDragOut = useCallback((event: any) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    event.stopPropagation()
 | 
			
		||||
    dragCounter.current -= 1
 | 
			
		||||
    if (dragCounter.current > 0) return
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleDrop = useCallback((event: any) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    event.stopPropagation()
 | 
			
		||||
    if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
 | 
			
		||||
      if (event.dataTransfer.files.length > 1) {
 | 
			
		||||
        // setToastState({
 | 
			
		||||
        //   open: true,
 | 
			
		||||
        //   desc: "Please drag and drop only one file",
 | 
			
		||||
        //   state: "error",
 | 
			
		||||
        //   duration: 3000,
 | 
			
		||||
        // })
 | 
			
		||||
      } else {
 | 
			
		||||
        const dragFile = event.dataTransfer.files[0]
 | 
			
		||||
        const fileType = dragFile.type
 | 
			
		||||
        if (SUPPORTED_FILE_TYPE.includes(fileType)) {
 | 
			
		||||
          setFile(dragFile)
 | 
			
		||||
        } else {
 | 
			
		||||
          // setToastState({
 | 
			
		||||
          //   open: true,
 | 
			
		||||
          //   desc: "Please drag and drop an image file",
 | 
			
		||||
          //   state: "error",
 | 
			
		||||
          //   duration: 3000,
 | 
			
		||||
          // })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      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
 | 
			
		||||
    }
 | 
			
		||||
    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
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    if (items.length === 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    event.stopPropagation()
 | 
			
		||||
 | 
			
		||||
    // TODO: add confirm dialog
 | 
			
		||||
 | 
			
		||||
    const item = items[0]
 | 
			
		||||
    // Get the blob of image
 | 
			
		||||
    const blob = item.getAsFile()
 | 
			
		||||
    if (blob) {
 | 
			
		||||
      setFile(blob)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    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)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  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">
 | 
			
		||||
      <Toaster />
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Workspace />
 | 
			
		||||
      {!file ? (
 | 
			
		||||
        <FileSelect
 | 
			
		||||
          onSelection={async (f) => {
 | 
			
		||||
            setFile(f)
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </main>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Home
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 203 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import { Coffee as CoffeeIcon } from "lucide-react"
 | 
			
		||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog"
 | 
			
		||||
import { IconButton } from "./ui/button"
 | 
			
		||||
import { DialogDescription } from "@radix-ui/react-dialog"
 | 
			
		||||
import Kofi from "@/assets/kofi_button_black.png"
 | 
			
		||||
 | 
			
		||||
export function Coffee() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <IconButton tooltip="Buy me a coffee">
 | 
			
		||||
          <CoffeeIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogTitle>Buy me a coffee</DialogTitle>
 | 
			
		||||
        <DialogDescription className="mb-8">
 | 
			
		||||
          Hi, if you found my project is useful, please conside buy me a coffee
 | 
			
		||||
          to support my work. Thanks!
 | 
			
		||||
        </DialogDescription>
 | 
			
		||||
        <div className="w-full flex items-center justify-center">
 | 
			
		||||
          <a
 | 
			
		||||
            href="https://ko-fi.com/Z8Z1CZJGY"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noreferrer"
 | 
			
		||||
          >
 | 
			
		||||
            <img src={Kofi} className="h-[32px]" />
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Coffee
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,400 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import React, { useEffect, useState } from "react"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
 | 
			
		||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
 | 
			
		||||
 | 
			
		||||
const DRAG_HANDLE_BORDER = 2
 | 
			
		||||
 | 
			
		||||
interface EVData {
 | 
			
		||||
  initX: number
 | 
			
		||||
  initY: number
 | 
			
		||||
  initHeight: number
 | 
			
		||||
  initWidth: number
 | 
			
		||||
  startResizeX: number
 | 
			
		||||
  startResizeY: number
 | 
			
		||||
  ord: string // top/right/bottom/left
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  maxHeight: number
 | 
			
		||||
  maxWidth: number
 | 
			
		||||
  scale: number
 | 
			
		||||
  minHeight: number
 | 
			
		||||
  minWidth: number
 | 
			
		||||
  show: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const clamp = (
 | 
			
		||||
  newPos: number,
 | 
			
		||||
  newLength: number,
 | 
			
		||||
  oldPos: number,
 | 
			
		||||
  oldLength: number,
 | 
			
		||||
  minLength: number,
 | 
			
		||||
  maxLength: number
 | 
			
		||||
) => {
 | 
			
		||||
  if (newPos !== oldPos && newLength === oldLength) {
 | 
			
		||||
    if (newPos < 0) {
 | 
			
		||||
      return [0, oldLength]
 | 
			
		||||
    }
 | 
			
		||||
    if (newPos + newLength > maxLength) {
 | 
			
		||||
      return [maxLength - oldLength, oldLength]
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (newLength < minLength) {
 | 
			
		||||
      if (newPos === oldPos) {
 | 
			
		||||
        return [newPos, minLength]
 | 
			
		||||
      }
 | 
			
		||||
      return [newPos + newLength - minLength, minLength]
 | 
			
		||||
    }
 | 
			
		||||
    if (newPos < 0) {
 | 
			
		||||
      return [0, newPos + newLength]
 | 
			
		||||
    }
 | 
			
		||||
    if (newPos + newLength > maxLength) {
 | 
			
		||||
      return [newPos, maxLength - newPos]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [newPos, newLength]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Cropper = (props: Props) => {
 | 
			
		||||
  const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props
 | 
			
		||||
 | 
			
		||||
  const [
 | 
			
		||||
    imageWidth,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    isInpainting,
 | 
			
		||||
    isSD,
 | 
			
		||||
    { x, y, width, height },
 | 
			
		||||
    setX,
 | 
			
		||||
    setY,
 | 
			
		||||
    setWidth,
 | 
			
		||||
    setHeight,
 | 
			
		||||
    isResizing,
 | 
			
		||||
    setIsResizing,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.imageWidth,
 | 
			
		||||
    state.imageHeight,
 | 
			
		||||
    state.isInpainting,
 | 
			
		||||
    state.isSD(),
 | 
			
		||||
    state.cropperState,
 | 
			
		||||
    state.setCropperX,
 | 
			
		||||
    state.setCropperY,
 | 
			
		||||
    state.setCropperWidth,
 | 
			
		||||
    state.setCropperHeight,
 | 
			
		||||
    state.isCropperExtenderResizing,
 | 
			
		||||
    state.setIsCropperExtenderResizing,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  // const [isResizing, setIsResizing] = useState(false)
 | 
			
		||||
  const [isMoving, setIsMoving] = useState(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setX(Math.round((maxWidth - 512) / 2))
 | 
			
		||||
    setY(Math.round((maxHeight - 512) / 2))
 | 
			
		||||
    // TODO: 换了一张较小的图片,cropper 的起始位置和边界要修改
 | 
			
		||||
    // TODO: 一开始的 scale 不对
 | 
			
		||||
  }, [maxHeight, maxWidth, imageWidth, imageHeight])
 | 
			
		||||
 | 
			
		||||
  const [evData, setEVData] = useState<EVData>({
 | 
			
		||||
    initX: 0,
 | 
			
		||||
    initY: 0,
 | 
			
		||||
    initHeight: 0,
 | 
			
		||||
    initWidth: 0,
 | 
			
		||||
    startResizeX: 0,
 | 
			
		||||
    startResizeY: 0,
 | 
			
		||||
    ord: "top",
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onDragFocus = () => {
 | 
			
		||||
    // console.log("focus")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const clampLeftRight = (newX: number, newWidth: number) => {
 | 
			
		||||
    return clamp(newX, newWidth, x, width, minWidth, maxWidth)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const clampTopBottom = (newY: number, newHeight: number) => {
 | 
			
		||||
    return clamp(newY, newHeight, y, height, minHeight, maxHeight)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPointerMove = (e: PointerEvent) => {
 | 
			
		||||
    if (isInpainting) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const curX = e.clientX
 | 
			
		||||
    const curY = e.clientY
 | 
			
		||||
 | 
			
		||||
    const offsetY = Math.round((curY - evData.startResizeY) / scale)
 | 
			
		||||
    const offsetX = Math.round((curX - evData.startResizeX) / scale)
 | 
			
		||||
 | 
			
		||||
    const moveTop = () => {
 | 
			
		||||
      const newHeight = evData.initHeight - offsetY
 | 
			
		||||
      const newY = evData.initY + offsetY
 | 
			
		||||
      const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight)
 | 
			
		||||
      setHeight(clampedHeight)
 | 
			
		||||
      setY(clampedY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveBottom = () => {
 | 
			
		||||
      const newHeight = evData.initHeight + offsetY
 | 
			
		||||
      const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
 | 
			
		||||
      setHeight(clampedHeight)
 | 
			
		||||
      setY(clampedY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveLeft = () => {
 | 
			
		||||
      const newWidth = evData.initWidth - offsetX
 | 
			
		||||
      const newX = evData.initX + offsetX
 | 
			
		||||
      const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth)
 | 
			
		||||
      setWidth(clampedWidth)
 | 
			
		||||
      setX(clampedX)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveRight = () => {
 | 
			
		||||
      const newWidth = evData.initWidth + offsetX
 | 
			
		||||
      const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
 | 
			
		||||
      setWidth(clampedWidth)
 | 
			
		||||
      setX(clampedX)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isResizing) {
 | 
			
		||||
      switch (evData.ord) {
 | 
			
		||||
        case "topleft": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "topright": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottomleft": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottomright": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "top": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "right": {
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottom": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "left": {
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isMoving) {
 | 
			
		||||
      const newX = evData.initX + offsetX
 | 
			
		||||
      const newY = evData.initY + offsetY
 | 
			
		||||
      const [clampedX, clampedWidth] = clampLeftRight(newX, evData.initWidth)
 | 
			
		||||
      const [clampedY, clampedHeight] = clampTopBottom(newY, evData.initHeight)
 | 
			
		||||
      setWidth(clampedWidth)
 | 
			
		||||
      setHeight(clampedHeight)
 | 
			
		||||
      setX(clampedX)
 | 
			
		||||
      setY(clampedY)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPointerDone = () => {
 | 
			
		||||
    if (isResizing) {
 | 
			
		||||
      setIsResizing(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isMoving) {
 | 
			
		||||
      setIsMoving(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isResizing || isMoving) {
 | 
			
		||||
      document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
 | 
			
		||||
      document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
      document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
      return () => {
 | 
			
		||||
        document.removeEventListener(
 | 
			
		||||
          "pointermove",
 | 
			
		||||
          onPointerMove,
 | 
			
		||||
          DOC_MOVE_OPTS
 | 
			
		||||
        )
 | 
			
		||||
        document.removeEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
        document.removeEventListener(
 | 
			
		||||
          "pointercancel",
 | 
			
		||||
          onPointerDone,
 | 
			
		||||
          DOC_MOVE_OPTS
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [isResizing, isMoving, width, height, evData])
 | 
			
		||||
 | 
			
		||||
  const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
 | 
			
		||||
    const { ord } = (e.target as HTMLElement).dataset
 | 
			
		||||
    if (ord) {
 | 
			
		||||
      setIsResizing(true)
 | 
			
		||||
      setEVData({
 | 
			
		||||
        initX: x,
 | 
			
		||||
        initY: y,
 | 
			
		||||
        initHeight: height,
 | 
			
		||||
        initWidth: width,
 | 
			
		||||
        startResizeX: e.clientX,
 | 
			
		||||
        startResizeY: e.clientY,
 | 
			
		||||
        ord,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createDragHandle = (cursor: string, side1: string, side2: string) => {
 | 
			
		||||
    const sideLength = 12
 | 
			
		||||
    const halfSideLength = sideLength / 2
 | 
			
		||||
    const draghandleCls = `w-[${sideLength}px] h-[${sideLength}px] z-[4] absolute content-[''] block border-2 border-primary borde pointer-events-auto hover:bg-primary`
 | 
			
		||||
 | 
			
		||||
    let xTrans = "0"
 | 
			
		||||
    let yTrans = "0"
 | 
			
		||||
 | 
			
		||||
    let side2Key = side2
 | 
			
		||||
    let side2Val = `${-halfSideLength}px`
 | 
			
		||||
    if (side2 === "") {
 | 
			
		||||
      side2Val = "50%"
 | 
			
		||||
      if (side1 === "left" || side1 === "right") {
 | 
			
		||||
        side2Key = "top"
 | 
			
		||||
        yTrans = "-50%"
 | 
			
		||||
      } else {
 | 
			
		||||
        side2Key = "left"
 | 
			
		||||
        xTrans = "-50%"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={cn(draghandleCls, cursor)}
 | 
			
		||||
        style={{
 | 
			
		||||
          [side1]: -halfSideLength,
 | 
			
		||||
          [side2Key]: side2Val,
 | 
			
		||||
          transform: `translate(${xTrans}, ${yTrans}) scale(${1 / scale})`,
 | 
			
		||||
        }}
 | 
			
		||||
        data-ord={side1 + side2}
 | 
			
		||||
        aria-label={side1 + side2}
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        role="button"
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createCropSelection = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        onFocus={onDragFocus}
 | 
			
		||||
        onPointerDown={onCropPointerDown}
 | 
			
		||||
        className="absolute top-0 h-full w-full"
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
 | 
			
		||||
          data-ord="top"
 | 
			
		||||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
 | 
			
		||||
          data-ord="right"
 | 
			
		||||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
 | 
			
		||||
          data-ord="bottom"
 | 
			
		||||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
 | 
			
		||||
          data-ord="left"
 | 
			
		||||
        />
 | 
			
		||||
        {createDragHandle("cursor-nw-resize", "top", "left")}
 | 
			
		||||
        {createDragHandle("cursor-ne-resize", "top", "right")}
 | 
			
		||||
        {createDragHandle("cursor-sw-resize", "bottom", "left")}
 | 
			
		||||
        {createDragHandle("cursor-se-resize", "bottom", "right")}
 | 
			
		||||
        {createDragHandle("cursor-ns-resize", "top", "")}
 | 
			
		||||
        {createDragHandle("cursor-ns-resize", "bottom", "")}
 | 
			
		||||
        {createDragHandle("cursor-ew-resize", "left", "")}
 | 
			
		||||
        {createDragHandle("cursor-ew-resize", "right", "")}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
 | 
			
		||||
    setIsMoving(true)
 | 
			
		||||
    setEVData({
 | 
			
		||||
      initX: x,
 | 
			
		||||
      initY: y,
 | 
			
		||||
      initHeight: height,
 | 
			
		||||
      initWidth: width,
 | 
			
		||||
      startResizeX: e.clientX,
 | 
			
		||||
      startResizeY: e.clientY,
 | 
			
		||||
      ord: "",
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createInfoBar = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={twMerge(
 | 
			
		||||
          "border absolute pointer-events-auto px-2 py-1 rounded-full hover:cursor-move bg-background",
 | 
			
		||||
          "origin-top-left top-0 left-0"
 | 
			
		||||
        )}
 | 
			
		||||
        style={{
 | 
			
		||||
          transform: `scale(${(1 / scale) * 0.8})`,
 | 
			
		||||
        }}
 | 
			
		||||
        onPointerDown={onInfoBarPointerDown}
 | 
			
		||||
      >
 | 
			
		||||
        {/* TODO: 移动的时候会显示 brush */}
 | 
			
		||||
        {width} x {height}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createBorder = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className="outline-dashed outline-primary"
 | 
			
		||||
        style={{
 | 
			
		||||
          height,
 | 
			
		||||
          width,
 | 
			
		||||
          outlineWidth: `${(DRAG_HANDLE_BORDER / scale) * 1.3}px`,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (show === false || !isSD) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="absolute h-full w-full overflow-hidden pointer-events-none z-[2]">
 | 
			
		||||
      <div
 | 
			
		||||
        className="relative pointer-events-none z-[2] [box-shadow:0_0_0_9999px_rgba(0,_0,_0,_0.5)]"
 | 
			
		||||
        style={{ height, width, left: x, top: y }}
 | 
			
		||||
      >
 | 
			
		||||
        {createBorder()}
 | 
			
		||||
        {createInfoBar()}
 | 
			
		||||
        {createCropSelection()}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Cropper
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import io from "socket.io-client"
 | 
			
		||||
import { Progress } from "./ui/progress"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
 | 
			
		||||
export const API_ENDPOINT = import.meta.env.DEV
 | 
			
		||||
  ? import.meta.env.VITE_BACKEND
 | 
			
		||||
  : ""
 | 
			
		||||
const socket = io(API_ENDPOINT)
 | 
			
		||||
 | 
			
		||||
const DiffusionProgress = () => {
 | 
			
		||||
  const [settings, isInpainting, isSD] = useStore((state) => [
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.isInpainting,
 | 
			
		||||
    state.isSD(),
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const [isConnected, setIsConnected] = React.useState(false)
 | 
			
		||||
  const [step, setStep] = React.useState(0)
 | 
			
		||||
 | 
			
		||||
  const progress = Math.min(Math.round((step / settings.sdSteps) * 100), 100)
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    socket.on("connect", () => {
 | 
			
		||||
      setIsConnected(true)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    socket.on("disconnect", () => {
 | 
			
		||||
      setIsConnected(false)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    socket.on("diffusion_progress", (data) => {
 | 
			
		||||
      if (data) {
 | 
			
		||||
        setStep(data.step + 1)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    socket.on("diffusion_finish", () => {
 | 
			
		||||
      setStep(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.off("connect")
 | 
			
		||||
      socket.off("disconnect")
 | 
			
		||||
      socket.off("diffusion_progress")
 | 
			
		||||
      socket.off("diffusion_finish")
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="z-10 fixed bg-background w-[220px] left-1/2 -translate-x-1/2 top-[68px] h-[32px] flex justify-center items-center gap-[18px] border-[1px] border-[solid] rounded-[14px] pl-[8px] pr-[8px]"
 | 
			
		||||
      style={{
 | 
			
		||||
        visibility: isConnected && isInpainting && isSD ? "visible" : "hidden",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Progress value={progress} />
 | 
			
		||||
      <div className="w-[45px] flex justify-center font-nums">{progress}%</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DiffusionProgress
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,989 @@
 | 
			
		|||
import { SyntheticEvent, useCallback, useEffect, useRef, useState } from "react"
 | 
			
		||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
 | 
			
		||||
import { useToast } from "@/components/ui/use-toast"
 | 
			
		||||
import {
 | 
			
		||||
  ReactZoomPanPinchContentRef,
 | 
			
		||||
  TransformComponent,
 | 
			
		||||
  TransformWrapper,
 | 
			
		||||
} from "react-zoom-pan-pinch"
 | 
			
		||||
import { useKeyPressEvent } from "react-use"
 | 
			
		||||
import { downloadToOutput, runPlugin } from "@/lib/api"
 | 
			
		||||
import { IconButton } from "@/components/ui/button"
 | 
			
		||||
import {
 | 
			
		||||
  askWritePermission,
 | 
			
		||||
  cn,
 | 
			
		||||
  copyCanvasImage,
 | 
			
		||||
  downloadImage,
 | 
			
		||||
  drawLines,
 | 
			
		||||
  generateMask,
 | 
			
		||||
  isMidClick,
 | 
			
		||||
  isRightClick,
 | 
			
		||||
  mouseXY,
 | 
			
		||||
  srcToFile,
 | 
			
		||||
} from "@/lib/utils"
 | 
			
		||||
import { Eraser, Eye, Redo, Undo, Expand, Download } from "lucide-react"
 | 
			
		||||
import { useImage } from "@/hooks/useImage"
 | 
			
		||||
import { Slider } from "./ui/slider"
 | 
			
		||||
import { PluginName } from "@/lib/types"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import Cropper from "./Cropper"
 | 
			
		||||
import { InteractiveSegPoints } from "./InteractiveSeg"
 | 
			
		||||
import useHotKey from "@/hooks/useHotkey"
 | 
			
		||||
import Extender from "./Extender"
 | 
			
		||||
import { MAX_BRUSH_SIZE, MIN_BRUSH_SIZE } from "@/lib/const"
 | 
			
		||||
 | 
			
		||||
const TOOLBAR_HEIGHT = 200
 | 
			
		||||
const COMPARE_SLIDER_DURATION_MS = 300
 | 
			
		||||
 | 
			
		||||
interface EditorProps {
 | 
			
		||||
  file: File
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Editor(props: EditorProps) {
 | 
			
		||||
  const { file } = props
 | 
			
		||||
  const { toast } = useToast()
 | 
			
		||||
 | 
			
		||||
  const [
 | 
			
		||||
    disableShortCuts,
 | 
			
		||||
    windowSize,
 | 
			
		||||
    isInpainting,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    settings,
 | 
			
		||||
    enableAutoSaving,
 | 
			
		||||
    setImageSize,
 | 
			
		||||
    setBaseBrushSize,
 | 
			
		||||
    interactiveSegState,
 | 
			
		||||
    updateInteractiveSegState,
 | 
			
		||||
    handleCanvasMouseDown,
 | 
			
		||||
    handleCanvasMouseMove,
 | 
			
		||||
    undo,
 | 
			
		||||
    redo,
 | 
			
		||||
    undoDisabled,
 | 
			
		||||
    redoDisabled,
 | 
			
		||||
    isProcessing,
 | 
			
		||||
    updateAppState,
 | 
			
		||||
    runMannually,
 | 
			
		||||
    runInpainting,
 | 
			
		||||
    isCropperExtenderResizing,
 | 
			
		||||
    decreaseBaseBrushSize,
 | 
			
		||||
    increaseBaseBrushSize,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.disableShortCuts,
 | 
			
		||||
    state.windowSize,
 | 
			
		||||
    state.isInpainting,
 | 
			
		||||
    state.imageWidth,
 | 
			
		||||
    state.imageHeight,
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.serverConfig.enableAutoSaving,
 | 
			
		||||
    state.setImageSize,
 | 
			
		||||
    state.setBaseBrushSize,
 | 
			
		||||
    state.interactiveSegState,
 | 
			
		||||
    state.updateInteractiveSegState,
 | 
			
		||||
    state.handleCanvasMouseDown,
 | 
			
		||||
    state.handleCanvasMouseMove,
 | 
			
		||||
    state.undo,
 | 
			
		||||
    state.redo,
 | 
			
		||||
    state.undoDisabled(),
 | 
			
		||||
    state.redoDisabled(),
 | 
			
		||||
    state.getIsProcessing(),
 | 
			
		||||
    state.updateAppState,
 | 
			
		||||
    state.runMannually(),
 | 
			
		||||
    state.runInpainting,
 | 
			
		||||
    state.isCropperExtenderResizing,
 | 
			
		||||
    state.decreaseBaseBrushSize,
 | 
			
		||||
    state.increaseBaseBrushSize,
 | 
			
		||||
  ])
 | 
			
		||||
  const baseBrushSize = useStore((state) => state.editorState.baseBrushSize)
 | 
			
		||||
  const brushSize = useStore((state) => state.getBrushSize())
 | 
			
		||||
  const renders = useStore((state) => state.editorState.renders)
 | 
			
		||||
  const extraMasks = useStore((state) => state.editorState.extraMasks)
 | 
			
		||||
  const temporaryMasks = useStore((state) => state.editorState.temporaryMasks)
 | 
			
		||||
  const lineGroups = useStore((state) => state.editorState.lineGroups)
 | 
			
		||||
  const curLineGroup = useStore((state) => state.editorState.curLineGroup)
 | 
			
		||||
 | 
			
		||||
  // Local State
 | 
			
		||||
  const [showOriginal, setShowOriginal] = useState(false)
 | 
			
		||||
  const [original, isOriginalLoaded] = useImage(file)
 | 
			
		||||
  const [context, setContext] = useState<CanvasRenderingContext2D>()
 | 
			
		||||
  const [imageContext, setImageContext] = useState<CanvasRenderingContext2D>()
 | 
			
		||||
  const [{ x, y }, setCoords] = useState({ x: -1, y: -1 })
 | 
			
		||||
  const [showBrush, setShowBrush] = useState(false)
 | 
			
		||||
  const [showRefBrush, setShowRefBrush] = useState(false)
 | 
			
		||||
  const [isPanning, setIsPanning] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const [scale, setScale] = useState<number>(1)
 | 
			
		||||
  const [panned, setPanned] = useState<boolean>(false)
 | 
			
		||||
  const [minScale, setMinScale] = useState<number>(1.0)
 | 
			
		||||
  const windowCenterX = windowSize.width / 2
 | 
			
		||||
  const windowCenterY = windowSize.height / 2
 | 
			
		||||
  const viewportRef = useRef<ReactZoomPanPinchContentRef | null>(null)
 | 
			
		||||
  // Indicates that the image has been loaded and is centered on first load
 | 
			
		||||
  const [initialCentered, setInitialCentered] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const [isDraging, setIsDraging] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const [sliderPos, setSliderPos] = useState<number>(0)
 | 
			
		||||
  const [isChangingBrushSizeByWheel, setIsChangingBrushSizeByWheel] =
 | 
			
		||||
    useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const hadDrawSomething = useCallback(() => {
 | 
			
		||||
    return curLineGroup.length !== 0
 | 
			
		||||
  }, [curLineGroup])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      !imageContext ||
 | 
			
		||||
      !isOriginalLoaded ||
 | 
			
		||||
      imageWidth === 0 ||
 | 
			
		||||
      imageHeight === 0
 | 
			
		||||
    ) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const render = renders.length === 0 ? original : renders[renders.length - 1]
 | 
			
		||||
    imageContext.canvas.width = imageWidth
 | 
			
		||||
    imageContext.canvas.height = imageHeight
 | 
			
		||||
 | 
			
		||||
    imageContext.clearRect(
 | 
			
		||||
      0,
 | 
			
		||||
      0,
 | 
			
		||||
      imageContext.canvas.width,
 | 
			
		||||
      imageContext.canvas.height
 | 
			
		||||
    )
 | 
			
		||||
    imageContext.drawImage(render, 0, 0, imageWidth, imageHeight)
 | 
			
		||||
  }, [
 | 
			
		||||
    renders,
 | 
			
		||||
    original,
 | 
			
		||||
    isOriginalLoaded,
 | 
			
		||||
    imageContext,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      !context ||
 | 
			
		||||
      !isOriginalLoaded ||
 | 
			
		||||
      imageWidth === 0 ||
 | 
			
		||||
      imageHeight === 0
 | 
			
		||||
    ) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    context.canvas.width = imageWidth
 | 
			
		||||
    context.canvas.height = imageHeight
 | 
			
		||||
    context.clearRect(0, 0, context.canvas.width, context.canvas.height)
 | 
			
		||||
    temporaryMasks.forEach((maskImage) => {
 | 
			
		||||
      context.drawImage(maskImage, 0, 0, imageWidth, imageHeight)
 | 
			
		||||
    })
 | 
			
		||||
    extraMasks.forEach((maskImage) => {
 | 
			
		||||
      context.drawImage(maskImage, 0, 0, imageWidth, imageHeight)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      interactiveSegState.isInteractiveSeg &&
 | 
			
		||||
      interactiveSegState.tmpInteractiveSegMask
 | 
			
		||||
    ) {
 | 
			
		||||
      context.drawImage(
 | 
			
		||||
        interactiveSegState.tmpInteractiveSegMask,
 | 
			
		||||
        0,
 | 
			
		||||
        0,
 | 
			
		||||
        imageWidth,
 | 
			
		||||
        imageHeight
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    drawLines(context, curLineGroup)
 | 
			
		||||
  }, [
 | 
			
		||||
    temporaryMasks,
 | 
			
		||||
    extraMasks,
 | 
			
		||||
    isOriginalLoaded,
 | 
			
		||||
    interactiveSegState,
 | 
			
		||||
    context,
 | 
			
		||||
    curLineGroup,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const getCurrentRender = useCallback(async () => {
 | 
			
		||||
    let targetFile = file
 | 
			
		||||
    if (renders.length > 0) {
 | 
			
		||||
      const lastRender = renders[renders.length - 1]
 | 
			
		||||
      targetFile = await srcToFile(lastRender.currentSrc, file.name, file.type)
 | 
			
		||||
    }
 | 
			
		||||
    return targetFile
 | 
			
		||||
  }, [file, renders])
 | 
			
		||||
 | 
			
		||||
  const hadRunInpainting = () => {
 | 
			
		||||
    return renders.length !== 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getCurrentWidthHeight = useCallback(() => {
 | 
			
		||||
    let width = 512
 | 
			
		||||
    let height = 512
 | 
			
		||||
    if (!isOriginalLoaded) {
 | 
			
		||||
      return [width, height]
 | 
			
		||||
    }
 | 
			
		||||
    if (renders.length === 0) {
 | 
			
		||||
      width = original.naturalWidth
 | 
			
		||||
      height = original.naturalHeight
 | 
			
		||||
    } else if (renders.length !== 0) {
 | 
			
		||||
      width = renders[renders.length - 1].width
 | 
			
		||||
      height = renders[renders.length - 1].height
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [width, height]
 | 
			
		||||
  }, [original, isOriginalLoaded, renders])
 | 
			
		||||
 | 
			
		||||
  // Draw once the original image is loaded
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!isOriginalLoaded) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [width, height] = getCurrentWidthHeight()
 | 
			
		||||
    if (width !== imageWidth || height !== imageHeight) {
 | 
			
		||||
      setImageSize(width, height)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rW = windowSize.width / width
 | 
			
		||||
    const rH = (windowSize.height - TOOLBAR_HEIGHT) / height
 | 
			
		||||
 | 
			
		||||
    let s = 1.0
 | 
			
		||||
    if (rW < 1 || rH < 1) {
 | 
			
		||||
      s = Math.min(rW, rH)
 | 
			
		||||
    }
 | 
			
		||||
    setMinScale(s)
 | 
			
		||||
    setScale(s)
 | 
			
		||||
 | 
			
		||||
    console.log(
 | 
			
		||||
      `[on file load] image size: ${width}x${height}, scale: ${s}, initialCentered: ${initialCentered}`
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (context?.canvas) {
 | 
			
		||||
      console.log("[on file load] set canvas size")
 | 
			
		||||
      if (width != context.canvas.width) {
 | 
			
		||||
        context.canvas.width = width
 | 
			
		||||
      }
 | 
			
		||||
      if (height != context.canvas.height) {
 | 
			
		||||
        context.canvas.height = height
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!initialCentered) {
 | 
			
		||||
      // 防止每次擦除以后图片 zoom 还原
 | 
			
		||||
      viewportRef.current?.centerView(s, 1)
 | 
			
		||||
      console.log("[on file load] centerView")
 | 
			
		||||
      setInitialCentered(true)
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    viewportRef,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
    original,
 | 
			
		||||
    isOriginalLoaded,
 | 
			
		||||
    windowSize,
 | 
			
		||||
    initialCentered,
 | 
			
		||||
    getCurrentWidthHeight,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log("[useEffect] centerView")
 | 
			
		||||
    // render 改变尺寸以后,undo/redo 重新 center
 | 
			
		||||
    viewportRef?.current?.centerView(minScale, 1)
 | 
			
		||||
  }, [imageHeight, imageWidth, viewportRef, minScale])
 | 
			
		||||
 | 
			
		||||
  // Zoom reset
 | 
			
		||||
  const resetZoom = useCallback(() => {
 | 
			
		||||
    if (!minScale || !windowSize) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const viewport = viewportRef.current
 | 
			
		||||
    if (!viewport) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const offsetX = (windowSize.width - imageWidth * minScale) / 2
 | 
			
		||||
    const offsetY = (windowSize.height - imageHeight * minScale) / 2
 | 
			
		||||
    viewport.setTransform(offsetX, offsetY, minScale, 200, "easeOutQuad")
 | 
			
		||||
    if (viewport.instance.transformState.scale) {
 | 
			
		||||
      viewport.instance.transformState.scale = minScale
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setScale(minScale)
 | 
			
		||||
    setPanned(false)
 | 
			
		||||
  }, [
 | 
			
		||||
    viewportRef,
 | 
			
		||||
    windowSize,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
    windowSize.height,
 | 
			
		||||
    minScale,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.addEventListener("resize", () => {
 | 
			
		||||
      resetZoom()
 | 
			
		||||
    })
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener("resize", () => {
 | 
			
		||||
        resetZoom()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [windowSize, resetZoom])
 | 
			
		||||
 | 
			
		||||
  const handleEscPressed = () => {
 | 
			
		||||
    if (isProcessing) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isDraging) {
 | 
			
		||||
      setIsDraging(false)
 | 
			
		||||
    } else {
 | 
			
		||||
      resetZoom()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useHotKey("Escape", handleEscPressed, [
 | 
			
		||||
    isDraging,
 | 
			
		||||
    isInpainting,
 | 
			
		||||
    resetZoom,
 | 
			
		||||
    // drawOnCurrentRender,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const onMouseMove = (ev: SyntheticEvent) => {
 | 
			
		||||
    const mouseEvent = ev.nativeEvent as MouseEvent
 | 
			
		||||
    setCoords({ x: mouseEvent.pageX, y: mouseEvent.pageY })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onMouseDrag = (ev: SyntheticEvent) => {
 | 
			
		||||
    if (isProcessing) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (interactiveSegState.isInteractiveSeg) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (isPanning) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!isDraging) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (curLineGroup.length === 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleCanvasMouseMove(mouseXY(ev))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const runInteractiveSeg = async (newClicks: number[][]) => {
 | 
			
		||||
    updateAppState({ isPluginRunning: true })
 | 
			
		||||
    const targetFile = await getCurrentRender()
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await runPlugin(
 | 
			
		||||
        true,
 | 
			
		||||
        PluginName.InteractiveSeg,
 | 
			
		||||
        targetFile,
 | 
			
		||||
        undefined,
 | 
			
		||||
        newClicks
 | 
			
		||||
      )
 | 
			
		||||
      const { blob } = res
 | 
			
		||||
      const img = new Image()
 | 
			
		||||
      img.onload = () => {
 | 
			
		||||
        updateInteractiveSegState({ tmpInteractiveSegMask: img })
 | 
			
		||||
      }
 | 
			
		||||
      img.src = blob
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      toast({
 | 
			
		||||
        variant: "destructive",
 | 
			
		||||
        description: e.message ? e.message : e.toString(),
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    updateAppState({ isPluginRunning: false })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPointerUp = (ev: SyntheticEvent) => {
 | 
			
		||||
    if (isMidClick(ev)) {
 | 
			
		||||
      setIsPanning(false)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!hadDrawSomething()) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (interactiveSegState.isInteractiveSeg) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (isPanning) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!original.src) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const canvas = context?.canvas
 | 
			
		||||
    if (!canvas) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (isInpainting) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!isDraging) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (runMannually) {
 | 
			
		||||
      setIsDraging(false)
 | 
			
		||||
    } else {
 | 
			
		||||
      runInpainting()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onCanvasMouseUp = (ev: SyntheticEvent) => {
 | 
			
		||||
    if (interactiveSegState.isInteractiveSeg) {
 | 
			
		||||
      const xy = mouseXY(ev)
 | 
			
		||||
      const newClicks: number[][] = [...interactiveSegState.clicks]
 | 
			
		||||
      if (isRightClick(ev)) {
 | 
			
		||||
        newClicks.push([xy.x, xy.y, 0, newClicks.length])
 | 
			
		||||
      } else {
 | 
			
		||||
        newClicks.push([xy.x, xy.y, 1, newClicks.length])
 | 
			
		||||
      }
 | 
			
		||||
      runInteractiveSeg(newClicks)
 | 
			
		||||
      updateInteractiveSegState({ clicks: newClicks })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onMouseDown = (ev: SyntheticEvent) => {
 | 
			
		||||
    if (isProcessing) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (interactiveSegState.isInteractiveSeg) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (isPanning) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!isOriginalLoaded) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const canvas = context?.canvas
 | 
			
		||||
    if (!canvas) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isRightClick(ev)) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isMidClick(ev)) {
 | 
			
		||||
      setIsPanning(true)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setIsDraging(true)
 | 
			
		||||
    handleCanvasMouseDown(mouseXY(ev))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleUndo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
 | 
			
		||||
    keyboardEvent.preventDefault()
 | 
			
		||||
    undo()
 | 
			
		||||
  }
 | 
			
		||||
  useHotKey("meta+z,ctrl+z", handleUndo)
 | 
			
		||||
 | 
			
		||||
  const handleRedo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
 | 
			
		||||
    keyboardEvent.preventDefault()
 | 
			
		||||
    redo()
 | 
			
		||||
  }
 | 
			
		||||
  useHotKey("shift+ctrl+z,shift+meta+z", handleRedo)
 | 
			
		||||
 | 
			
		||||
  useKeyPressEvent(
 | 
			
		||||
    "Tab",
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      ev?.preventDefault()
 | 
			
		||||
      ev?.stopPropagation()
 | 
			
		||||
      if (hadRunInpainting()) {
 | 
			
		||||
        setShowOriginal(() => {
 | 
			
		||||
          window.setTimeout(() => {
 | 
			
		||||
            setSliderPos(100)
 | 
			
		||||
          }, 10)
 | 
			
		||||
          return true
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      ev?.preventDefault()
 | 
			
		||||
      ev?.stopPropagation()
 | 
			
		||||
      if (hadRunInpainting()) {
 | 
			
		||||
        window.setTimeout(() => {
 | 
			
		||||
          setSliderPos(0)
 | 
			
		||||
        }, 10)
 | 
			
		||||
        window.setTimeout(() => {
 | 
			
		||||
          setShowOriginal(false)
 | 
			
		||||
        }, COMPARE_SLIDER_DURATION_MS)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const download = useCallback(async () => {
 | 
			
		||||
    if (file === undefined) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (enableAutoSaving && renders.length > 0) {
 | 
			
		||||
      try {
 | 
			
		||||
        await downloadToOutput(
 | 
			
		||||
          renders[renders.length - 1],
 | 
			
		||||
          file.name,
 | 
			
		||||
          file.type
 | 
			
		||||
        )
 | 
			
		||||
        toast({
 | 
			
		||||
          description: "Save image success",
 | 
			
		||||
        })
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        toast({
 | 
			
		||||
          variant: "destructive",
 | 
			
		||||
          title: "Uh oh! Something went wrong.",
 | 
			
		||||
          description: e.message ? e.message : e.toString(),
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: download to output directory
 | 
			
		||||
    const name = file.name.replace(/(\.[\w\d_-]+)$/i, "_cleanup$1")
 | 
			
		||||
    const curRender = renders[renders.length - 1]
 | 
			
		||||
    downloadImage(curRender.currentSrc, name)
 | 
			
		||||
    if (settings.enableDownloadMask) {
 | 
			
		||||
      let maskFileName = file.name.replace(/(\.[\w\d_-]+)$/i, "_mask$1")
 | 
			
		||||
      maskFileName = maskFileName.replace(/\.[^/.]+$/, ".jpg")
 | 
			
		||||
 | 
			
		||||
      const maskCanvas = generateMask(imageWidth, imageHeight, lineGroups)
 | 
			
		||||
      // Create a link
 | 
			
		||||
      const aDownloadLink = document.createElement("a")
 | 
			
		||||
      // Add the name of the file to the link
 | 
			
		||||
      aDownloadLink.download = maskFileName
 | 
			
		||||
      // Attach the data to the link
 | 
			
		||||
      aDownloadLink.href = maskCanvas.toDataURL("image/jpeg")
 | 
			
		||||
      // Get the code to click the download link
 | 
			
		||||
      aDownloadLink.click()
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    file,
 | 
			
		||||
    enableAutoSaving,
 | 
			
		||||
    renders,
 | 
			
		||||
    settings,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWidth,
 | 
			
		||||
    lineGroups,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useHotKey("meta+s,ctrl+s", download)
 | 
			
		||||
 | 
			
		||||
  const toggleShowBrush = (newState: boolean) => {
 | 
			
		||||
    if (newState !== showBrush && !isPanning && !isCropperExtenderResizing) {
 | 
			
		||||
      setShowBrush(newState)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getCursor = useCallback(() => {
 | 
			
		||||
    if (isProcessing) {
 | 
			
		||||
      return "default"
 | 
			
		||||
    }
 | 
			
		||||
    if (isPanning) {
 | 
			
		||||
      return "grab"
 | 
			
		||||
    }
 | 
			
		||||
    if (showBrush) {
 | 
			
		||||
      return "none"
 | 
			
		||||
    }
 | 
			
		||||
    return undefined
 | 
			
		||||
  }, [showBrush, isPanning, isProcessing])
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "[",
 | 
			
		||||
    () => {
 | 
			
		||||
      decreaseBaseBrushSize()
 | 
			
		||||
    },
 | 
			
		||||
    [decreaseBaseBrushSize]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "]",
 | 
			
		||||
    () => {
 | 
			
		||||
      increaseBaseBrushSize()
 | 
			
		||||
    },
 | 
			
		||||
    [increaseBaseBrushSize]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // Manual Inpainting Hotkey
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "shift+r",
 | 
			
		||||
    () => {
 | 
			
		||||
      if (runMannually && hadDrawSomething()) {
 | 
			
		||||
        runInpainting()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [runMannually, runInpainting, hadDrawSomething]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "ctrl+c,meta+c",
 | 
			
		||||
    async () => {
 | 
			
		||||
      const hasPermission = await askWritePermission()
 | 
			
		||||
      if (hasPermission && renders.length > 0) {
 | 
			
		||||
        if (context?.canvas) {
 | 
			
		||||
          await copyCanvasImage(context?.canvas)
 | 
			
		||||
          toast({
 | 
			
		||||
            title: "Copy inpainting result to clipboard",
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [renders, context]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // Toggle clean/zoom tool on spacebar.
 | 
			
		||||
  useKeyPressEvent(
 | 
			
		||||
    " ",
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      if (!disableShortCuts) {
 | 
			
		||||
        ev?.preventDefault()
 | 
			
		||||
        ev?.stopPropagation()
 | 
			
		||||
        setShowBrush(false)
 | 
			
		||||
        setIsPanning(true)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      if (!disableShortCuts) {
 | 
			
		||||
        ev?.preventDefault()
 | 
			
		||||
        ev?.stopPropagation()
 | 
			
		||||
        setShowBrush(true)
 | 
			
		||||
        setIsPanning(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useKeyPressEvent(
 | 
			
		||||
    "Alt",
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      if (!disableShortCuts) {
 | 
			
		||||
        ev?.preventDefault()
 | 
			
		||||
        ev?.stopPropagation()
 | 
			
		||||
        setIsChangingBrushSizeByWheel(true)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (ev) => {
 | 
			
		||||
      if (!disableShortCuts) {
 | 
			
		||||
        ev?.preventDefault()
 | 
			
		||||
        ev?.stopPropagation()
 | 
			
		||||
        setIsChangingBrushSizeByWheel(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getCurScale = (): number => {
 | 
			
		||||
    let s = minScale
 | 
			
		||||
    if (viewportRef.current?.instance?.transformState.scale !== undefined) {
 | 
			
		||||
      s = viewportRef.current?.instance?.transformState.scale
 | 
			
		||||
    }
 | 
			
		||||
    return s!
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getBrushStyle = (_x: number, _y: number) => {
 | 
			
		||||
    const curScale = getCurScale()
 | 
			
		||||
    return {
 | 
			
		||||
      width: `${brushSize * curScale}px`,
 | 
			
		||||
      height: `${brushSize * curScale}px`,
 | 
			
		||||
      left: `${_x}px`,
 | 
			
		||||
      top: `${_y}px`,
 | 
			
		||||
      transform: "translate(-50%, -50%)",
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) => {
 | 
			
		||||
    setBaseBrushSize(value)
 | 
			
		||||
 | 
			
		||||
    if (!showRefBrush) {
 | 
			
		||||
      setShowRefBrush(true)
 | 
			
		||||
      window.setTimeout(() => {
 | 
			
		||||
        setShowRefBrush(false)
 | 
			
		||||
      }, 10000)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderInteractiveSegCursor = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className="absolute h-[20px] w-[20px] pointer-events-none rounded-[50%] bg-[rgba(21,_215,_121,_0.936)] [box-shadow:0_0_0_0_rgba(21,_215,_121,_0.936)] animate-pulse"
 | 
			
		||||
        style={{
 | 
			
		||||
          left: `${x}px`,
 | 
			
		||||
          top: `${y}px`,
 | 
			
		||||
          transform: "translate(-50%, -50%)",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CursorArrowRaysIcon />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderCanvas = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <TransformWrapper
 | 
			
		||||
        ref={(r) => {
 | 
			
		||||
          if (r) {
 | 
			
		||||
            viewportRef.current = r
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        panning={{ disabled: !isPanning, velocityDisabled: true }}
 | 
			
		||||
        wheel={{ step: 0.05, wheelDisabled: isChangingBrushSizeByWheel }}
 | 
			
		||||
        centerZoomedOut
 | 
			
		||||
        alignmentAnimation={{ disabled: true }}
 | 
			
		||||
        centerOnInit
 | 
			
		||||
        limitToBounds={false}
 | 
			
		||||
        doubleClick={{ disabled: true }}
 | 
			
		||||
        initialScale={minScale}
 | 
			
		||||
        minScale={minScale * 0.3}
 | 
			
		||||
        onPanning={() => {
 | 
			
		||||
          if (!panned) {
 | 
			
		||||
            setPanned(true)
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        onZoom={(ref) => {
 | 
			
		||||
          setScale(ref.state.scale)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <TransformComponent
 | 
			
		||||
          contentStyle={{
 | 
			
		||||
            visibility: initialCentered ? "visible" : "hidden",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <div className="grid [grid-template-areas:'editor-content'] gap-y-4">
 | 
			
		||||
            <canvas
 | 
			
		||||
              className="[grid-area:editor-content]"
 | 
			
		||||
              style={{
 | 
			
		||||
                clipPath: `inset(0 ${sliderPos}% 0 0)`,
 | 
			
		||||
                transition: `clip-path ${COMPARE_SLIDER_DURATION_MS}ms`,
 | 
			
		||||
              }}
 | 
			
		||||
              ref={(r) => {
 | 
			
		||||
                if (r && !imageContext) {
 | 
			
		||||
                  const ctx = r.getContext("2d")
 | 
			
		||||
                  if (ctx) {
 | 
			
		||||
                    setImageContext(ctx)
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <canvas
 | 
			
		||||
              className={cn(
 | 
			
		||||
                "[grid-area:editor-content]",
 | 
			
		||||
                isProcessing
 | 
			
		||||
                  ? "pointer-events-none animate-pulse duration-600"
 | 
			
		||||
                  : ""
 | 
			
		||||
              )}
 | 
			
		||||
              style={{
 | 
			
		||||
                cursor: getCursor(),
 | 
			
		||||
                clipPath: `inset(0 ${sliderPos}% 0 0)`,
 | 
			
		||||
                transition: `clip-path ${COMPARE_SLIDER_DURATION_MS}ms`,
 | 
			
		||||
              }}
 | 
			
		||||
              onContextMenu={(e) => {
 | 
			
		||||
                e.preventDefault()
 | 
			
		||||
              }}
 | 
			
		||||
              onMouseOver={() => {
 | 
			
		||||
                toggleShowBrush(true)
 | 
			
		||||
                setShowRefBrush(false)
 | 
			
		||||
              }}
 | 
			
		||||
              onFocus={() => toggleShowBrush(true)}
 | 
			
		||||
              onMouseLeave={() => toggleShowBrush(false)}
 | 
			
		||||
              onMouseDown={onMouseDown}
 | 
			
		||||
              onMouseUp={onCanvasMouseUp}
 | 
			
		||||
              onMouseMove={onMouseDrag}
 | 
			
		||||
              ref={(r) => {
 | 
			
		||||
                if (r && !context) {
 | 
			
		||||
                  const ctx = r.getContext("2d")
 | 
			
		||||
                  if (ctx) {
 | 
			
		||||
                    setContext(ctx)
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              className="[grid-area:editor-content] pointer-events-none grid [grid-template-areas:'original-image-content']"
 | 
			
		||||
              style={{
 | 
			
		||||
                width: `${imageWidth}px`,
 | 
			
		||||
                height: `${imageHeight}px`,
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {showOriginal && (
 | 
			
		||||
                <>
 | 
			
		||||
                  <div
 | 
			
		||||
                    className="[grid-area:original-image-content] z-10 bg-primary h-full w-[6px] justify-self-end"
 | 
			
		||||
                    style={{
 | 
			
		||||
                      marginRight: `${sliderPos}%`,
 | 
			
		||||
                      transition: `margin-right ${COMPARE_SLIDER_DURATION_MS}ms`,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <img
 | 
			
		||||
                    className="[grid-area:original-image-content]"
 | 
			
		||||
                    src={original.src}
 | 
			
		||||
                    alt="original"
 | 
			
		||||
                    style={{
 | 
			
		||||
                      width: `${imageWidth}px`,
 | 
			
		||||
                      height: `${imageHeight}px`,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Cropper
 | 
			
		||||
            maxHeight={imageHeight}
 | 
			
		||||
            maxWidth={imageWidth}
 | 
			
		||||
            minHeight={Math.min(512, imageHeight)}
 | 
			
		||||
            minWidth={Math.min(512, imageWidth)}
 | 
			
		||||
            scale={getCurScale()}
 | 
			
		||||
            show={settings.showCropper}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <Extender
 | 
			
		||||
            minHeight={Math.min(512, imageHeight)}
 | 
			
		||||
            minWidth={Math.min(512, imageWidth)}
 | 
			
		||||
            scale={getCurScale()}
 | 
			
		||||
            show={settings.showExtender}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          {interactiveSegState.isInteractiveSeg ? (
 | 
			
		||||
            <InteractiveSegPoints />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
        </TransformComponent>
 | 
			
		||||
      </TransformWrapper>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleScroll = (event: React.WheelEvent<HTMLDivElement>) => {
 | 
			
		||||
    // deltaY 是垂直滚动增量,正值表示向下滚动,负值表示向上滚动
 | 
			
		||||
    // deltaX 是水平滚动增量,正值表示向右滚动,负值表示向左滚动
 | 
			
		||||
    if (!isChangingBrushSizeByWheel) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { deltaY } = event
 | 
			
		||||
    // console.log(`水平滚动增量: ${deltaX}, 垂直滚动增量: ${deltaY}`)
 | 
			
		||||
    if (deltaY > 0) {
 | 
			
		||||
      increaseBaseBrushSize()
 | 
			
		||||
    } else if (deltaY < 0) {
 | 
			
		||||
      decreaseBaseBrushSize()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="flex w-screen h-screen justify-center items-center"
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
      onMouseMove={onMouseMove}
 | 
			
		||||
      onMouseUp={onPointerUp}
 | 
			
		||||
      onWheel={handleScroll}
 | 
			
		||||
    >
 | 
			
		||||
      {renderCanvas()}
 | 
			
		||||
      {showBrush &&
 | 
			
		||||
        !isInpainting &&
 | 
			
		||||
        !isPanning &&
 | 
			
		||||
        (interactiveSegState.isInteractiveSeg
 | 
			
		||||
          ? renderInteractiveSegCursor()
 | 
			
		||||
          : renderBrush(getBrushStyle(x, y)))}
 | 
			
		||||
 | 
			
		||||
      {showRefBrush && renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
 | 
			
		||||
 | 
			
		||||
      <div className="fixed flex bottom-5 border px-4 py-2 rounded-[3rem] gap-8 items-center justify-center backdrop-filter backdrop-blur-md bg-background/70">
 | 
			
		||||
        <Slider
 | 
			
		||||
          className="w-48"
 | 
			
		||||
          defaultValue={[50]}
 | 
			
		||||
          min={MIN_BRUSH_SIZE}
 | 
			
		||||
          max={MAX_BRUSH_SIZE}
 | 
			
		||||
          step={1}
 | 
			
		||||
          tabIndex={-1}
 | 
			
		||||
          value={[baseBrushSize]}
 | 
			
		||||
          onValueChange={(vals) => handleSliderChange(vals[0])}
 | 
			
		||||
          onClick={() => setShowRefBrush(false)}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="flex gap-2">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Reset zoom & pan"
 | 
			
		||||
            disabled={scale === minScale && panned === false}
 | 
			
		||||
            onClick={resetZoom}
 | 
			
		||||
          >
 | 
			
		||||
            <Expand />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Undo"
 | 
			
		||||
            onClick={handleUndo}
 | 
			
		||||
            disabled={undoDisabled}
 | 
			
		||||
          >
 | 
			
		||||
            <Undo />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Redo"
 | 
			
		||||
            onClick={handleRedo}
 | 
			
		||||
            disabled={redoDisabled}
 | 
			
		||||
          >
 | 
			
		||||
            <Redo />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Show original image"
 | 
			
		||||
            onPointerDown={(ev) => {
 | 
			
		||||
              ev.preventDefault()
 | 
			
		||||
              setShowOriginal(() => {
 | 
			
		||||
                window.setTimeout(() => {
 | 
			
		||||
                  setSliderPos(100)
 | 
			
		||||
                }, 10)
 | 
			
		||||
                return true
 | 
			
		||||
              })
 | 
			
		||||
            }}
 | 
			
		||||
            onPointerUp={() => {
 | 
			
		||||
              window.setTimeout(() => {
 | 
			
		||||
                // 防止快速点击 show original image 按钮时图片消失
 | 
			
		||||
                setSliderPos(0)
 | 
			
		||||
              }, 10)
 | 
			
		||||
 | 
			
		||||
              window.setTimeout(() => {
 | 
			
		||||
                setShowOriginal(false)
 | 
			
		||||
              }, COMPARE_SLIDER_DURATION_MS)
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={renders.length === 0}
 | 
			
		||||
          >
 | 
			
		||||
            <Eye />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Save Image"
 | 
			
		||||
            disabled={!renders.length}
 | 
			
		||||
            onClick={download}
 | 
			
		||||
          >
 | 
			
		||||
            <Download />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
 | 
			
		||||
          {settings.enableManualInpainting &&
 | 
			
		||||
          settings.model.model_type === "inpaint" ? (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              tooltip="Run Inpainting"
 | 
			
		||||
              disabled={
 | 
			
		||||
                isProcessing || (!hadDrawSomething() && extraMasks.length === 0)
 | 
			
		||||
              }
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                runInpainting()
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Eraser />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,414 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { ExtenderDirection } from "@/lib/types"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import React, { useEffect, useState } from "react"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
 | 
			
		||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
 | 
			
		||||
 | 
			
		||||
const DRAG_HANDLE_BORDER = 2
 | 
			
		||||
 | 
			
		||||
interface EVData {
 | 
			
		||||
  initX: number
 | 
			
		||||
  initY: number
 | 
			
		||||
  initHeight: number
 | 
			
		||||
  initWidth: number
 | 
			
		||||
  startResizeX: number
 | 
			
		||||
  startResizeY: number
 | 
			
		||||
  ord: string // top/right/bottom/left
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  scale: number
 | 
			
		||||
  minHeight: number
 | 
			
		||||
  minWidth: number
 | 
			
		||||
  show: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const clamp = (
 | 
			
		||||
  newPos: number,
 | 
			
		||||
  newLength: number,
 | 
			
		||||
  oldPos: number,
 | 
			
		||||
  minLength: number
 | 
			
		||||
) => {
 | 
			
		||||
  if (newLength < minLength) {
 | 
			
		||||
    if (newPos === oldPos) {
 | 
			
		||||
      return [newPos, minLength]
 | 
			
		||||
    }
 | 
			
		||||
    return [newPos + newLength - minLength, minLength]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [newPos, newLength]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Extender = (props: Props) => {
 | 
			
		||||
  const { minHeight, minWidth, scale, show } = props
 | 
			
		||||
 | 
			
		||||
  const [
 | 
			
		||||
    isInpainting,
 | 
			
		||||
    imageHeight,
 | 
			
		||||
    imageWdith,
 | 
			
		||||
    isSD,
 | 
			
		||||
    { x, y, width, height },
 | 
			
		||||
    setX,
 | 
			
		||||
    setY,
 | 
			
		||||
    setWidth,
 | 
			
		||||
    setHeight,
 | 
			
		||||
    extenderDirection,
 | 
			
		||||
    isResizing,
 | 
			
		||||
    setIsResizing,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.isInpainting,
 | 
			
		||||
    state.imageHeight,
 | 
			
		||||
    state.imageWidth,
 | 
			
		||||
    state.isSD(),
 | 
			
		||||
    state.extenderState,
 | 
			
		||||
    state.setExtenderX,
 | 
			
		||||
    state.setExtenderY,
 | 
			
		||||
    state.setExtenderWidth,
 | 
			
		||||
    state.setExtenderHeight,
 | 
			
		||||
    state.settings.extenderDirection,
 | 
			
		||||
    state.isCropperExtenderResizing,
 | 
			
		||||
    state.setIsCropperExtenderResizing,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const [evData, setEVData] = useState<EVData>({
 | 
			
		||||
    initX: 0,
 | 
			
		||||
    initY: 0,
 | 
			
		||||
    initHeight: 0,
 | 
			
		||||
    initWidth: 0,
 | 
			
		||||
    startResizeX: 0,
 | 
			
		||||
    startResizeY: 0,
 | 
			
		||||
    ord: "top",
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onDragFocus = () => {
 | 
			
		||||
    // console.log("focus")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const clampLeftRight = (newX: number, newWidth: number) => {
 | 
			
		||||
    return clamp(newX, newWidth, x, minWidth)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const clampTopBottom = (newY: number, newHeight: number) => {
 | 
			
		||||
    return clamp(newY, newHeight, y, minHeight)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPointerMove = (e: PointerEvent) => {
 | 
			
		||||
    if (isInpainting) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const curX = e.clientX
 | 
			
		||||
    const curY = e.clientY
 | 
			
		||||
 | 
			
		||||
    const offsetY = Math.round((curY - evData.startResizeY) / scale)
 | 
			
		||||
    const offsetX = Math.round((curX - evData.startResizeX) / scale)
 | 
			
		||||
 | 
			
		||||
    const moveTop = () => {
 | 
			
		||||
      const newHeight = evData.initHeight - offsetY
 | 
			
		||||
      const newY = evData.initY + offsetY
 | 
			
		||||
      let clampedY = newY
 | 
			
		||||
      let clampedHeight = newHeight
 | 
			
		||||
      if (extenderDirection === ExtenderDirection.xy) {
 | 
			
		||||
        if (clampedY > 0) {
 | 
			
		||||
          clampedY = 0
 | 
			
		||||
          clampedHeight = evData.initHeight - Math.abs(evData.initY)
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const clamped = clampTopBottom(newY, newHeight)
 | 
			
		||||
        clampedY = clamped[0]
 | 
			
		||||
        clampedHeight = clamped[1]
 | 
			
		||||
      }
 | 
			
		||||
      setHeight(clampedHeight)
 | 
			
		||||
      setY(clampedY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveBottom = () => {
 | 
			
		||||
      const newHeight = evData.initHeight + offsetY
 | 
			
		||||
      let [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
 | 
			
		||||
      if (extenderDirection === ExtenderDirection.xy) {
 | 
			
		||||
        if (clampedHeight < Math.abs(clampedY) + imageHeight) {
 | 
			
		||||
          clampedHeight = Math.abs(clampedY) + imageHeight
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      setHeight(clampedHeight)
 | 
			
		||||
      setY(clampedY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveLeft = () => {
 | 
			
		||||
      const newWidth = evData.initWidth - offsetX
 | 
			
		||||
      const newX = evData.initX + offsetX
 | 
			
		||||
      let clampedX = newX
 | 
			
		||||
      let clampedWidth = newWidth
 | 
			
		||||
      if (extenderDirection === ExtenderDirection.xy) {
 | 
			
		||||
        if (clampedX > 0) {
 | 
			
		||||
          clampedX = 0
 | 
			
		||||
          clampedWidth = evData.initWidth - Math.abs(evData.initX)
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const clamped = clampLeftRight(newX, newWidth)
 | 
			
		||||
        clampedX = clamped[0]
 | 
			
		||||
        clampedWidth = clamped[1]
 | 
			
		||||
      }
 | 
			
		||||
      setWidth(clampedWidth)
 | 
			
		||||
      setX(clampedX)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moveRight = () => {
 | 
			
		||||
      const newWidth = evData.initWidth + offsetX
 | 
			
		||||
      let [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
 | 
			
		||||
      if (extenderDirection === ExtenderDirection.xy) {
 | 
			
		||||
        if (clampedWidth < Math.abs(clampedX) + imageWdith) {
 | 
			
		||||
          clampedWidth = Math.abs(clampedX) + imageWdith
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      setWidth(clampedWidth)
 | 
			
		||||
      setX(clampedX)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isResizing) {
 | 
			
		||||
      switch (evData.ord) {
 | 
			
		||||
        case "topleft": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "topright": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottomleft": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottomright": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "top": {
 | 
			
		||||
          moveTop()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "right": {
 | 
			
		||||
          moveRight()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "bottom": {
 | 
			
		||||
          moveBottom()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case "left": {
 | 
			
		||||
          moveLeft()
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPointerDone = () => {
 | 
			
		||||
    if (isResizing) {
 | 
			
		||||
      setIsResizing(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isResizing) {
 | 
			
		||||
      document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
 | 
			
		||||
      document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
      document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
      return () => {
 | 
			
		||||
        document.removeEventListener(
 | 
			
		||||
          "pointermove",
 | 
			
		||||
          onPointerMove,
 | 
			
		||||
          DOC_MOVE_OPTS
 | 
			
		||||
        )
 | 
			
		||||
        document.removeEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
 | 
			
		||||
        document.removeEventListener(
 | 
			
		||||
          "pointercancel",
 | 
			
		||||
          onPointerDone,
 | 
			
		||||
          DOC_MOVE_OPTS
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [isResizing, width, height, evData])
 | 
			
		||||
 | 
			
		||||
  const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
 | 
			
		||||
    const { ord } = (e.target as HTMLElement).dataset
 | 
			
		||||
    if (ord) {
 | 
			
		||||
      setIsResizing(true)
 | 
			
		||||
      setEVData({
 | 
			
		||||
        initX: x,
 | 
			
		||||
        initY: y,
 | 
			
		||||
        initHeight: height,
 | 
			
		||||
        initWidth: width,
 | 
			
		||||
        startResizeX: e.clientX,
 | 
			
		||||
        startResizeY: e.clientY,
 | 
			
		||||
        ord,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createDragHandle = (cursor: string, side1: string, side2: string) => {
 | 
			
		||||
    const sideLength = 12
 | 
			
		||||
    const halfSideLength = sideLength / 2
 | 
			
		||||
    const draghandleCls = `w-[${sideLength}px] h-[${sideLength}px] z-[4] absolute content-[''] block border-2 border-primary borde pointer-events-auto hover:bg-primary`
 | 
			
		||||
 | 
			
		||||
    let xTrans = "0"
 | 
			
		||||
    let yTrans = "0"
 | 
			
		||||
 | 
			
		||||
    let side2Key = side2
 | 
			
		||||
    let side2Val = `${-halfSideLength}px`
 | 
			
		||||
    if (side2 === "") {
 | 
			
		||||
      side2Val = "50%"
 | 
			
		||||
      if (side1 === "left" || side1 === "right") {
 | 
			
		||||
        side2Key = "top"
 | 
			
		||||
        yTrans = "-50%"
 | 
			
		||||
      } else {
 | 
			
		||||
        side2Key = "left"
 | 
			
		||||
        xTrans = "-50%"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={cn(draghandleCls, cursor)}
 | 
			
		||||
        style={{
 | 
			
		||||
          [side1]: -halfSideLength,
 | 
			
		||||
          [side2Key]: side2Val,
 | 
			
		||||
          transform: `translate(${xTrans}, ${yTrans}) scale(${1 / scale})`,
 | 
			
		||||
        }}
 | 
			
		||||
        data-ord={side1 + side2}
 | 
			
		||||
        aria-label={side1 + side2}
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        role="button"
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createCropSelection = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        onFocus={onDragFocus}
 | 
			
		||||
        onPointerDown={onCropPointerDown}
 | 
			
		||||
        className="absolute top-0 h-full w-full"
 | 
			
		||||
      >
 | 
			
		||||
        {[ExtenderDirection.y, ExtenderDirection.xy].includes(
 | 
			
		||||
          extenderDirection
 | 
			
		||||
        ) ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <div
 | 
			
		||||
              className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
 | 
			
		||||
              data-ord="top"
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
 | 
			
		||||
              data-ord="bottom"
 | 
			
		||||
            />
 | 
			
		||||
            {createDragHandle("cursor-ns-resize", "top", "")}
 | 
			
		||||
            {createDragHandle("cursor-ns-resize", "bottom", "")}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {[ExtenderDirection.x, ExtenderDirection.xy].includes(
 | 
			
		||||
          extenderDirection
 | 
			
		||||
        ) ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <div
 | 
			
		||||
              className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
 | 
			
		||||
              data-ord="right"
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
 | 
			
		||||
              data-ord="left"
 | 
			
		||||
            />
 | 
			
		||||
            {createDragHandle("cursor-ew-resize", "left", "")}
 | 
			
		||||
            {createDragHandle("cursor-ew-resize", "right", "")}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {extenderDirection === ExtenderDirection.xy ? (
 | 
			
		||||
          <>
 | 
			
		||||
            {createDragHandle("cursor-nw-resize", "top", "left")}
 | 
			
		||||
            {createDragHandle("cursor-ne-resize", "top", "right")}
 | 
			
		||||
            {createDragHandle("cursor-sw-resize", "bottom", "left")}
 | 
			
		||||
            {createDragHandle("cursor-se-resize", "bottom", "right")}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
 | 
			
		||||
    setEVData({
 | 
			
		||||
      initX: x,
 | 
			
		||||
      initY: y,
 | 
			
		||||
      initHeight: height,
 | 
			
		||||
      initWidth: width,
 | 
			
		||||
      startResizeX: e.clientX,
 | 
			
		||||
      startResizeY: e.clientY,
 | 
			
		||||
      ord: "",
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createInfoBar = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={twMerge(
 | 
			
		||||
          "border absolute pointer-events-auto px-2 py-1 rounded-full bg-background",
 | 
			
		||||
          "origin-top-left top-0 left-0"
 | 
			
		||||
        )}
 | 
			
		||||
        style={{
 | 
			
		||||
          transform: `scale(${(1 / scale) * 0.8})`,
 | 
			
		||||
        }}
 | 
			
		||||
        onPointerDown={onInfoBarPointerDown}
 | 
			
		||||
      >
 | 
			
		||||
        {/* TODO: 移动的时候会显示 brush */}
 | 
			
		||||
        {width} x {height}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const createBorder = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={cn("outline-dashed outline-primary")}
 | 
			
		||||
        style={{
 | 
			
		||||
          height,
 | 
			
		||||
          width,
 | 
			
		||||
          outlineWidth: `${(DRAG_HANDLE_BORDER / scale) * 1.3}px`,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (show === false || !isSD) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="absolute h-full w-full pointer-events-none z-[2]">
 | 
			
		||||
      <div
 | 
			
		||||
        className="relative pointer-events-none z-[2] [box-shadow:0_0_0_9999px_rgba(0,_0,_0,_0.5)]"
 | 
			
		||||
        style={{ height, width, left: x, top: y }}
 | 
			
		||||
      >
 | 
			
		||||
        {createBorder()}
 | 
			
		||||
        {createInfoBar()}
 | 
			
		||||
        {createCropSelection()}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Extender
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,343 @@
 | 
			
		|||
import {
 | 
			
		||||
  SyntheticEvent,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useState,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useRef,
 | 
			
		||||
  FormEvent,
 | 
			
		||||
} from "react"
 | 
			
		||||
import _ from "lodash"
 | 
			
		||||
import PhotoAlbum from "react-photo-album"
 | 
			
		||||
import { BarsArrowDownIcon, BarsArrowUpIcon } from "@heroicons/react/24/outline"
 | 
			
		||||
import {
 | 
			
		||||
  MagnifyingGlassIcon,
 | 
			
		||||
  ViewHorizontalIcon,
 | 
			
		||||
  ViewGridIcon,
 | 
			
		||||
} from "@radix-ui/react-icons"
 | 
			
		||||
import { useToggle } from "react-use"
 | 
			
		||||
import { useDebounce } from "@uidotdev/usehooks"
 | 
			
		||||
import Fuse from "fuse.js"
 | 
			
		||||
import { useToast } from "@/components/ui/use-toast"
 | 
			
		||||
import { API_ENDPOINT, getMedias } from "@/lib/api"
 | 
			
		||||
import { IconButton } from "./ui/button"
 | 
			
		||||
import { Input } from "./ui/input"
 | 
			
		||||
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
 | 
			
		||||
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "./ui/select"
 | 
			
		||||
import { ScrollArea } from "./ui/scroll-area"
 | 
			
		||||
import { DialogTrigger } from "@radix-ui/react-dialog"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { Filename, SortBy, SortOrder } from "@/lib/types"
 | 
			
		||||
import { FolderClosed } from "lucide-react"
 | 
			
		||||
import useHotKey from "@/hooks/useHotkey"
 | 
			
		||||
 | 
			
		||||
interface Photo {
 | 
			
		||||
  src: string
 | 
			
		||||
  height: number
 | 
			
		||||
  width: number
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SORT_BY_NAME = "Name"
 | 
			
		||||
const SORT_BY_CREATED_TIME = "Created time"
 | 
			
		||||
const SORT_BY_MODIFIED_TIME = "Modified time"
 | 
			
		||||
 | 
			
		||||
const IMAGE_TAB = "input"
 | 
			
		||||
const OUTPUT_TAB = "output"
 | 
			
		||||
 | 
			
		||||
const SortByMap = {
 | 
			
		||||
  [SortBy.NAME]: SORT_BY_NAME,
 | 
			
		||||
  [SortBy.CTIME]: SORT_BY_CREATED_TIME,
 | 
			
		||||
  [SortBy.MTIME]: SORT_BY_MODIFIED_TIME,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  onPhotoClick(tab: string, filename: string): void
 | 
			
		||||
  photoWidth: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function FileManager(props: Props) {
 | 
			
		||||
  const { onPhotoClick, photoWidth } = props
 | 
			
		||||
  const [open, toggleOpen] = useToggle(false)
 | 
			
		||||
 | 
			
		||||
  const [fileManagerState, updateFileManagerState] = useStore((state) => [
 | 
			
		||||
    state.fileManagerState,
 | 
			
		||||
    state.updateFileManagerState,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const { toast } = useToast()
 | 
			
		||||
  const [scrollTop, setScrollTop] = useState(0)
 | 
			
		||||
  const [closeScrollTop, setCloseScrollTop] = useState(0)
 | 
			
		||||
 | 
			
		||||
  const ref = useRef(null)
 | 
			
		||||
  const debouncedSearchText = useDebounce(fileManagerState.searchText, 300)
 | 
			
		||||
  const [tab, setTab] = useState(IMAGE_TAB)
 | 
			
		||||
  const [filenames, setFilenames] = useState<Filename[]>([])
 | 
			
		||||
  const [photos, setPhotos] = useState<Photo[]>([])
 | 
			
		||||
  const [photoIndex, setPhotoIndex] = useState(0)
 | 
			
		||||
 | 
			
		||||
  useHotKey("f", () => {
 | 
			
		||||
    toggleOpen()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "left",
 | 
			
		||||
    () => {
 | 
			
		||||
      let newIndex = photoIndex
 | 
			
		||||
      if (photoIndex > 0) {
 | 
			
		||||
        newIndex = photoIndex - 1
 | 
			
		||||
      }
 | 
			
		||||
      setPhotoIndex(newIndex)
 | 
			
		||||
      onPhotoClick(tab, photos[newIndex].name)
 | 
			
		||||
    },
 | 
			
		||||
    [photoIndex, photos]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "right",
 | 
			
		||||
    () => {
 | 
			
		||||
      let newIndex = photoIndex
 | 
			
		||||
      if (photoIndex < photos.length - 1) {
 | 
			
		||||
        newIndex = photoIndex + 1
 | 
			
		||||
      }
 | 
			
		||||
      setPhotoIndex(newIndex)
 | 
			
		||||
      onPhotoClick(tab, photos[newIndex].name)
 | 
			
		||||
    },
 | 
			
		||||
    [photoIndex, photos]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!open) {
 | 
			
		||||
      setCloseScrollTop(scrollTop)
 | 
			
		||||
    }
 | 
			
		||||
  }, [open, scrollTop])
 | 
			
		||||
 | 
			
		||||
  const onRefChange = useCallback(
 | 
			
		||||
    (node: HTMLDivElement) => {
 | 
			
		||||
      if (node !== null) {
 | 
			
		||||
        if (open) {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            // TODO: without timeout, scrollTo not work, why?
 | 
			
		||||
            node.scrollTo({ top: closeScrollTop, left: 0 })
 | 
			
		||||
          }, 100)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [open, closeScrollTop]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const filenames = await getMedias(tab)
 | 
			
		||||
        setFilenames(filenames)
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        toast({
 | 
			
		||||
          variant: "destructive",
 | 
			
		||||
          title: "Uh oh! Something went wrong.",
 | 
			
		||||
          description: e.message ? e.message : e.toString(),
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
  }, [tab])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!open) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        let filteredFilenames = filenames
 | 
			
		||||
        if (debouncedSearchText) {
 | 
			
		||||
          const fuse = new Fuse(filteredFilenames, {
 | 
			
		||||
            keys: ["name"],
 | 
			
		||||
          })
 | 
			
		||||
          const items = fuse.search(debouncedSearchText)
 | 
			
		||||
          filteredFilenames = items.map(
 | 
			
		||||
            (item) => filteredFilenames[item.refIndex]
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        filteredFilenames = _.orderBy(
 | 
			
		||||
          filteredFilenames,
 | 
			
		||||
          fileManagerState.sortBy,
 | 
			
		||||
          fileManagerState.sortOrder
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const newPhotos = filteredFilenames.map((filename: Filename) => {
 | 
			
		||||
          const width = photoWidth
 | 
			
		||||
          const height = filename.height * (width / filename.width)
 | 
			
		||||
          const src = `${API_ENDPOINT}/media_thumbnail_file?tab=${tab}&filename=${encodeURIComponent(
 | 
			
		||||
            filename.name
 | 
			
		||||
          )}&width=${Math.ceil(width)}&height=${Math.ceil(height)}`
 | 
			
		||||
          return { src, height, width, name: filename.name }
 | 
			
		||||
        })
 | 
			
		||||
        setPhotos(newPhotos)
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        toast({
 | 
			
		||||
          variant: "destructive",
 | 
			
		||||
          title: "Uh oh! Something went wrong.",
 | 
			
		||||
          description: e.message ? e.message : e.toString(),
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
  }, [filenames, debouncedSearchText, fileManagerState, photoWidth, open])
 | 
			
		||||
 | 
			
		||||
  const onScroll = (event: SyntheticEvent) => {
 | 
			
		||||
    setScrollTop(event.currentTarget.scrollTop)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onClick = ({ index }: { index: number }) => {
 | 
			
		||||
    toggleOpen()
 | 
			
		||||
    setPhotoIndex(index)
 | 
			
		||||
    onPhotoClick(tab, photos[index].name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderTitle = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex justify-start items-center gap-[12px]">
 | 
			
		||||
        <div>{`Images (${photos.length})`}</div>
 | 
			
		||||
        <div className="flex">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Rows layout"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              updateFileManagerState({ layout: "rows" })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <ViewHorizontalIcon
 | 
			
		||||
              className={fileManagerState.layout !== "rows" ? "opacity-50" : ""}
 | 
			
		||||
            />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            tooltip="Grid layout"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              updateFileManagerState({ layout: "masonry" })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <ViewGridIcon
 | 
			
		||||
              className={
 | 
			
		||||
                fileManagerState.layout !== "masonry" ? "opacity-50" : ""
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={open} onOpenChange={toggleOpen}>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <IconButton tooltip="File Manager">
 | 
			
		||||
          <FolderClosed />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent className="h-4/5 max-w-6xl">
 | 
			
		||||
        <DialogTitle>{renderTitle()}</DialogTitle>
 | 
			
		||||
        <div className="flex justify-between gap-8 items-center">
 | 
			
		||||
          <div className="flex relative justify-start items-center">
 | 
			
		||||
            <MagnifyingGlassIcon className="absolute left-[8px]" />
 | 
			
		||||
            <Input
 | 
			
		||||
              ref={ref}
 | 
			
		||||
              value={fileManagerState.searchText}
 | 
			
		||||
              className="w-[250px] pl-[30px]"
 | 
			
		||||
              tabIndex={-1}
 | 
			
		||||
              onInput={(evt: FormEvent<HTMLInputElement>) => {
 | 
			
		||||
                evt.preventDefault()
 | 
			
		||||
                evt.stopPropagation()
 | 
			
		||||
                const target = evt.target as HTMLInputElement
 | 
			
		||||
                updateFileManagerState({ searchText: target.value })
 | 
			
		||||
              }}
 | 
			
		||||
              placeholder="Search by file name"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Tabs defaultValue={tab} onValueChange={(val) => setTab(val)}>
 | 
			
		||||
            <TabsList aria-label="Manage your account">
 | 
			
		||||
              <TabsTrigger value={IMAGE_TAB}>Image Directory</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value={OUTPUT_TAB}>Output Directory</TabsTrigger>
 | 
			
		||||
            </TabsList>
 | 
			
		||||
          </Tabs>
 | 
			
		||||
 | 
			
		||||
          <div className="flex gap-2">
 | 
			
		||||
            <div className="flex gap-1">
 | 
			
		||||
              <Select
 | 
			
		||||
                value={SortByMap[fileManagerState.sortBy]}
 | 
			
		||||
                onValueChange={(val) => {
 | 
			
		||||
                  switch (val) {
 | 
			
		||||
                    case SORT_BY_NAME:
 | 
			
		||||
                      updateFileManagerState({ sortBy: SortBy.NAME })
 | 
			
		||||
                      break
 | 
			
		||||
                    case SORT_BY_CREATED_TIME:
 | 
			
		||||
                      updateFileManagerState({ sortBy: SortBy.CTIME })
 | 
			
		||||
                      break
 | 
			
		||||
                    case SORT_BY_MODIFIED_TIME:
 | 
			
		||||
                      updateFileManagerState({ sortBy: SortBy.MTIME })
 | 
			
		||||
                      break
 | 
			
		||||
                    default:
 | 
			
		||||
                      break
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <SelectTrigger className="w-[140px]">
 | 
			
		||||
                  <SelectValue />
 | 
			
		||||
                </SelectTrigger>
 | 
			
		||||
                <SelectContent>
 | 
			
		||||
                  {Object.values(SortByMap).map((val) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                      <SelectItem value={val} key={val}>
 | 
			
		||||
                        {val}
 | 
			
		||||
                      </SelectItem>
 | 
			
		||||
                    )
 | 
			
		||||
                  })}
 | 
			
		||||
                </SelectContent>
 | 
			
		||||
              </Select>
 | 
			
		||||
 | 
			
		||||
              {fileManagerState.sortOrder === SortOrder.DESCENDING ? (
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  tooltip="Descending Order"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    updateFileManagerState({ sortOrder: SortOrder.ASCENDING })
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <BarsArrowDownIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  tooltip="Ascending Order"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    updateFileManagerState({ sortOrder: SortOrder.DESCENDING })
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <BarsArrowUpIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <ScrollArea
 | 
			
		||||
          className="w-full h-full rounded-md"
 | 
			
		||||
          onScroll={onScroll}
 | 
			
		||||
          ref={onRefChange}
 | 
			
		||||
        >
 | 
			
		||||
          <PhotoAlbum
 | 
			
		||||
            layout={fileManagerState.layout}
 | 
			
		||||
            photos={photos}
 | 
			
		||||
            spacing={12}
 | 
			
		||||
            padding={0}
 | 
			
		||||
            onClick={onClick}
 | 
			
		||||
          />
 | 
			
		||||
        </ScrollArea>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
import { useState } from "react"
 | 
			
		||||
import useResolution from "@/hooks/useResolution"
 | 
			
		||||
 | 
			
		||||
type FileSelectProps = {
 | 
			
		||||
  onSelection: (file: File) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function FileSelect(props: FileSelectProps) {
 | 
			
		||||
  const { onSelection } = props
 | 
			
		||||
 | 
			
		||||
  const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
 | 
			
		||||
 | 
			
		||||
  const resolution = useResolution()
 | 
			
		||||
 | 
			
		||||
  function onFileSelected(file: File) {
 | 
			
		||||
    if (!file) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    // Skip non-image files
 | 
			
		||||
    const isImage = file.type.match("image.*")
 | 
			
		||||
    if (!isImage) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      // Check if file is larger than 20mb
 | 
			
		||||
      if (file.size > 20 * 1024 * 1024) {
 | 
			
		||||
        throw new Error("file too large")
 | 
			
		||||
      }
 | 
			
		||||
      onSelection(file)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // eslint-disable-next-line
 | 
			
		||||
      alert(`error: ${(e as any).message}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="absolute flex w-screen h-screen justify-center items-center pointer-events-none">
 | 
			
		||||
      <label
 | 
			
		||||
        htmlFor={uploadElemId}
 | 
			
		||||
        className="grid bg-background border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:bg-primary hover:text-primary-foreground pointer-events-auto"
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          className="grid p-16 w-full h-full"
 | 
			
		||||
          onDragOver={(ev) => {
 | 
			
		||||
            ev.stopPropagation()
 | 
			
		||||
            ev.preventDefault()
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <input
 | 
			
		||||
            className="hidden"
 | 
			
		||||
            id={uploadElemId}
 | 
			
		||||
            name={uploadElemId}
 | 
			
		||||
            type="file"
 | 
			
		||||
            onChange={(ev) => {
 | 
			
		||||
              const file = ev.currentTarget.files?.[0]
 | 
			
		||||
              if (file) {
 | 
			
		||||
                onFileSelected(file)
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            accept="image/png, image/jpeg"
 | 
			
		||||
          />
 | 
			
		||||
          <p className="text-center">
 | 
			
		||||
            {resolution === "desktop"
 | 
			
		||||
              ? "Click here or drag an image file"
 | 
			
		||||
              : "Tap here to load your picture"}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,198 @@
 | 
			
		|||
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 { 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"
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onRerunMouseEnter = () => {
 | 
			
		||||
    showPrevMask()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onRerunMouseLeave = () => {
 | 
			
		||||
    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)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <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}
 | 
			
		||||
            tooltip="Rerun previous mask"
 | 
			
		||||
            onClick={handleRerunLastMask}
 | 
			
		||||
            onMouseEnter={onRerunMouseEnter}
 | 
			
		||||
            onMouseLeave={onRerunMouseLeave}
 | 
			
		||||
          >
 | 
			
		||||
            <RotateCw />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {model.need_prompt ? <PromptInput /> : <></>}
 | 
			
		||||
 | 
			
		||||
      <div className="flex gap-1">
 | 
			
		||||
        <Coffee />
 | 
			
		||||
        <Shortcuts />
 | 
			
		||||
        {serverConfig.disableModelSwitch ? <></> : <SettingsDialog />}
 | 
			
		||||
      </div>
 | 
			
		||||
    </header>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Header
 | 
			
		||||
| 
						 | 
				
			
			@ -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 bg-background">
 | 
			
		||||
      {imageWidth}x{imageHeight}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ImageSize
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,130 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { Button } from "./ui/button"
 | 
			
		||||
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
 | 
			
		||||
 | 
			
		||||
interface InteractiveSegReplaceModal {
 | 
			
		||||
  show: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onCleanClick: () => void
 | 
			
		||||
  onReplaceClick: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const InteractiveSegReplaceModal = (props: InteractiveSegReplaceModal) => {
 | 
			
		||||
  const { show, onClose, onCleanClick, onReplaceClick } = props
 | 
			
		||||
 | 
			
		||||
  const onOpenChange = (open: boolean) => {
 | 
			
		||||
    if (!open) {
 | 
			
		||||
      onClose()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={show} onOpenChange={onOpenChange}>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogTitle>Do you want to remove it or create a new one?</DialogTitle>
 | 
			
		||||
        <div className="flex gap-[12px] w-full justify-end items-center">
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              onClose()
 | 
			
		||||
              onCleanClick()
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Remove
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={onReplaceClick}>Create new</Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const InteractiveSegConfirmActions = () => {
 | 
			
		||||
  const [
 | 
			
		||||
    interactiveSegState,
 | 
			
		||||
    resetInteractiveSegState,
 | 
			
		||||
    handleInteractiveSegAccept,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.interactiveSegState,
 | 
			
		||||
    state.resetInteractiveSegState,
 | 
			
		||||
    state.handleInteractiveSegAccept,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  if (!interactiveSegState.isInteractiveSeg) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="z-10 absolute top-[68px] rounded-xl border-solid border p-[8px] left-1/2 translate-x-[-50%] flex justify-center items-center gap-[8px] bg-background">
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          resetInteractiveSegState()
 | 
			
		||||
        }}
 | 
			
		||||
        size="sm"
 | 
			
		||||
        variant="secondary"
 | 
			
		||||
      >
 | 
			
		||||
        Cancel
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        size="sm"
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          handleInteractiveSegAccept()
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        Accept
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ItemProps {
 | 
			
		||||
  x: number
 | 
			
		||||
  y: number
 | 
			
		||||
  positive: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Item = (props: ItemProps) => {
 | 
			
		||||
  const { x, y, positive } = props
 | 
			
		||||
  const name = positive
 | 
			
		||||
    ? "bg-[rgba(21,_215,_121,_0.936)] outline-[rgba(98,255,179,0.31)]"
 | 
			
		||||
    : "bg-[rgba(237,_49,_55,_0.942)] outline-[rgba(255,89,95,0.31)]"
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`absolute h-[10px] w-[10px] rounded-[50%] ${name} outline-8 outline`}
 | 
			
		||||
      style={{
 | 
			
		||||
        left: x,
 | 
			
		||||
        top: y,
 | 
			
		||||
        transform: "translate(-50%, -50%)",
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const InteractiveSegPoints = () => {
 | 
			
		||||
  const clicks = useStore((state) => state.interactiveSegState.clicks)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="absolute h-full w-full overflow-hidden pointer-events-none">
 | 
			
		||||
      {clicks.map((click) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Item
 | 
			
		||||
            key={click[3]}
 | 
			
		||||
            x={click[0]}
 | 
			
		||||
            y={click[1]}
 | 
			
		||||
            positive={click[2] === 1}
 | 
			
		||||
          />
 | 
			
		||||
        )
 | 
			
		||||
      })}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const InteractiveSeg = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <InteractiveSegConfirmActions />
 | 
			
		||||
      {/* <InteractiveSegReplaceModal /> */}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { InteractiveSeg, InteractiveSegPoints }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuSub,
 | 
			
		||||
  DropdownMenuSubContent,
 | 
			
		||||
  DropdownMenuSubTrigger,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
} from "./ui/dropdown-menu"
 | 
			
		||||
import { Button } from "./ui/button"
 | 
			
		||||
import {
 | 
			
		||||
  Blocks,
 | 
			
		||||
  Fullscreen,
 | 
			
		||||
  MousePointerClick,
 | 
			
		||||
  Slice,
 | 
			
		||||
  Smile,
 | 
			
		||||
} from "lucide-react"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { PluginInfo } from "@/lib/types"
 | 
			
		||||
 | 
			
		||||
export enum PluginName {
 | 
			
		||||
  RemoveBG = "RemoveBG",
 | 
			
		||||
  AnimeSeg = "AnimeSeg",
 | 
			
		||||
  RealESRGAN = "RealESRGAN",
 | 
			
		||||
  GFPGAN = "GFPGAN",
 | 
			
		||||
  RestoreFormer = "RestoreFormer",
 | 
			
		||||
  InteractiveSeg = "InteractiveSeg",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: get plugin config from server and using form-render??
 | 
			
		||||
const pluginMap = {
 | 
			
		||||
  [PluginName.RemoveBG]: {
 | 
			
		||||
    IconClass: Slice,
 | 
			
		||||
    showName: "RemoveBG",
 | 
			
		||||
  },
 | 
			
		||||
  [PluginName.AnimeSeg]: {
 | 
			
		||||
    IconClass: Slice,
 | 
			
		||||
    showName: "Anime Segmentation",
 | 
			
		||||
  },
 | 
			
		||||
  [PluginName.RealESRGAN]: {
 | 
			
		||||
    IconClass: Fullscreen,
 | 
			
		||||
    showName: "RealESRGAN",
 | 
			
		||||
  },
 | 
			
		||||
  [PluginName.GFPGAN]: {
 | 
			
		||||
    IconClass: Smile,
 | 
			
		||||
    showName: "GFPGAN",
 | 
			
		||||
  },
 | 
			
		||||
  [PluginName.RestoreFormer]: {
 | 
			
		||||
    IconClass: Smile,
 | 
			
		||||
    showName: "RestoreFormer",
 | 
			
		||||
  },
 | 
			
		||||
  [PluginName.InteractiveSeg]: {
 | 
			
		||||
    IconClass: MousePointerClick,
 | 
			
		||||
    showName: "Interactive Segmentation",
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Plugins = () => {
 | 
			
		||||
  const [
 | 
			
		||||
    file,
 | 
			
		||||
    plugins,
 | 
			
		||||
    isPluginRunning,
 | 
			
		||||
    updateInteractiveSegState,
 | 
			
		||||
    runRenderablePlugin,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.file,
 | 
			
		||||
    state.serverConfig.plugins,
 | 
			
		||||
    state.isPluginRunning,
 | 
			
		||||
    state.updateInteractiveSegState,
 | 
			
		||||
    state.runRenderablePlugin,
 | 
			
		||||
  ])
 | 
			
		||||
  const disabled = !file
 | 
			
		||||
 | 
			
		||||
  if (plugins.length === 0) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onPluginClick = (genMask: boolean, pluginName: string) => {
 | 
			
		||||
    if (pluginName === PluginName.InteractiveSeg) {
 | 
			
		||||
      updateInteractiveSegState({ isInteractiveSeg: true })
 | 
			
		||||
    } else {
 | 
			
		||||
      runRenderablePlugin(genMask, pluginName)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderRealESRGANPlugin = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <DropdownMenuSub key="RealESRGAN">
 | 
			
		||||
        <DropdownMenuSubTrigger disabled={disabled}>
 | 
			
		||||
          <div className="flex gap-2 items-center">
 | 
			
		||||
            <Fullscreen />
 | 
			
		||||
            RealESRGAN
 | 
			
		||||
          </div>
 | 
			
		||||
        </DropdownMenuSubTrigger>
 | 
			
		||||
        <DropdownMenuSubContent>
 | 
			
		||||
          <DropdownMenuItem
 | 
			
		||||
            onClick={() =>
 | 
			
		||||
              runRenderablePlugin(false, PluginName.RealESRGAN, { upscale: 2 })
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            upscale 2x
 | 
			
		||||
          </DropdownMenuItem>
 | 
			
		||||
          <DropdownMenuItem
 | 
			
		||||
            onClick={() =>
 | 
			
		||||
              runRenderablePlugin(false, PluginName.RealESRGAN, { upscale: 4 })
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            upscale 4x
 | 
			
		||||
          </DropdownMenuItem>
 | 
			
		||||
        </DropdownMenuSubContent>
 | 
			
		||||
      </DropdownMenuSub>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderGenImageAndMaskPlugin = (plugin: PluginInfo) => {
 | 
			
		||||
    const { IconClass, showName } = pluginMap[plugin.name as PluginName]
 | 
			
		||||
    return (
 | 
			
		||||
      <DropdownMenuSub key={plugin.name}>
 | 
			
		||||
        <DropdownMenuSubTrigger disabled={disabled}>
 | 
			
		||||
          <div className="flex gap-2 items-center">
 | 
			
		||||
            <IconClass className="p-1" />
 | 
			
		||||
            {showName}
 | 
			
		||||
          </div>
 | 
			
		||||
        </DropdownMenuSubTrigger>
 | 
			
		||||
        <DropdownMenuSubContent>
 | 
			
		||||
          <DropdownMenuItem onClick={() => onPluginClick(false, plugin.name)}>
 | 
			
		||||
            Remove Background
 | 
			
		||||
          </DropdownMenuItem>
 | 
			
		||||
          <DropdownMenuItem onClick={() => onPluginClick(true, plugin.name)}>
 | 
			
		||||
            Generate Mask
 | 
			
		||||
          </DropdownMenuItem>
 | 
			
		||||
        </DropdownMenuSubContent>
 | 
			
		||||
      </DropdownMenuSub>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderPlugins = () => {
 | 
			
		||||
    return plugins.map((plugin: PluginInfo) => {
 | 
			
		||||
      const { IconClass, showName } = pluginMap[plugin.name as PluginName]
 | 
			
		||||
      if (plugin.name === PluginName.RealESRGAN) {
 | 
			
		||||
        return renderRealESRGANPlugin()
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        plugin.name === PluginName.RemoveBG ||
 | 
			
		||||
        plugin.name === PluginName.AnimeSeg
 | 
			
		||||
      ) {
 | 
			
		||||
        return renderGenImageAndMaskPlugin(plugin)
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <DropdownMenuItem
 | 
			
		||||
          key={plugin.name}
 | 
			
		||||
          onClick={() => onPluginClick(false, plugin.name)}
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
        >
 | 
			
		||||
          <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 bg-background outline-none"
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
      >
 | 
			
		||||
        <Button variant="ghost" size="icon" asChild className="p-1.5">
 | 
			
		||||
          {isPluginRunning ? (
 | 
			
		||||
            <div role="status">
 | 
			
		||||
              <svg
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                className="w-5 h-5 animate-spin fill-primary"
 | 
			
		||||
                viewBox="0 0 100 101"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
              >
 | 
			
		||||
                <path
 | 
			
		||||
                  d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
 | 
			
		||||
                  fill="currentColor"
 | 
			
		||||
                />
 | 
			
		||||
                <path
 | 
			
		||||
                  d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
 | 
			
		||||
                  fill="currentFill"
 | 
			
		||||
                />
 | 
			
		||||
              </svg>
 | 
			
		||||
            </div>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Blocks strokeWidth={1} />
 | 
			
		||||
          )}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DropdownMenuTrigger>
 | 
			
		||||
      <DropdownMenuContent side="bottom" align="start">
 | 
			
		||||
        {renderPlugins()}
 | 
			
		||||
      </DropdownMenuContent>
 | 
			
		||||
    </DropdownMenu>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Plugins
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
import React, { FormEvent, useRef } from "react"
 | 
			
		||||
import { Button } from "./ui/button"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { useClickAway, useToggle } from "react-use"
 | 
			
		||||
import { Textarea } from "./ui/textarea"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const PromptInput = () => {
 | 
			
		||||
  const [
 | 
			
		||||
    isProcessing,
 | 
			
		||||
    prompt,
 | 
			
		||||
    updateSettings,
 | 
			
		||||
    runInpainting,
 | 
			
		||||
    showPrevMask,
 | 
			
		||||
    hidePrevMask,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.getIsProcessing(),
 | 
			
		||||
    state.settings.prompt,
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
    state.runInpainting,
 | 
			
		||||
    state.showPrevMask,
 | 
			
		||||
    state.hidePrevMask,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const [showScroll, toggleShowScroll] = useToggle(false)
 | 
			
		||||
 | 
			
		||||
  const ref = useRef(null)
 | 
			
		||||
  useClickAway<MouseEvent>(ref, () => {
 | 
			
		||||
    if (ref?.current) {
 | 
			
		||||
      const input = ref.current as HTMLTextAreaElement
 | 
			
		||||
      input.blur()
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const handleOnInput = (evt: FormEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
    evt.preventDefault()
 | 
			
		||||
    evt.stopPropagation()
 | 
			
		||||
    const target = evt.target as HTMLTextAreaElement
 | 
			
		||||
    updateSettings({ prompt: target.value })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleRepaintClick = () => {
 | 
			
		||||
    if (!isProcessing) {
 | 
			
		||||
      runInpainting()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onKeyUp = (e: React.KeyboardEvent) => {
 | 
			
		||||
    if (e.key === "Enter" && e.ctrlKey && prompt.length !== 0) {
 | 
			
		||||
      handleRepaintClick()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onMouseEnter = () => {
 | 
			
		||||
    showPrevMask()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onMouseLeave = () => {
 | 
			
		||||
    hidePrevMask()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex gap-4 relative w-full justify-center h-full">
 | 
			
		||||
      <div className="absolute flex gap-4">
 | 
			
		||||
        <Textarea
 | 
			
		||||
          ref={ref}
 | 
			
		||||
          placeholder="I want to repaint of..."
 | 
			
		||||
          className={cn(
 | 
			
		||||
            showScroll ? "focus:overflow-y-auto" : "overflow-y-hidden",
 | 
			
		||||
            "min-h-[32px] h-[32px] overflow-x-hidden focus:h-[120px] overflow-y-hidden transition-[height] w-[500px] py-1 px-3 bg-background resize-none"
 | 
			
		||||
          )}
 | 
			
		||||
          style={{
 | 
			
		||||
            scrollbarGutter: "stable",
 | 
			
		||||
          }}
 | 
			
		||||
          value={prompt}
 | 
			
		||||
          onInput={handleOnInput}
 | 
			
		||||
          onKeyUp={onKeyUp}
 | 
			
		||||
          onTransitionEnd={toggleShowScroll}
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
          size="sm"
 | 
			
		||||
          onClick={handleRepaintClick}
 | 
			
		||||
          disabled={isProcessing}
 | 
			
		||||
          onMouseEnter={onMouseEnter}
 | 
			
		||||
          onMouseLeave={onMouseLeave}
 | 
			
		||||
        >
 | 
			
		||||
          Paint
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PromptInput
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,766 @@
 | 
			
		|||
import { IconButton } from "@/components/ui/button"
 | 
			
		||||
import { useToggle } from "@uidotdev/usehooks"
 | 
			
		||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog"
 | 
			
		||||
import { Settings } from "lucide-react"
 | 
			
		||||
import { zodResolver } from "@hookform/resolvers/zod"
 | 
			
		||||
import { useForm } from "react-hook-form"
 | 
			
		||||
import * as z from "zod"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import { Separator } from "@/components/ui/separator"
 | 
			
		||||
import {
 | 
			
		||||
  Form,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormField,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
} from "@/components/ui/form"
 | 
			
		||||
import { Switch } from "./ui/switch"
 | 
			
		||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
 | 
			
		||||
import { useEffect, useState } from "react"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { useQuery } from "@tanstack/react-query"
 | 
			
		||||
import { getServerConfig, switchModel, switchPluginModel } from "@/lib/api"
 | 
			
		||||
import { ModelInfo, PluginName } from "@/lib/types"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { ScrollArea } from "./ui/scroll-area"
 | 
			
		||||
import { useToast } from "./ui/use-toast"
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogHeader,
 | 
			
		||||
} from "./ui/alert-dialog"
 | 
			
		||||
import {
 | 
			
		||||
  MODEL_TYPE_DIFFUSERS_SD,
 | 
			
		||||
  MODEL_TYPE_DIFFUSERS_SDXL,
 | 
			
		||||
  MODEL_TYPE_DIFFUSERS_SDXL_INPAINT,
 | 
			
		||||
  MODEL_TYPE_DIFFUSERS_SD_INPAINT,
 | 
			
		||||
  MODEL_TYPE_INPAINT,
 | 
			
		||||
  MODEL_TYPE_OTHER,
 | 
			
		||||
} from "@/lib/const"
 | 
			
		||||
import useHotKey from "@/hooks/useHotkey"
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "./ui/select"
 | 
			
		||||
 | 
			
		||||
const formSchema = z.object({
 | 
			
		||||
  enableFileManager: z.boolean(),
 | 
			
		||||
  inputDirectory: z.string(),
 | 
			
		||||
  outputDirectory: z.string(),
 | 
			
		||||
  enableDownloadMask: z.boolean(),
 | 
			
		||||
  enableManualInpainting: z.boolean(),
 | 
			
		||||
  enableUploadMask: z.boolean(),
 | 
			
		||||
  enableAutoExtractPrompt: z.boolean(),
 | 
			
		||||
  removeBGModel: z.string(),
 | 
			
		||||
  realesrganModel: z.string(),
 | 
			
		||||
  interactiveSegModel: z.string(),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const TAB_GENERAL = "General"
 | 
			
		||||
const TAB_MODEL = "Model"
 | 
			
		||||
const TAB_PLUGINS = "Plugins"
 | 
			
		||||
// const TAB_FILE_MANAGER = "File Manager"
 | 
			
		||||
 | 
			
		||||
const TAB_NAMES = [TAB_MODEL, TAB_GENERAL, TAB_PLUGINS]
 | 
			
		||||
 | 
			
		||||
export function SettingsDialog() {
 | 
			
		||||
  const [open, toggleOpen] = useToggle(false)
 | 
			
		||||
  const [tab, setTab] = useState(TAB_MODEL)
 | 
			
		||||
  const [
 | 
			
		||||
    updateAppState,
 | 
			
		||||
    settings,
 | 
			
		||||
    updateSettings,
 | 
			
		||||
    fileManagerState,
 | 
			
		||||
    setAppModel,
 | 
			
		||||
    setServerConfig,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.updateAppState,
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
    state.fileManagerState,
 | 
			
		||||
    state.setModel,
 | 
			
		||||
    state.setServerConfig,
 | 
			
		||||
  ])
 | 
			
		||||
  const { toast } = useToast()
 | 
			
		||||
  const [model, setModel] = useState<ModelInfo>(settings.model)
 | 
			
		||||
  const [modelSwitchingTexts, setModelSwitchingTexts] = useState<string[]>([])
 | 
			
		||||
  const openModelSwitching = modelSwitchingTexts.length > 0
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setModel(settings.model)
 | 
			
		||||
  }, [settings.model])
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    data: serverConfig,
 | 
			
		||||
    status,
 | 
			
		||||
    refetch,
 | 
			
		||||
  } = useQuery({
 | 
			
		||||
    queryKey: ["serverConfig"],
 | 
			
		||||
    queryFn: getServerConfig,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // 1. Define your form.
 | 
			
		||||
  const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
    resolver: zodResolver(formSchema),
 | 
			
		||||
    defaultValues: {
 | 
			
		||||
      enableDownloadMask: settings.enableDownloadMask,
 | 
			
		||||
      enableManualInpainting: settings.enableManualInpainting,
 | 
			
		||||
      enableUploadMask: settings.enableUploadMask,
 | 
			
		||||
      enableAutoExtractPrompt: settings.enableAutoExtractPrompt,
 | 
			
		||||
      inputDirectory: fileManagerState.inputDirectory,
 | 
			
		||||
      outputDirectory: fileManagerState.outputDirectory,
 | 
			
		||||
      removeBGModel: serverConfig?.removeBGModel,
 | 
			
		||||
      realesrganModel: serverConfig?.realesrganModel,
 | 
			
		||||
      interactiveSegModel: serverConfig?.interactiveSegModel,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (serverConfig) {
 | 
			
		||||
      setServerConfig(serverConfig)
 | 
			
		||||
      form.setValue("removeBGModel", serverConfig.removeBGModel)
 | 
			
		||||
      form.setValue("realesrganModel", serverConfig.realesrganModel)
 | 
			
		||||
      form.setValue("interactiveSegModel", serverConfig.interactiveSegModel)
 | 
			
		||||
    }
 | 
			
		||||
  }, [form, serverConfig])
 | 
			
		||||
 | 
			
		||||
  async function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
    // Do something with the form values. ✅ This will be type-safe and validated.
 | 
			
		||||
    updateSettings({
 | 
			
		||||
      enableDownloadMask: values.enableDownloadMask,
 | 
			
		||||
      enableManualInpainting: values.enableManualInpainting,
 | 
			
		||||
      enableUploadMask: values.enableUploadMask,
 | 
			
		||||
      enableAutoExtractPrompt: values.enableAutoExtractPrompt,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // TODO: validate input/output Directory
 | 
			
		||||
    // updateFileManagerState({
 | 
			
		||||
    //   inputDirectory: values.inputDirectory,
 | 
			
		||||
    //   outputDirectory: values.outputDirectory,
 | 
			
		||||
    // })
 | 
			
		||||
 | 
			
		||||
    const shouldSwitchModel = model.name !== settings.model.name
 | 
			
		||||
    const shouldSwitchRemoveBGModel =
 | 
			
		||||
      serverConfig?.removeBGModel !== values.removeBGModel && removeBGEnabled
 | 
			
		||||
    const shouldSwitchRealesrganModel =
 | 
			
		||||
      serverConfig?.realesrganModel !== values.realesrganModel &&
 | 
			
		||||
      realesrganEnabled
 | 
			
		||||
    const shouldSwitchInteractiveModel =
 | 
			
		||||
      serverConfig?.interactiveSegModel !== values.interactiveSegModel &&
 | 
			
		||||
      interactiveSegEnabled
 | 
			
		||||
 | 
			
		||||
    const showModelSwitching =
 | 
			
		||||
      shouldSwitchModel ||
 | 
			
		||||
      shouldSwitchRemoveBGModel ||
 | 
			
		||||
      shouldSwitchRealesrganModel ||
 | 
			
		||||
      shouldSwitchInteractiveModel
 | 
			
		||||
 | 
			
		||||
    if (showModelSwitching) {
 | 
			
		||||
      const newModelSwitchingTexts: string[] = []
 | 
			
		||||
      if (shouldSwitchModel) {
 | 
			
		||||
        newModelSwitchingTexts.push(
 | 
			
		||||
          `Switching model from ${settings.model.name} to ${model.name}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      if (shouldSwitchRemoveBGModel) {
 | 
			
		||||
        newModelSwitchingTexts.push(
 | 
			
		||||
          `Switching RemoveBG model from ${serverConfig?.removeBGModel} to ${values.removeBGModel}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      if (shouldSwitchRealesrganModel) {
 | 
			
		||||
        newModelSwitchingTexts.push(
 | 
			
		||||
          `Switching RealESRGAN model from ${serverConfig?.realesrganModel} to ${values.realesrganModel}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      if (shouldSwitchInteractiveModel) {
 | 
			
		||||
        newModelSwitchingTexts.push(
 | 
			
		||||
          `Switching ${PluginName.InteractiveSeg} model from ${serverConfig?.interactiveSegModel} to ${values.interactiveSegModel}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      setModelSwitchingTexts(newModelSwitchingTexts)
 | 
			
		||||
 | 
			
		||||
      updateAppState({ disableShortCuts: true })
 | 
			
		||||
 | 
			
		||||
      if (shouldSwitchModel) {
 | 
			
		||||
        try {
 | 
			
		||||
          const newModel = await switchModel(model.name)
 | 
			
		||||
          toast({
 | 
			
		||||
            title: `Switch to ${newModel.name} success`,
 | 
			
		||||
          })
 | 
			
		||||
          setAppModel(model)
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          toast({
 | 
			
		||||
            variant: "destructive",
 | 
			
		||||
            title: `Switch to ${model.name} failed: ${error}`,
 | 
			
		||||
          })
 | 
			
		||||
          setModel(settings.model)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (shouldSwitchRemoveBGModel) {
 | 
			
		||||
        try {
 | 
			
		||||
          const res = await switchPluginModel(
 | 
			
		||||
            PluginName.RemoveBG,
 | 
			
		||||
            values.removeBGModel
 | 
			
		||||
          )
 | 
			
		||||
          if (res.status !== 200) {
 | 
			
		||||
            throw new Error(res.statusText)
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          toast({
 | 
			
		||||
            variant: "destructive",
 | 
			
		||||
            title: `Switch RemoveBG model to ${values.removeBGModel} failed: ${error}`,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (shouldSwitchRealesrganModel) {
 | 
			
		||||
        try {
 | 
			
		||||
          const res = await switchPluginModel(
 | 
			
		||||
            PluginName.RealESRGAN,
 | 
			
		||||
            values.realesrganModel
 | 
			
		||||
          )
 | 
			
		||||
          if (res.status !== 200) {
 | 
			
		||||
            throw new Error(res.statusText)
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          toast({
 | 
			
		||||
            variant: "destructive",
 | 
			
		||||
            title: `Switch RealESRGAN model to ${values.realesrganModel} failed: ${error}`,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (shouldSwitchInteractiveModel) {
 | 
			
		||||
        try {
 | 
			
		||||
          const res = await switchPluginModel(
 | 
			
		||||
            PluginName.InteractiveSeg,
 | 
			
		||||
            values.interactiveSegModel
 | 
			
		||||
          )
 | 
			
		||||
          if (res.status !== 200) {
 | 
			
		||||
            throw new Error(res.statusText)
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          toast({
 | 
			
		||||
            variant: "destructive",
 | 
			
		||||
            title: `Switch ${PluginName.InteractiveSeg} model to ${values.interactiveSegModel} failed: ${error}`,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setModelSwitchingTexts([])
 | 
			
		||||
      updateAppState({ disableShortCuts: false })
 | 
			
		||||
 | 
			
		||||
      refetch()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useHotKey(
 | 
			
		||||
    "s",
 | 
			
		||||
    () => {
 | 
			
		||||
      toggleOpen()
 | 
			
		||||
      if (open) {
 | 
			
		||||
        onSubmit(form.getValues())
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [open, form, model, serverConfig]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  if (status !== "success") {
 | 
			
		||||
    return <></>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const modelInfos = serverConfig.modelInfos
 | 
			
		||||
  const plugins = serverConfig.plugins
 | 
			
		||||
  const removeBGEnabled = plugins.some(
 | 
			
		||||
    (plugin) => plugin.name === PluginName.RemoveBG
 | 
			
		||||
  )
 | 
			
		||||
  const realesrganEnabled = plugins.some(
 | 
			
		||||
    (plugin) => plugin.name === PluginName.RealESRGAN
 | 
			
		||||
  )
 | 
			
		||||
  const interactiveSegEnabled = plugins.some(
 | 
			
		||||
    (plugin) => plugin.name === PluginName.InteractiveSeg
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  function onOpenChange(value: boolean) {
 | 
			
		||||
    toggleOpen()
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      onSubmit(form.getValues())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onModelSelect(info: ModelInfo) {
 | 
			
		||||
    setModel(info)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderModelList(model_types: string[]) {
 | 
			
		||||
    if (!modelInfos) {
 | 
			
		||||
      return <div>Please download model first</div>
 | 
			
		||||
    }
 | 
			
		||||
    return modelInfos
 | 
			
		||||
      .filter((info) => model_types.includes(info.model_type))
 | 
			
		||||
      .map((info: ModelInfo) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div
 | 
			
		||||
            key={info.name}
 | 
			
		||||
            onClick={() => onModelSelect(info)}
 | 
			
		||||
            className="px-2"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              className={cn([
 | 
			
		||||
                info.name === model.name ? "bg-muted" : "hover:bg-muted",
 | 
			
		||||
                "rounded-md px-2 py-2",
 | 
			
		||||
                "cursor-default",
 | 
			
		||||
              ])}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="text-base">{info.name}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Separator className="my-1" />
 | 
			
		||||
          </div>
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderModelSettings() {
 | 
			
		||||
    let defaultTab = MODEL_TYPE_INPAINT
 | 
			
		||||
    for (let info of modelInfos) {
 | 
			
		||||
      if (model.name === info.name) {
 | 
			
		||||
        defaultTab = info.model_type
 | 
			
		||||
        if (defaultTab === MODEL_TYPE_DIFFUSERS_SDXL) {
 | 
			
		||||
          defaultTab = MODEL_TYPE_DIFFUSERS_SD
 | 
			
		||||
        }
 | 
			
		||||
        if (defaultTab === MODEL_TYPE_DIFFUSERS_SDXL_INPAINT) {
 | 
			
		||||
          defaultTab = MODEL_TYPE_DIFFUSERS_SD_INPAINT
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-4 w-[510px]">
 | 
			
		||||
        <div className="flex flex-col gap-4 rounded-md">
 | 
			
		||||
          <div className="font-medium">Current Model</div>
 | 
			
		||||
          <div>{model.name}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Separator />
 | 
			
		||||
 | 
			
		||||
        <div className="space-y-4  rounded-md">
 | 
			
		||||
          <div className="flex gap-1 items-center justify-start">
 | 
			
		||||
            <div className="font-medium">Available models</div>
 | 
			
		||||
            {/* <IconButton tooltip="How to download new model">
 | 
			
		||||
              <Info size={20} strokeWidth={2} className="opacity-50" />
 | 
			
		||||
            </IconButton> */}
 | 
			
		||||
          </div>
 | 
			
		||||
          <Tabs defaultValue={defaultTab}>
 | 
			
		||||
            <TabsList>
 | 
			
		||||
              <TabsTrigger value={MODEL_TYPE_INPAINT}>Inpaint</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value={MODEL_TYPE_DIFFUSERS_SD}>
 | 
			
		||||
                Stable Diffusion
 | 
			
		||||
              </TabsTrigger>
 | 
			
		||||
              <TabsTrigger value={MODEL_TYPE_DIFFUSERS_SD_INPAINT}>
 | 
			
		||||
                Stable Diffusion Inpaint
 | 
			
		||||
              </TabsTrigger>
 | 
			
		||||
              <TabsTrigger value={MODEL_TYPE_OTHER}>
 | 
			
		||||
                Other Diffusion
 | 
			
		||||
              </TabsTrigger>
 | 
			
		||||
            </TabsList>
 | 
			
		||||
            <ScrollArea className="h-[240px] w-full mt-2 outline-none border rounded-lg">
 | 
			
		||||
              <TabsContent value={MODEL_TYPE_INPAINT}>
 | 
			
		||||
                {renderModelList([MODEL_TYPE_INPAINT])}
 | 
			
		||||
              </TabsContent>
 | 
			
		||||
              <TabsContent value={MODEL_TYPE_DIFFUSERS_SD}>
 | 
			
		||||
                {renderModelList([
 | 
			
		||||
                  MODEL_TYPE_DIFFUSERS_SD,
 | 
			
		||||
                  MODEL_TYPE_DIFFUSERS_SDXL,
 | 
			
		||||
                ])}
 | 
			
		||||
              </TabsContent>
 | 
			
		||||
              <TabsContent value={MODEL_TYPE_DIFFUSERS_SD_INPAINT}>
 | 
			
		||||
                {renderModelList([
 | 
			
		||||
                  MODEL_TYPE_DIFFUSERS_SD_INPAINT,
 | 
			
		||||
                  MODEL_TYPE_DIFFUSERS_SDXL_INPAINT,
 | 
			
		||||
                ])}
 | 
			
		||||
              </TabsContent>
 | 
			
		||||
              <TabsContent value={MODEL_TYPE_OTHER}>
 | 
			
		||||
                {renderModelList([MODEL_TYPE_OTHER])}
 | 
			
		||||
              </TabsContent>
 | 
			
		||||
            </ScrollArea>
 | 
			
		||||
          </Tabs>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderGeneralSettings() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="space-y-4 w-[510px]">
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="enableManualInpainting"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Enable manual inpainting</FormLabel>
 | 
			
		||||
                <FormDescription>
 | 
			
		||||
                  For erase model, click a button to trigger inpainting after
 | 
			
		||||
                  draw mask.
 | 
			
		||||
                </FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <FormControl>
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checked={field.value}
 | 
			
		||||
                  onCheckedChange={field.onChange}
 | 
			
		||||
                />
 | 
			
		||||
              </FormControl>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Separator />
 | 
			
		||||
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="enableDownloadMask"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Enable download mask</FormLabel>
 | 
			
		||||
                <FormDescription>
 | 
			
		||||
                  Also download the mask after save the inpainting result.
 | 
			
		||||
                </FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <FormControl>
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checked={field.value}
 | 
			
		||||
                  onCheckedChange={field.onChange}
 | 
			
		||||
                />
 | 
			
		||||
              </FormControl>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Separator />
 | 
			
		||||
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="enableAutoExtractPrompt"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Enable auto extract prompt</FormLabel>
 | 
			
		||||
                <FormDescription>
 | 
			
		||||
                  Automatically extract prompt/negativate prompt from the image
 | 
			
		||||
                  meta.
 | 
			
		||||
                </FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <FormControl>
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checked={field.value}
 | 
			
		||||
                  onCheckedChange={field.onChange}
 | 
			
		||||
                />
 | 
			
		||||
              </FormControl>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {/* <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="enableUploadMask"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex tems-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Enable upload mask</FormLabel>
 | 
			
		||||
                <FormDescription>
 | 
			
		||||
                  Enable upload custom mask to perform inpainting.
 | 
			
		||||
                </FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <FormControl>
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checked={field.value}
 | 
			
		||||
                  onCheckedChange={field.onChange}
 | 
			
		||||
                />
 | 
			
		||||
              </FormControl>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
        <Separator /> */}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderPluginsSettings() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="space-y-4 w-[510px]">
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="removeBGModel"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Remove Background</FormLabel>
 | 
			
		||||
                <FormDescription>Remove background model</FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Select
 | 
			
		||||
                onValueChange={field.onChange}
 | 
			
		||||
                defaultValue={field.value}
 | 
			
		||||
                disabled={!removeBGEnabled}
 | 
			
		||||
              >
 | 
			
		||||
                <FormControl>
 | 
			
		||||
                  <SelectTrigger className="w-auto">
 | 
			
		||||
                    <SelectValue placeholder="Select removebg model" />
 | 
			
		||||
                  </SelectTrigger>
 | 
			
		||||
                </FormControl>
 | 
			
		||||
                <SelectContent align="end">
 | 
			
		||||
                  <SelectGroup>
 | 
			
		||||
                    {serverConfig?.removeBGModels.map((model) => (
 | 
			
		||||
                      <SelectItem key={model} value={model}>
 | 
			
		||||
                        {model}
 | 
			
		||||
                      </SelectItem>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </SelectGroup>
 | 
			
		||||
                </SelectContent>
 | 
			
		||||
              </Select>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Separator />
 | 
			
		||||
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="realesrganModel"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>RealESRGAN</FormLabel>
 | 
			
		||||
                <FormDescription>RealESRGAN Model</FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Select
 | 
			
		||||
                onValueChange={field.onChange}
 | 
			
		||||
                defaultValue={field.value}
 | 
			
		||||
                disabled={!realesrganEnabled}
 | 
			
		||||
              >
 | 
			
		||||
                <FormControl>
 | 
			
		||||
                  <SelectTrigger className="w-auto">
 | 
			
		||||
                    <SelectValue placeholder="Select RealESRGAN model" />
 | 
			
		||||
                  </SelectTrigger>
 | 
			
		||||
                </FormControl>
 | 
			
		||||
                <SelectContent align="end">
 | 
			
		||||
                  <SelectGroup>
 | 
			
		||||
                    {serverConfig?.realesrganModels.map((model) => (
 | 
			
		||||
                      <SelectItem key={model} value={model}>
 | 
			
		||||
                        {model}
 | 
			
		||||
                      </SelectItem>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </SelectGroup>
 | 
			
		||||
                </SelectContent>
 | 
			
		||||
              </Select>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Separator />
 | 
			
		||||
 | 
			
		||||
        <FormField
 | 
			
		||||
          control={form.control}
 | 
			
		||||
          name="interactiveSegModel"
 | 
			
		||||
          render={({ field }) => (
 | 
			
		||||
            <FormItem className="flex items-center justify-between">
 | 
			
		||||
              <div className="space-y-0.5">
 | 
			
		||||
                <FormLabel>Interactive Segmentation</FormLabel>
 | 
			
		||||
                <FormDescription>
 | 
			
		||||
                  Interactive Segmentation Model
 | 
			
		||||
                </FormDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Select
 | 
			
		||||
                onValueChange={field.onChange}
 | 
			
		||||
                defaultValue={field.value}
 | 
			
		||||
                disabled={!interactiveSegEnabled}
 | 
			
		||||
              >
 | 
			
		||||
                <FormControl>
 | 
			
		||||
                  <SelectTrigger className="w-auto">
 | 
			
		||||
                    <SelectValue placeholder="Select interactive segmentation model" />
 | 
			
		||||
                  </SelectTrigger>
 | 
			
		||||
                </FormControl>
 | 
			
		||||
                <SelectContent align="end">
 | 
			
		||||
                  <SelectGroup>
 | 
			
		||||
                    {serverConfig?.interactiveSegModels.map((model) => (
 | 
			
		||||
                      <SelectItem key={model} value={model}>
 | 
			
		||||
                        {model}
 | 
			
		||||
                      </SelectItem>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </SelectGroup>
 | 
			
		||||
                </SelectContent>
 | 
			
		||||
              </Select>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  // function renderFileManagerSettings() {
 | 
			
		||||
  //   return (
 | 
			
		||||
  //     <div className="flex flex-col justify-between rounded-lg gap-4 w-[400px]">
 | 
			
		||||
  //       <FormField
 | 
			
		||||
  //         control={form.control}
 | 
			
		||||
  //         name="enableFileManager"
 | 
			
		||||
  //         render={({ field }) => (
 | 
			
		||||
  //           <FormItem className="flex items-center justify-between gap-4">
 | 
			
		||||
  //             <div className="space-y-0.5">
 | 
			
		||||
  //               <FormLabel>Enable file manger</FormLabel>
 | 
			
		||||
  //               <FormDescription className="max-w-sm">
 | 
			
		||||
  //                 Browser images
 | 
			
		||||
  //               </FormDescription>
 | 
			
		||||
  //             </div>
 | 
			
		||||
  //             <FormControl>
 | 
			
		||||
  //               <Switch
 | 
			
		||||
  //                 checked={field.value}
 | 
			
		||||
  //                 onCheckedChange={field.onChange}
 | 
			
		||||
  //               />
 | 
			
		||||
  //             </FormControl>
 | 
			
		||||
  //           </FormItem>
 | 
			
		||||
  //         )}
 | 
			
		||||
  //       />
 | 
			
		||||
 | 
			
		||||
  //       <Separator />
 | 
			
		||||
 | 
			
		||||
  //       <FormField
 | 
			
		||||
  //         control={form.control}
 | 
			
		||||
  //         name="inputDirectory"
 | 
			
		||||
  //         render={({ field }) => (
 | 
			
		||||
  //           <FormItem>
 | 
			
		||||
  //             <FormLabel>Input directory</FormLabel>
 | 
			
		||||
  //             <FormControl>
 | 
			
		||||
  //               <Input placeholder="" {...field} />
 | 
			
		||||
  //             </FormControl>
 | 
			
		||||
  //             <FormDescription>
 | 
			
		||||
  //               Browser images from this directory.
 | 
			
		||||
  //             </FormDescription>
 | 
			
		||||
  //             <FormMessage />
 | 
			
		||||
  //           </FormItem>
 | 
			
		||||
  //         )}
 | 
			
		||||
  //       />
 | 
			
		||||
 | 
			
		||||
  //       <FormField
 | 
			
		||||
  //         control={form.control}
 | 
			
		||||
  //         name="outputDirectory"
 | 
			
		||||
  //         render={({ field }) => (
 | 
			
		||||
  //           <FormItem>
 | 
			
		||||
  //             <FormLabel>Save directory</FormLabel>
 | 
			
		||||
  //             <FormControl>
 | 
			
		||||
  //               <Input placeholder="" {...field} />
 | 
			
		||||
  //             </FormControl>
 | 
			
		||||
  //             <FormDescription>
 | 
			
		||||
  //               Result images will be saved to this directory.
 | 
			
		||||
  //             </FormDescription>
 | 
			
		||||
  //             <FormMessage />
 | 
			
		||||
  //           </FormItem>
 | 
			
		||||
  //         )}
 | 
			
		||||
  //       />
 | 
			
		||||
  //     </div>
 | 
			
		||||
  //   )
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AlertDialog open={openModelSwitching}>
 | 
			
		||||
        <AlertDialogContent>
 | 
			
		||||
          <AlertDialogHeader>
 | 
			
		||||
            {/* <AlertDialogDescription> */}
 | 
			
		||||
            <div className="flex flex-col justify-center items-center gap-4">
 | 
			
		||||
              <div role="status">
 | 
			
		||||
                <svg
 | 
			
		||||
                  aria-hidden="true"
 | 
			
		||||
                  className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-primary"
 | 
			
		||||
                  viewBox="0 0 100 101"
 | 
			
		||||
                  fill="none"
 | 
			
		||||
                  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                >
 | 
			
		||||
                  <path
 | 
			
		||||
                    d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
 | 
			
		||||
                    fill="currentColor"
 | 
			
		||||
                  />
 | 
			
		||||
                  <path
 | 
			
		||||
                    d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
 | 
			
		||||
                    fill="currentFill"
 | 
			
		||||
                  />
 | 
			
		||||
                </svg>
 | 
			
		||||
                <span className="sr-only">Loading...</span>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              {modelSwitchingTexts ? (
 | 
			
		||||
                <div className="flex flex-col">
 | 
			
		||||
                  {modelSwitchingTexts.map((text, index) => (
 | 
			
		||||
                    <div key={index}>{text}</div>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </div>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
            {/* </AlertDialogDescription> */}
 | 
			
		||||
          </AlertDialogHeader>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
      <Dialog open={open} onOpenChange={onOpenChange}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
          <IconButton tooltip="Settings">
 | 
			
		||||
            <Settings />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </DialogTrigger>
 | 
			
		||||
        <DialogContent
 | 
			
		||||
          className="max-w-3xl h-[600px]"
 | 
			
		||||
          // onEscapeKeyDown={(event) => event.preventDefault()}
 | 
			
		||||
          onOpenAutoFocus={(event) => event.preventDefault()}
 | 
			
		||||
          // onPointerDownOutside={(event) => event.preventDefault()}
 | 
			
		||||
        >
 | 
			
		||||
          <DialogTitle>Settings</DialogTitle>
 | 
			
		||||
          <Separator />
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-row space-x-8 h-full">
 | 
			
		||||
            <div className="flex flex-col space-y-1">
 | 
			
		||||
              {TAB_NAMES.map((item) => (
 | 
			
		||||
                <Button
 | 
			
		||||
                  key={item}
 | 
			
		||||
                  variant="ghost"
 | 
			
		||||
                  onClick={() => setTab(item)}
 | 
			
		||||
                  className={cn(
 | 
			
		||||
                    tab === item ? "bg-muted " : "hover:bg-muted",
 | 
			
		||||
                    "justify-start"
 | 
			
		||||
                  )}
 | 
			
		||||
                >
 | 
			
		||||
                  {item}
 | 
			
		||||
                </Button>
 | 
			
		||||
              ))}
 | 
			
		||||
            </div>
 | 
			
		||||
            <Separator orientation="vertical" />
 | 
			
		||||
            <Form {...form}>
 | 
			
		||||
              <div className="flex w-full justify-center">
 | 
			
		||||
                <form onSubmit={form.handleSubmit(onSubmit)}>
 | 
			
		||||
                  {tab === TAB_MODEL ? renderModelSettings() : <></>}
 | 
			
		||||
                  {tab === TAB_GENERAL ? renderGeneralSettings() : <></>}
 | 
			
		||||
                  {tab === TAB_PLUGINS ? renderPluginsSettings() : <></>}
 | 
			
		||||
                  {/* {tab === TAB_FILE_MANAGER ? (
 | 
			
		||||
                    renderFileManagerSettings()
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <></>
 | 
			
		||||
                  )} */}
 | 
			
		||||
 | 
			
		||||
                  <div className="absolute right-10 bottom-6">
 | 
			
		||||
                    <Button onClick={() => onOpenChange(false)}>Ok</Button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </form>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Form>
 | 
			
		||||
          </div>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SettingsDialog
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,86 @@
 | 
			
		|||
import { Keyboard } from "lucide-react"
 | 
			
		||||
import { IconButton } from "@/components/ui/button"
 | 
			
		||||
import { useToggle } from "@uidotdev/usehooks"
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "./ui/dialog"
 | 
			
		||||
import useHotKey from "@/hooks/useHotkey"
 | 
			
		||||
 | 
			
		||||
interface ShortcutProps {
 | 
			
		||||
  content: string
 | 
			
		||||
  keys: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ShortCut(props: ShortcutProps) {
 | 
			
		||||
  const { content, keys } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex justify-between">
 | 
			
		||||
      <div>{content}</div>
 | 
			
		||||
      <div className="flex gap-[8px]">
 | 
			
		||||
        {keys.map((k) => (
 | 
			
		||||
          // TODO: 优化快捷键显示
 | 
			
		||||
          <div className="border px-2 py-1 rounded-lg" key={k}>
 | 
			
		||||
            {k}
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isMac = function () {
 | 
			
		||||
  return /macintosh|mac os x/i.test(navigator.userAgent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CmdOrCtrl = () => {
 | 
			
		||||
  return isMac() ? "Cmd" : "Ctrl"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Shortcuts() {
 | 
			
		||||
  const [open, toggleOpen] = useToggle(false)
 | 
			
		||||
 | 
			
		||||
  useHotKey("h", () => {
 | 
			
		||||
    toggleOpen()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={open} onOpenChange={toggleOpen}>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <IconButton tooltip="Hotkeys">
 | 
			
		||||
          <Keyboard />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogHeader>
 | 
			
		||||
          <DialogTitle>Hotkeys</DialogTitle>
 | 
			
		||||
          <div className="flex gap-2 flex-col pt-4">
 | 
			
		||||
            <ShortCut content="Pan" keys={["Space + Drag"]} />
 | 
			
		||||
            <ShortCut content="Reset Zoom/Pan" keys={["Esc"]} />
 | 
			
		||||
            <ShortCut content="Decrease Brush Size" keys={["["]} />
 | 
			
		||||
            <ShortCut content="Increase Brush Size" keys={["]"]} />
 | 
			
		||||
            <ShortCut content="View Original Image" keys={["Hold Tab"]} />
 | 
			
		||||
 | 
			
		||||
            <ShortCut content="Undo" keys={[CmdOrCtrl(), "Z"]} />
 | 
			
		||||
            <ShortCut content="Redo" keys={[CmdOrCtrl(), "Shift", "Z"]} />
 | 
			
		||||
            <ShortCut content="Copy Result" keys={[CmdOrCtrl(), "C"]} />
 | 
			
		||||
            <ShortCut content="Paste Image" keys={[CmdOrCtrl(), "V"]} />
 | 
			
		||||
            <ShortCut
 | 
			
		||||
              content="Trigger Manually Inpainting"
 | 
			
		||||
              keys={["Shift", "R"]}
 | 
			
		||||
            />
 | 
			
		||||
            <ShortCut content="Toggle Hotkeys Dialog" keys={["H"]} />
 | 
			
		||||
            <ShortCut content="Toggle Settings Dialog" keys={["S"]} />
 | 
			
		||||
            <ShortCut content="Toggle File Manager" keys={["F"]} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </DialogHeader>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Shortcuts
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { LabelTitle, RowContainer } from "./LabelTitle"
 | 
			
		||||
import { NumberInput } from "../ui/input"
 | 
			
		||||
import { Slider } from "../ui/slider"
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "../ui/select"
 | 
			
		||||
import { CV2Flag } from "@/lib/types"
 | 
			
		||||
 | 
			
		||||
const CV2Options = () => {
 | 
			
		||||
  const [settings, updateSettings] = useStore((state) => [
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col gap-4 mt-4">
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="CV2 Flag"
 | 
			
		||||
          url="https://docs.opencv.org/4.8.0/d7/d8b/group__photo__inpaint.html#gga8002a65f5a3328fbf15df81b842d3c3ca892824c38e258feb5e72f308a358d52e"
 | 
			
		||||
        />
 | 
			
		||||
        <Select
 | 
			
		||||
          value={settings.cv2Flag as string}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            const flag = value as CV2Flag
 | 
			
		||||
            updateSettings({ cv2Flag: flag })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger className="w-[160px]">
 | 
			
		||||
            <SelectValue placeholder="Select flag" />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent align="end">
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              {Object.values(CV2Flag).map((flag) => (
 | 
			
		||||
                <SelectItem key={flag as string} value={flag as string}>
 | 
			
		||||
                  {flag as string}
 | 
			
		||||
                </SelectItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
      <LabelTitle
 | 
			
		||||
        text="CV2 Radius"
 | 
			
		||||
        url="https://docs.opencv.org/4.8.0/d7/d8b/group__photo__inpaint.html#gga8002a65f5a3328fbf15df81b842d3c3ca892824c38e258feb5e72f308a358d52e"
 | 
			
		||||
      />
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <Slider
 | 
			
		||||
          className="w-[180px]"
 | 
			
		||||
          defaultValue={[5]}
 | 
			
		||||
          min={1}
 | 
			
		||||
          max={100}
 | 
			
		||||
          step={1}
 | 
			
		||||
          value={[Math.floor(settings.cv2Radius)]}
 | 
			
		||||
          onValueChange={(vals) => updateSettings({ cv2Radius: vals[0] })}
 | 
			
		||||
        />
 | 
			
		||||
        <NumberInput
 | 
			
		||||
          id="cv2-radius"
 | 
			
		||||
          className="w-[60px] rounded-full"
 | 
			
		||||
          numberValue={settings.cv2Radius}
 | 
			
		||||
          allowFloat={false}
 | 
			
		||||
          onNumberValueChange={(val) => {
 | 
			
		||||
            updateSettings({ cv2Radius: val })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CV2Options
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,894 @@
 | 
			
		|||
import { FormEvent, useRef } from "react"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { Switch } from "../ui/switch"
 | 
			
		||||
import { NumberInput } from "../ui/input"
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "../ui/select"
 | 
			
		||||
import { Textarea } from "../ui/textarea"
 | 
			
		||||
import { ExtenderDirection, PowerPaintTask } from "@/lib/types"
 | 
			
		||||
import { Separator } from "../ui/separator"
 | 
			
		||||
import { Button, ImageUploadButton } from "../ui/button"
 | 
			
		||||
import { Slider } from "../ui/slider"
 | 
			
		||||
import { useImage } from "@/hooks/useImage"
 | 
			
		||||
import {
 | 
			
		||||
  ANYTEXT,
 | 
			
		||||
  INSTRUCT_PIX2PIX,
 | 
			
		||||
  PAINT_BY_EXAMPLE,
 | 
			
		||||
  POWERPAINT,
 | 
			
		||||
} from "@/lib/const"
 | 
			
		||||
import { RowContainer, LabelTitle } from "./LabelTitle"
 | 
			
		||||
import { Upload } from "lucide-react"
 | 
			
		||||
import { useClickAway } from "react-use"
 | 
			
		||||
 | 
			
		||||
const ExtenderButton = ({
 | 
			
		||||
  text,
 | 
			
		||||
  onClick,
 | 
			
		||||
}: {
 | 
			
		||||
  text: string
 | 
			
		||||
  onClick: () => void
 | 
			
		||||
}) => {
 | 
			
		||||
  const [showExtender] = useStore((state) => [state.settings.showExtender])
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      variant="outline"
 | 
			
		||||
      className="p-1 h-8"
 | 
			
		||||
      disabled={!showExtender}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="flex items-center gap-1">{text}</div>
 | 
			
		||||
    </Button>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DiffusionOptions = () => {
 | 
			
		||||
  const [
 | 
			
		||||
    samplers,
 | 
			
		||||
    settings,
 | 
			
		||||
    paintByExampleFile,
 | 
			
		||||
    isProcessing,
 | 
			
		||||
    updateSettings,
 | 
			
		||||
    runInpainting,
 | 
			
		||||
    updateAppState,
 | 
			
		||||
    updateExtenderByBuiltIn,
 | 
			
		||||
    updateExtenderDirection,
 | 
			
		||||
    adjustMask,
 | 
			
		||||
    clearMask,
 | 
			
		||||
  ] = useStore((state) => [
 | 
			
		||||
    state.serverConfig.samplers,
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.paintByExampleFile,
 | 
			
		||||
    state.getIsProcessing(),
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
    state.runInpainting,
 | 
			
		||||
    state.updateAppState,
 | 
			
		||||
    state.updateExtenderByBuiltIn,
 | 
			
		||||
    state.updateExtenderDirection,
 | 
			
		||||
    state.adjustMask,
 | 
			
		||||
    state.clearMask,
 | 
			
		||||
  ])
 | 
			
		||||
  const [exampleImage, isExampleImageLoaded] = useImage(paintByExampleFile)
 | 
			
		||||
  const negativePromptRef = useRef(null)
 | 
			
		||||
  useClickAway<MouseEvent>(negativePromptRef, () => {
 | 
			
		||||
    if (negativePromptRef?.current) {
 | 
			
		||||
      const input = negativePromptRef.current as HTMLInputElement
 | 
			
		||||
      input.blur()
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onKeyUp = (e: React.KeyboardEvent) => {
 | 
			
		||||
    // negativePrompt 回车触发 inpainting
 | 
			
		||||
    if (e.key === "Enter" && e.ctrlKey && settings.prompt.length !== 0) {
 | 
			
		||||
      runInpainting()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderCropper = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Cropper"
 | 
			
		||||
          toolTip="Inpainting on part of image, improve inference speed and reduce memory usage."
 | 
			
		||||
        />
 | 
			
		||||
        <Switch
 | 
			
		||||
          id="cropper"
 | 
			
		||||
          checked={settings.showCropper}
 | 
			
		||||
          onCheckedChange={(value) => {
 | 
			
		||||
            updateSettings({ showCropper: value })
 | 
			
		||||
            if (value) {
 | 
			
		||||
              updateSettings({ showExtender: false })
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderConterNetSetting = () => {
 | 
			
		||||
    if (!settings.model.support_controlnet) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-4">
 | 
			
		||||
        <div className="flex flex-col gap-4">
 | 
			
		||||
          <div className="flex justify-between items-center pr-2">
 | 
			
		||||
            <LabelTitle
 | 
			
		||||
              text="ControlNet"
 | 
			
		||||
              toolTip="Using an additional conditioning image to control how an image is generated"
 | 
			
		||||
              url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#controlnet"
 | 
			
		||||
            />
 | 
			
		||||
            <Switch
 | 
			
		||||
              id="controlnet"
 | 
			
		||||
              checked={settings.enableControlnet}
 | 
			
		||||
              onCheckedChange={(value) => {
 | 
			
		||||
                updateSettings({ enableControlnet: value })
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-col gap-1">
 | 
			
		||||
            <RowContainer>
 | 
			
		||||
              <Slider
 | 
			
		||||
                className="w-[180px]"
 | 
			
		||||
                defaultValue={[100]}
 | 
			
		||||
                min={1}
 | 
			
		||||
                max={100}
 | 
			
		||||
                step={1}
 | 
			
		||||
                disabled={!settings.enableControlnet}
 | 
			
		||||
                value={[Math.floor(settings.controlnetConditioningScale * 100)]}
 | 
			
		||||
                onValueChange={(vals) =>
 | 
			
		||||
                  updateSettings({ controlnetConditioningScale: vals[0] / 100 })
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <NumberInput
 | 
			
		||||
                id="controlnet-weight"
 | 
			
		||||
                className="w-[60px] rounded-full"
 | 
			
		||||
                disabled={!settings.enableControlnet}
 | 
			
		||||
                numberValue={settings.controlnetConditioningScale}
 | 
			
		||||
                allowFloat={false}
 | 
			
		||||
                onNumberValueChange={(val) => {
 | 
			
		||||
                  updateSettings({ controlnetConditioningScale: val })
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </RowContainer>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className="pr-2">
 | 
			
		||||
            <Select
 | 
			
		||||
              defaultValue={settings.controlnetMethod}
 | 
			
		||||
              value={settings.controlnetMethod}
 | 
			
		||||
              onValueChange={(value) => {
 | 
			
		||||
                updateSettings({ controlnetMethod: value })
 | 
			
		||||
              }}
 | 
			
		||||
              disabled={!settings.enableControlnet}
 | 
			
		||||
            >
 | 
			
		||||
              <SelectTrigger>
 | 
			
		||||
                <SelectValue placeholder="Select control method" />
 | 
			
		||||
              </SelectTrigger>
 | 
			
		||||
              <SelectContent align="end">
 | 
			
		||||
                <SelectGroup>
 | 
			
		||||
                  {Object.values(settings.model.controlnets).map((method) => (
 | 
			
		||||
                    <SelectItem key={method} value={method}>
 | 
			
		||||
                      {method.split("/")[1]}
 | 
			
		||||
                    </SelectItem>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </SelectGroup>
 | 
			
		||||
              </SelectContent>
 | 
			
		||||
            </Select>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderLCMLora = () => {
 | 
			
		||||
    if (!settings.model.support_lcm_lora) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <LabelTitle
 | 
			
		||||
            text="LCM LoRA"
 | 
			
		||||
            url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inference_with_lcm_lora"
 | 
			
		||||
            toolTip="Enable quality image generation in typically 2-4 steps. Suggest disabling guidance_scale by setting it to 0. You can also try values between 1.0 and 2.0. When LCM Lora is enabled, LCMSampler will be used automatically."
 | 
			
		||||
          />
 | 
			
		||||
          <Switch
 | 
			
		||||
            id="lcm-lora"
 | 
			
		||||
            checked={settings.enableLCMLora}
 | 
			
		||||
            onCheckedChange={(value) => {
 | 
			
		||||
              updateSettings({ enableLCMLora: value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderFreeu = () => {
 | 
			
		||||
    if (!settings.model.support_freeu) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-4">
 | 
			
		||||
        <div className="flex justify-between items-center pr-2">
 | 
			
		||||
          <LabelTitle
 | 
			
		||||
            text="FreeU"
 | 
			
		||||
            toolTip="FreeU is a technique for improving image quality. Different models may require different FreeU-specific hyperparameters, which can be viewed in the more info section."
 | 
			
		||||
            url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu"
 | 
			
		||||
          />
 | 
			
		||||
          <Switch
 | 
			
		||||
            id="freeu"
 | 
			
		||||
            checked={settings.enableFreeu}
 | 
			
		||||
            onCheckedChange={(value) => {
 | 
			
		||||
              updateSettings({ enableFreeu: value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="flex flex-col gap-4">
 | 
			
		||||
          <div className="flex justify-center gap-6">
 | 
			
		||||
            <div className="flex gap-2 items-center justify-center">
 | 
			
		||||
              <LabelTitle
 | 
			
		||||
                htmlFor="freeu-s1"
 | 
			
		||||
                text="s1"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
              />
 | 
			
		||||
              <NumberInput
 | 
			
		||||
                id="freeu-s1"
 | 
			
		||||
                className="w-14"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
                numberValue={settings.freeuConfig.s1}
 | 
			
		||||
                allowFloat
 | 
			
		||||
                onNumberValueChange={(value) => {
 | 
			
		||||
                  updateSettings({
 | 
			
		||||
                    freeuConfig: { ...settings.freeuConfig, s1: value },
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex gap-2 items-center justify-center">
 | 
			
		||||
              <LabelTitle
 | 
			
		||||
                htmlFor="freeu-s2"
 | 
			
		||||
                text="s2"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
              />
 | 
			
		||||
              <NumberInput
 | 
			
		||||
                id="freeu-s2"
 | 
			
		||||
                className="w-14"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
                numberValue={settings.freeuConfig.s2}
 | 
			
		||||
                allowFloat
 | 
			
		||||
                onNumberValueChange={(value) => {
 | 
			
		||||
                  updateSettings({
 | 
			
		||||
                    freeuConfig: { ...settings.freeuConfig, s2: value },
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className="flex justify-center gap-6">
 | 
			
		||||
            <div className="flex gap-2 items-center justify-center">
 | 
			
		||||
              <LabelTitle
 | 
			
		||||
                htmlFor="freeu-b1"
 | 
			
		||||
                text="b1"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
              />
 | 
			
		||||
              <NumberInput
 | 
			
		||||
                id="freeu-b1"
 | 
			
		||||
                className="w-14"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
                numberValue={settings.freeuConfig.b1}
 | 
			
		||||
                allowFloat
 | 
			
		||||
                onNumberValueChange={(value) => {
 | 
			
		||||
                  updateSettings({
 | 
			
		||||
                    freeuConfig: { ...settings.freeuConfig, b1: value },
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex gap-2 items-center justify-center">
 | 
			
		||||
              <LabelTitle
 | 
			
		||||
                htmlFor="freeu-b2"
 | 
			
		||||
                text="b2"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
              />
 | 
			
		||||
              <NumberInput
 | 
			
		||||
                id="freeu-b2"
 | 
			
		||||
                className="w-14"
 | 
			
		||||
                disabled={!settings.enableFreeu}
 | 
			
		||||
                numberValue={settings.freeuConfig.b2}
 | 
			
		||||
                allowFloat
 | 
			
		||||
                onNumberValueChange={(value) => {
 | 
			
		||||
                  updateSettings({
 | 
			
		||||
                    freeuConfig: { ...settings.freeuConfig, b2: value },
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderNegativePrompt = () => {
 | 
			
		||||
    if (!settings.model.need_prompt) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-4">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Negative prompt"
 | 
			
		||||
          url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#negative-prompt"
 | 
			
		||||
          toolTip="Negative prompt guides the model away from generating certain things in an image"
 | 
			
		||||
        />
 | 
			
		||||
        <div className="pl-2 pr-4">
 | 
			
		||||
          <Textarea
 | 
			
		||||
            ref={negativePromptRef}
 | 
			
		||||
            rows={4}
 | 
			
		||||
            onKeyUp={onKeyUp}
 | 
			
		||||
            className="max-h-[8rem] overflow-y-auto mb-2"
 | 
			
		||||
            placeholder=""
 | 
			
		||||
            id="negative-prompt"
 | 
			
		||||
            value={settings.negativePrompt}
 | 
			
		||||
            onInput={(evt: FormEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
              evt.preventDefault()
 | 
			
		||||
              evt.stopPropagation()
 | 
			
		||||
              const target = evt.target as HTMLTextAreaElement
 | 
			
		||||
              updateSettings({ negativePrompt: target.value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderPaintByExample = () => {
 | 
			
		||||
    if (settings.model.name !== PAINT_BY_EXAMPLE) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <LabelTitle
 | 
			
		||||
            text="Example Image"
 | 
			
		||||
            toolTip="An example image to guide image generation."
 | 
			
		||||
          />
 | 
			
		||||
          <ImageUploadButton
 | 
			
		||||
            tooltip="Upload example image"
 | 
			
		||||
            onFileUpload={(file) => {
 | 
			
		||||
              updateAppState({ paintByExampleFile: file })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Upload />
 | 
			
		||||
          </ImageUploadButton>
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
        {isExampleImageLoaded ? (
 | 
			
		||||
          <div className="flex justify-center items-center">
 | 
			
		||||
            <img
 | 
			
		||||
              src={exampleImage.src}
 | 
			
		||||
              alt="example"
 | 
			
		||||
              className="max-w-[200px] max-h-[200px] m-3"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="default"
 | 
			
		||||
          className="w-full"
 | 
			
		||||
          disabled={isProcessing || !isExampleImageLoaded}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            runInpainting()
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Paint
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderP2PImageGuidanceScale = () => {
 | 
			
		||||
    if (settings.model.name !== INSTRUCT_PIX2PIX) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Image guidance scale"
 | 
			
		||||
          toolTip="Push the generated image towards the inital image. Higher image guidance scale encourages generated images that are closely linked to the source image, usually at the expense of lower image quality."
 | 
			
		||||
          url="https://huggingface.co/docs/diffusers/main/en/api/pipelines/pix2pix"
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[150]}
 | 
			
		||||
            min={100}
 | 
			
		||||
            max={1000}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.p2pImageGuidanceScale * 100)]}
 | 
			
		||||
            onValueChange={(vals) =>
 | 
			
		||||
              updateSettings({ p2pImageGuidanceScale: vals[0] / 100 })
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="image-guidance-scale"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.p2pImageGuidanceScale}
 | 
			
		||||
            allowFloat
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ p2pImageGuidanceScale: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderStrength = () => {
 | 
			
		||||
    if (!settings.model.support_strength) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Strength"
 | 
			
		||||
          url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#strength"
 | 
			
		||||
          toolTip="Strength is a measure of how much noise is added to the base image, which influences how similar the output is to the base image. Higher value means more noise and more different from the base image"
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[100]}
 | 
			
		||||
            min={10}
 | 
			
		||||
            max={100}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.sdStrength * 100)]}
 | 
			
		||||
            onValueChange={(vals) =>
 | 
			
		||||
              updateSettings({ sdStrength: vals[0] / 100 })
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="strength"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.sdStrength}
 | 
			
		||||
            allowFloat
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ sdStrength: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderExtender = () => {
 | 
			
		||||
    if (!settings.model.support_outpainting) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className="flex flex-col gap-4">
 | 
			
		||||
          <RowContainer>
 | 
			
		||||
            <LabelTitle
 | 
			
		||||
              text="Extender"
 | 
			
		||||
              toolTip="Perform outpainting on images to expand it's content."
 | 
			
		||||
            />
 | 
			
		||||
            <Switch
 | 
			
		||||
              id="extender"
 | 
			
		||||
              checked={settings.showExtender}
 | 
			
		||||
              onCheckedChange={(value) => {
 | 
			
		||||
                updateSettings({ showExtender: value })
 | 
			
		||||
                if (value) {
 | 
			
		||||
                  updateSettings({ showCropper: false })
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </RowContainer>
 | 
			
		||||
 | 
			
		||||
          <RowContainer>
 | 
			
		||||
            <Select
 | 
			
		||||
              defaultValue={settings.extenderDirection}
 | 
			
		||||
              value={settings.extenderDirection}
 | 
			
		||||
              onValueChange={(value) => {
 | 
			
		||||
                updateExtenderDirection(value as ExtenderDirection)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <SelectTrigger
 | 
			
		||||
                className="w-[65px] h-7"
 | 
			
		||||
                disabled={!settings.showExtender}
 | 
			
		||||
              >
 | 
			
		||||
                <SelectValue placeholder="Select axis" />
 | 
			
		||||
              </SelectTrigger>
 | 
			
		||||
              <SelectContent align="end">
 | 
			
		||||
                <SelectGroup>
 | 
			
		||||
                  {Object.values(ExtenderDirection).map((v) => (
 | 
			
		||||
                    <SelectItem key={v} value={v}>
 | 
			
		||||
                      {v}
 | 
			
		||||
                    </SelectItem>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </SelectGroup>
 | 
			
		||||
              </SelectContent>
 | 
			
		||||
            </Select>
 | 
			
		||||
 | 
			
		||||
            <div className="flex gap-1 justify-center mt-0">
 | 
			
		||||
              <ExtenderButton
 | 
			
		||||
                text="1.25x"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  updateExtenderByBuiltIn(settings.extenderDirection, 1.25)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <ExtenderButton
 | 
			
		||||
                text="1.5x"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  updateExtenderByBuiltIn(settings.extenderDirection, 1.5)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <ExtenderButton
 | 
			
		||||
                text="1.75x"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  updateExtenderByBuiltIn(settings.extenderDirection, 1.75)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <ExtenderButton
 | 
			
		||||
                text="2.0x"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  updateExtenderByBuiltIn(settings.extenderDirection, 2.0)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </RowContainer>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderPowerPaintTaskType = () => {
 | 
			
		||||
    if (settings.model.name !== POWERPAINT) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Task"
 | 
			
		||||
          toolTip="PowerPaint task. When using extender, image-outpainting task will be auto used. For object-removal and image-outpainting, it is recommended to set the guidance_scale at 10 or above."
 | 
			
		||||
        />
 | 
			
		||||
        <Select
 | 
			
		||||
          defaultValue={settings.powerpaintTask}
 | 
			
		||||
          value={settings.powerpaintTask}
 | 
			
		||||
          onValueChange={(value: PowerPaintTask) => {
 | 
			
		||||
            updateSettings({ powerpaintTask: value })
 | 
			
		||||
          }}
 | 
			
		||||
          disabled={settings.showExtender}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger className="w-[140px]">
 | 
			
		||||
            <SelectValue placeholder="Select task" />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent align="end">
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              {[
 | 
			
		||||
                PowerPaintTask.text_guided,
 | 
			
		||||
                PowerPaintTask.object_remove,
 | 
			
		||||
                PowerPaintTask.shape_guided,
 | 
			
		||||
              ].map((task) => (
 | 
			
		||||
                <SelectItem key={task} value={task}>
 | 
			
		||||
                  {task}
 | 
			
		||||
                </SelectItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderSteps = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          htmlFor="steps"
 | 
			
		||||
          text="Steps"
 | 
			
		||||
          toolTip="The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference."
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[30]}
 | 
			
		||||
            min={1}
 | 
			
		||||
            max={100}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.sdSteps)]}
 | 
			
		||||
            onValueChange={(vals) => updateSettings({ sdSteps: vals[0] })}
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="steps"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.sdSteps}
 | 
			
		||||
            allowFloat={false}
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ sdSteps: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderGuidanceScale = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Guidance scale"
 | 
			
		||||
          url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#guidance-scale"
 | 
			
		||||
          toolTip="Guidance scale affects how aligned the text prompt and generated image are. Higher value means the prompt and generated image are closely aligned, so the output is a stricter interpretation of the prompt"
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[750]}
 | 
			
		||||
            min={0}
 | 
			
		||||
            max={1500}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.sdGuidanceScale * 100)]}
 | 
			
		||||
            onValueChange={(vals) =>
 | 
			
		||||
              updateSettings({ sdGuidanceScale: vals[0] / 100 })
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="guidance-scale"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.sdGuidanceScale}
 | 
			
		||||
            allowFloat
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ sdGuidanceScale: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderSampler = () => {
 | 
			
		||||
    if (settings.model.name === ANYTEXT) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <LabelTitle text="Sampler" />
 | 
			
		||||
        <Select
 | 
			
		||||
          defaultValue={settings.sdSampler}
 | 
			
		||||
          value={settings.sdSampler}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            updateSettings({ sdSampler: value })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger className="w-[175px] text-xs">
 | 
			
		||||
            <SelectValue placeholder="Select sampler" />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent align="end">
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              {samplers.map((sampler) => (
 | 
			
		||||
                <SelectItem key={sampler} value={sampler} className="text-xs">
 | 
			
		||||
                  {sampler}
 | 
			
		||||
                </SelectItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderSeed = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        {/* 每次会从服务器返回更新该值 */}
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Seed"
 | 
			
		||||
          toolTip="Using same parameters and a fixed seed can generate same result image."
 | 
			
		||||
        />
 | 
			
		||||
        {/* <Pin /> */}
 | 
			
		||||
        <div className="flex gap-2 justify-center items-center">
 | 
			
		||||
          <Switch
 | 
			
		||||
            id="seed"
 | 
			
		||||
            checked={settings.seedFixed}
 | 
			
		||||
            onCheckedChange={(value) => {
 | 
			
		||||
              updateSettings({ seedFixed: value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="seed"
 | 
			
		||||
            className="w-[100px]"
 | 
			
		||||
            disabled={!settings.seedFixed}
 | 
			
		||||
            numberValue={settings.seed}
 | 
			
		||||
            allowFloat={false}
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ seed: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderMaskBlur = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          text="Mask blur"
 | 
			
		||||
          toolTip="How much to blur the mask before processing, in pixels. Make the generated inpainting boundaries appear more natural."
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[settings.sdMaskBlur]}
 | 
			
		||||
            min={0}
 | 
			
		||||
            max={96}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.sdMaskBlur)]}
 | 
			
		||||
            onValueChange={(vals) => updateSettings({ sdMaskBlur: vals[0] })}
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="mask-blur"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.sdMaskBlur}
 | 
			
		||||
            allowFloat={false}
 | 
			
		||||
            onNumberValueChange={(value) => {
 | 
			
		||||
              updateSettings({ sdMaskBlur: value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderMatchHistograms = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <LabelTitle
 | 
			
		||||
            text="Match histograms"
 | 
			
		||||
            toolTip="Match the inpainting result histogram to the source image histogram"
 | 
			
		||||
            url="https://github.com/Sanster/lama-cleaner/pull/143#issuecomment-1325859307"
 | 
			
		||||
          />
 | 
			
		||||
          <Switch
 | 
			
		||||
            id="match-histograms"
 | 
			
		||||
            checked={settings.sdMatchHistograms}
 | 
			
		||||
            onCheckedChange={(value) => {
 | 
			
		||||
              updateSettings({ sdMatchHistograms: value })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderMaskAdjuster = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className="flex flex-col gap-1">
 | 
			
		||||
          <LabelTitle
 | 
			
		||||
            htmlFor="adjustMaskKernelSize"
 | 
			
		||||
            text="Adjust Mask"
 | 
			
		||||
            toolTip="Expand or shrink mask. Using the slider to adjust the kernel size for dilation or erosion."
 | 
			
		||||
          />
 | 
			
		||||
          <RowContainer>
 | 
			
		||||
            <Slider
 | 
			
		||||
              className="w-[180px]"
 | 
			
		||||
              defaultValue={[12]}
 | 
			
		||||
              min={1}
 | 
			
		||||
              max={100}
 | 
			
		||||
              step={1}
 | 
			
		||||
              value={[Math.floor(settings.adjustMaskKernelSize)]}
 | 
			
		||||
              onValueChange={(vals) =>
 | 
			
		||||
                updateSettings({ adjustMaskKernelSize: vals[0] })
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
            <NumberInput
 | 
			
		||||
              id="adjustMaskKernelSize"
 | 
			
		||||
              className="w-[60px] rounded-full"
 | 
			
		||||
              numberValue={settings.adjustMaskKernelSize}
 | 
			
		||||
              allowFloat={false}
 | 
			
		||||
              onNumberValueChange={(val) => {
 | 
			
		||||
                updateSettings({ adjustMaskKernelSize: val })
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </RowContainer>
 | 
			
		||||
 | 
			
		||||
          <RowContainer>
 | 
			
		||||
            <div className="flex gap-1 justify-start">
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="outline"
 | 
			
		||||
                className="p-1 h-8"
 | 
			
		||||
                onClick={() => adjustMask("expand")}
 | 
			
		||||
                disabled={isProcessing}
 | 
			
		||||
              >
 | 
			
		||||
                <div className="flex items-center gap-1 select-none">
 | 
			
		||||
                  {/* <Plus size={16} /> */}
 | 
			
		||||
                  Expand
 | 
			
		||||
                </div>
 | 
			
		||||
              </Button>
 | 
			
		||||
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="outline"
 | 
			
		||||
                className="p-1 h-8"
 | 
			
		||||
                onClick={() => adjustMask("shrink")}
 | 
			
		||||
                disabled={isProcessing}
 | 
			
		||||
              >
 | 
			
		||||
                <div className="flex items-center gap-1 select-none">
 | 
			
		||||
                  {/* <Minus size={16} /> */}
 | 
			
		||||
                  Shrink
 | 
			
		||||
                </div>
 | 
			
		||||
              </Button>
 | 
			
		||||
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="outline"
 | 
			
		||||
                className="p-1 h-8"
 | 
			
		||||
                onClick={() => adjustMask("reverse")}
 | 
			
		||||
                disabled={isProcessing}
 | 
			
		||||
              >
 | 
			
		||||
                <div className="flex items-center gap-1 select-none">
 | 
			
		||||
                  Reverse
 | 
			
		||||
                </div>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="outline"
 | 
			
		||||
              className="p-1 h-8 justify-self-end"
 | 
			
		||||
              onClick={clearMask}
 | 
			
		||||
              disabled={isProcessing}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="flex items-center gap-1 select-none">Clear</div>
 | 
			
		||||
            </Button>
 | 
			
		||||
          </RowContainer>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Separator />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col gap-4 mt-4">
 | 
			
		||||
      {renderCropper()}
 | 
			
		||||
      {renderExtender()}
 | 
			
		||||
      {renderMaskAdjuster()}
 | 
			
		||||
      {renderPowerPaintTaskType()}
 | 
			
		||||
      {renderSteps()}
 | 
			
		||||
      {renderGuidanceScale()}
 | 
			
		||||
      {renderP2PImageGuidanceScale()}
 | 
			
		||||
      {renderStrength()}
 | 
			
		||||
      {renderSampler()}
 | 
			
		||||
      {renderSeed()}
 | 
			
		||||
      {renderNegativePrompt()}
 | 
			
		||||
      <Separator />
 | 
			
		||||
      {renderConterNetSetting()}
 | 
			
		||||
      {renderLCMLora()}
 | 
			
		||||
      {renderMaskBlur()}
 | 
			
		||||
      {renderMatchHistograms()}
 | 
			
		||||
      {renderFreeu()}
 | 
			
		||||
      {renderPaintByExample()}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DiffusionOptions
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { LabelTitle, RowContainer } from "./LabelTitle"
 | 
			
		||||
import { NumberInput } from "../ui/input"
 | 
			
		||||
import { Slider } from "../ui/slider"
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "../ui/select"
 | 
			
		||||
import { LDMSampler } from "@/lib/types"
 | 
			
		||||
 | 
			
		||||
const LDMOptions = () => {
 | 
			
		||||
  const [settings, updateSettings] = useStore((state) => [
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col gap-4 mt-4">
 | 
			
		||||
      <div className="flex flex-col gap-1">
 | 
			
		||||
        <LabelTitle
 | 
			
		||||
          htmlFor="steps"
 | 
			
		||||
          text="Steps"
 | 
			
		||||
          toolTip="The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference."
 | 
			
		||||
        />
 | 
			
		||||
        <RowContainer>
 | 
			
		||||
          <Slider
 | 
			
		||||
            className="w-[180px]"
 | 
			
		||||
            defaultValue={[30]}
 | 
			
		||||
            min={1}
 | 
			
		||||
            max={100}
 | 
			
		||||
            step={1}
 | 
			
		||||
            value={[Math.floor(settings.ldmSteps)]}
 | 
			
		||||
            onValueChange={(vals) => updateSettings({ ldmSteps: vals[0] })}
 | 
			
		||||
          />
 | 
			
		||||
          <NumberInput
 | 
			
		||||
            id="steps"
 | 
			
		||||
            className="w-[60px] rounded-full"
 | 
			
		||||
            numberValue={settings.ldmSteps}
 | 
			
		||||
            allowFloat={false}
 | 
			
		||||
            onNumberValueChange={(val) => {
 | 
			
		||||
              updateSettings({ ldmSteps: val })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </RowContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
      <RowContainer>
 | 
			
		||||
        <LabelTitle text="Sampler" />
 | 
			
		||||
        <Select
 | 
			
		||||
          value={settings.ldmSampler as string}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            const sampler = value as LDMSampler
 | 
			
		||||
            updateSettings({ ldmSampler: sampler })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger className="w-[100px]">
 | 
			
		||||
            <SelectValue placeholder="Select sampler" />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent align="end">
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              {Object.values(LDMSampler).map((sampler) => (
 | 
			
		||||
                <SelectItem key={sampler as string} value={sampler as string}>
 | 
			
		||||
                  {sampler as string}
 | 
			
		||||
                </SelectItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </RowContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default LDMOptions
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
import { Button } from "../ui/button"
 | 
			
		||||
import { Label } from "../ui/label"
 | 
			
		||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
 | 
			
		||||
 | 
			
		||||
const RowContainer = ({ children }: { children: React.ReactNode }) => (
 | 
			
		||||
  <div className="flex justify-between items-center pr-2">{children}</div>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const LabelTitle = ({
 | 
			
		||||
  text,
 | 
			
		||||
  toolTip = "",
 | 
			
		||||
  url,
 | 
			
		||||
  htmlFor,
 | 
			
		||||
  disabled = false,
 | 
			
		||||
}: {
 | 
			
		||||
  text: string
 | 
			
		||||
  toolTip?: string
 | 
			
		||||
  url?: string
 | 
			
		||||
  htmlFor?: string
 | 
			
		||||
  disabled?: boolean
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip>
 | 
			
		||||
      <TooltipTrigger asChild>
 | 
			
		||||
        <Label
 | 
			
		||||
          htmlFor={htmlFor ? htmlFor : text.toLowerCase().replace(" ", "-")}
 | 
			
		||||
          className="font-medium"
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
        >
 | 
			
		||||
          {text}
 | 
			
		||||
        </Label>
 | 
			
		||||
      </TooltipTrigger>
 | 
			
		||||
      {toolTip || url ? (
 | 
			
		||||
        <TooltipContent className="flex flex-col max-w-xs text-sm" side="left">
 | 
			
		||||
          <p>{toolTip}</p>
 | 
			
		||||
          {url ? (
 | 
			
		||||
            <Button variant="link" className="justify-end">
 | 
			
		||||
              <a href={url} target="_blank">
 | 
			
		||||
                More info
 | 
			
		||||
              </a>
 | 
			
		||||
            </Button>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
        </TooltipContent>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { LabelTitle, RowContainer }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
import { useToggle } from "react-use"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { Separator } from "../ui/separator"
 | 
			
		||||
import { ScrollArea } from "../ui/scroll-area"
 | 
			
		||||
import { Sheet, SheetContent, SheetHeader, SheetTrigger } from "../ui/sheet"
 | 
			
		||||
import { ChevronLeft, ChevronRight } from "lucide-react"
 | 
			
		||||
import { Button } from "../ui/button"
 | 
			
		||||
import useHotKey from "@/hooks/useHotkey"
 | 
			
		||||
import { RowContainer } from "./LabelTitle"
 | 
			
		||||
import { CV2, LDM, MODEL_TYPE_INPAINT } from "@/lib/const"
 | 
			
		||||
import LDMOptions from "./LDMOptions"
 | 
			
		||||
import DiffusionOptions from "./DiffusionOptions"
 | 
			
		||||
import CV2Options from "./CV2Options"
 | 
			
		||||
 | 
			
		||||
const SidePanel = () => {
 | 
			
		||||
  const [settings, windowSize] = useStore((state) => [
 | 
			
		||||
    state.settings,
 | 
			
		||||
    state.windowSize,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const [open, toggleOpen] = useToggle(true)
 | 
			
		||||
 | 
			
		||||
  useHotKey("c", () => {
 | 
			
		||||
    toggleOpen()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    settings.model.name !== LDM &&
 | 
			
		||||
    settings.model.name !== CV2 &&
 | 
			
		||||
    settings.model.model_type === MODEL_TYPE_INPAINT
 | 
			
		||||
  ) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderSidePanelOptions = () => {
 | 
			
		||||
    if (settings.model.name === LDM) {
 | 
			
		||||
      return <LDMOptions />
 | 
			
		||||
    }
 | 
			
		||||
    if (settings.model.name === CV2) {
 | 
			
		||||
      return <CV2Options />
 | 
			
		||||
    }
 | 
			
		||||
    return <DiffusionOptions />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Sheet open={open} modal={false}>
 | 
			
		||||
      <SheetTrigger
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        className="z-10 outline-none absolute top-[68px] right-6 rounded-lg border bg-background"
 | 
			
		||||
        hidden={open}
 | 
			
		||||
      >
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="ghost"
 | 
			
		||||
          size="icon"
 | 
			
		||||
          asChild
 | 
			
		||||
          className="p-1.5"
 | 
			
		||||
          onClick={toggleOpen}
 | 
			
		||||
        >
 | 
			
		||||
          <ChevronLeft strokeWidth={1} />
 | 
			
		||||
        </Button>
 | 
			
		||||
      </SheetTrigger>
 | 
			
		||||
      <SheetContent
 | 
			
		||||
        side="right"
 | 
			
		||||
        className="w-[300px] mt-[60px] outline-none pl-4 pr-1"
 | 
			
		||||
        onOpenAutoFocus={(event) => event.preventDefault()}
 | 
			
		||||
        onPointerDownOutside={(event) => event.preventDefault()}
 | 
			
		||||
      >
 | 
			
		||||
        <SheetHeader>
 | 
			
		||||
          <RowContainer>
 | 
			
		||||
            <div className="overflow-hidden mr-8">
 | 
			
		||||
              {
 | 
			
		||||
                settings.model.name.split("/")[
 | 
			
		||||
                  settings.model.name.split("/").length - 1
 | 
			
		||||
                ]
 | 
			
		||||
              }
 | 
			
		||||
            </div>
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="ghost"
 | 
			
		||||
              size="icon"
 | 
			
		||||
              className="border h-6 w-6"
 | 
			
		||||
              onClick={toggleOpen}
 | 
			
		||||
            >
 | 
			
		||||
              <ChevronRight strokeWidth={1} />
 | 
			
		||||
            </Button>
 | 
			
		||||
          </RowContainer>
 | 
			
		||||
          <Separator />
 | 
			
		||||
        </SheetHeader>
 | 
			
		||||
        <ScrollArea
 | 
			
		||||
          style={{ height: windowSize.height - 160 }}
 | 
			
		||||
          className="pr-3"
 | 
			
		||||
        >
 | 
			
		||||
          {renderSidePanelOptions()}
 | 
			
		||||
        </ScrollArea>
 | 
			
		||||
      </SheetContent>
 | 
			
		||||
    </Sheet>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SidePanel
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
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"
 | 
			
		||||
 | 
			
		||||
const Workspace = () => {
 | 
			
		||||
  const [file, updateSettings] = useStore((state) => [
 | 
			
		||||
    state.file,
 | 
			
		||||
    state.updateSettings,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchCurrentModel = async () => {
 | 
			
		||||
      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
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
 | 
			
		||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Accordion = AccordionPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const AccordionItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("border-b", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AccordionItem.displayName = "AccordionItem"
 | 
			
		||||
 | 
			
		||||
const AccordionTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Header className="flex">
 | 
			
		||||
    <AccordionPrimitive.Trigger
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "outline-none flex flex-1 items-center justify-between py-3 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
 | 
			
		||||
    </AccordionPrimitive.Trigger>
 | 
			
		||||
  </AccordionPrimitive.Header>
 | 
			
		||||
))
 | 
			
		||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
 | 
			
		||||
 | 
			
		||||
const AccordionContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <div className={cn("pb-4 pt-0", className)}>{children}</div>
 | 
			
		||||
  </AccordionPrimitive.Content>
 | 
			
		||||
))
 | 
			
		||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,139 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { buttonVariants } from "@/components/ui/button"
 | 
			
		||||
 | 
			
		||||
const AlertDialog = AlertDialogPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
 | 
			
		||||
 | 
			
		||||
const AlertDialogOverlay = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPrimitive.Overlay
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
    ref={ref}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
 | 
			
		||||
 | 
			
		||||
const AlertDialogContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPortal>
 | 
			
		||||
    <AlertDialogOverlay />
 | 
			
		||||
    <AlertDialogPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "fixed left-[50%] top-[50%] z-50 grid 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
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </AlertDialogPortal>
 | 
			
		||||
))
 | 
			
		||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
const AlertDialogHeader = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col space-y-2 text-center sm:text-left",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
AlertDialogHeader.displayName = "AlertDialogHeader"
 | 
			
		||||
 | 
			
		||||
const AlertDialogFooter = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
AlertDialogFooter.displayName = "AlertDialogFooter"
 | 
			
		||||
 | 
			
		||||
const AlertDialogTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPrimitive.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-lg font-semibold", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
 | 
			
		||||
 | 
			
		||||
const AlertDialogDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPrimitive.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AlertDialogDescription.displayName =
 | 
			
		||||
  AlertDialogPrimitive.Description.displayName
 | 
			
		||||
 | 
			
		||||
const AlertDialogAction = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPrimitive.Action
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(buttonVariants(), className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
 | 
			
		||||
 | 
			
		||||
const AlertDialogCancel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AlertDialogPrimitive.Cancel
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      buttonVariants({ variant: "outline" }),
 | 
			
		||||
      "mt-2 sm:mt-0",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogPortal,
 | 
			
		||||
  AlertDialogOverlay,
 | 
			
		||||
  AlertDialogTrigger,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogHeader,
 | 
			
		||||
  AlertDialogFooter,
 | 
			
		||||
  AlertDialogTitle,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,128 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { Input } from "./input"
 | 
			
		||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default:
 | 
			
		||||
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
 | 
			
		||||
        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-4 py-2",
 | 
			
		||||
        sm: "h-8 rounded-md px-3 text-xs",
 | 
			
		||||
        lg: "h-10 rounded-md px-8",
 | 
			
		||||
        icon: "h-9 w-9",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export interface ButtonProps
 | 
			
		||||
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
    VariantProps<typeof buttonVariants> {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
 | 
			
		||||
  ({ className, variant, size, asChild = false, ...props }, ref) => {
 | 
			
		||||
    const Comp = asChild ? Slot : "button"
 | 
			
		||||
    return (
 | 
			
		||||
      <Comp
 | 
			
		||||
        className={cn(
 | 
			
		||||
          buttonVariants({ variant, size, className }),
 | 
			
		||||
          "outline-none cursor-default select-none"
 | 
			
		||||
        )}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Button.displayName = "Button"
 | 
			
		||||
 | 
			
		||||
export interface IconButtonProps extends ButtonProps {
 | 
			
		||||
  tooltip: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
 | 
			
		||||
  ({ tooltip, children, ...rest }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <Tooltip>
 | 
			
		||||
        <TooltipTrigger asChild>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="ghost"
 | 
			
		||||
            size="icon"
 | 
			
		||||
            {...rest}
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            tabIndex={-1}
 | 
			
		||||
            className="cursor-default bg-background"
 | 
			
		||||
          >
 | 
			
		||||
            <div className="icon-button-icon-wrapper">{children}</div>
 | 
			
		||||
          </Button>
 | 
			
		||||
        </TooltipTrigger>
 | 
			
		||||
        <TooltipContent>
 | 
			
		||||
          <p>{tooltip}</p>
 | 
			
		||||
        </TooltipContent>
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export interface UploadButtonProps extends IconButtonProps {
 | 
			
		||||
  onFileUpload: (file: File) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ImageUploadButton = (props: UploadButtonProps) => {
 | 
			
		||||
  const { onFileUpload, children, ...rest } = props
 | 
			
		||||
 | 
			
		||||
  const [uploadElemId] = React.useState(
 | 
			
		||||
    `file-upload-${Math.random().toString()}`
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    const newFile = ev.currentTarget.files?.[0]
 | 
			
		||||
    if (newFile) {
 | 
			
		||||
      onFileUpload(newFile)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <label htmlFor={uploadElemId}>
 | 
			
		||||
        <IconButton {...rest} asChild>
 | 
			
		||||
          {children}
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </label>
 | 
			
		||||
      <Input
 | 
			
		||||
        style={{ display: "none" }}
 | 
			
		||||
        id={uploadElemId}
 | 
			
		||||
        name={uploadElemId}
 | 
			
		||||
        type="file"
 | 
			
		||||
        onChange={handleChange}
 | 
			
		||||
        accept="image/png, image/jpeg"
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Button, IconButton, ImageUploadButton, buttonVariants }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
 | 
			
		||||
import {
 | 
			
		||||
  CheckIcon,
 | 
			
		||||
  ChevronRightIcon,
 | 
			
		||||
  DotFilledIcon,
 | 
			
		||||
} from "@radix-ui/react-icons"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const ContextMenu = ContextMenuPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const ContextMenuGroup = ContextMenuPrimitive.Group
 | 
			
		||||
 | 
			
		||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
 | 
			
		||||
 | 
			
		||||
const ContextMenuSub = ContextMenuPrimitive.Sub
 | 
			
		||||
 | 
			
		||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
 | 
			
		||||
 | 
			
		||||
const ContextMenuSubTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
 | 
			
		||||
    inset?: boolean
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, children, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.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 focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    {children}
 | 
			
		||||
    <ChevronRightIcon className="ml-auto h-4 w-4" />
 | 
			
		||||
  </ContextMenuPrimitive.SubTrigger>
 | 
			
		||||
))
 | 
			
		||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuSubContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.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}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.Portal>
 | 
			
		||||
    <ContextMenuPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      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}
 | 
			
		||||
    />
 | 
			
		||||
  </ContextMenuPrimitive.Portal>
 | 
			
		||||
))
 | 
			
		||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
 | 
			
		||||
    inset?: boolean
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuCheckboxItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
 | 
			
		||||
>(({ className, children, checked, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.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 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">
 | 
			
		||||
      <ContextMenuPrimitive.ItemIndicator>
 | 
			
		||||
        <CheckIcon className="h-4 w-4" />
 | 
			
		||||
      </ContextMenuPrimitive.ItemIndicator>
 | 
			
		||||
    </span>
 | 
			
		||||
    {children}
 | 
			
		||||
  </ContextMenuPrimitive.CheckboxItem>
 | 
			
		||||
))
 | 
			
		||||
ContextMenuCheckboxItem.displayName =
 | 
			
		||||
  ContextMenuPrimitive.CheckboxItem.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuRadioItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.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 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">
 | 
			
		||||
      <ContextMenuPrimitive.ItemIndicator>
 | 
			
		||||
        <DotFilledIcon className="h-4 w-4 fill-current" />
 | 
			
		||||
      </ContextMenuPrimitive.ItemIndicator>
 | 
			
		||||
    </span>
 | 
			
		||||
    {children}
 | 
			
		||||
  </ContextMenuPrimitive.RadioItem>
 | 
			
		||||
))
 | 
			
		||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuLabel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.Label>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
 | 
			
		||||
    inset?: boolean
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.Label
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "px-2 py-1.5 text-sm font-semibold text-foreground",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuSeparator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ContextMenuPrimitive.Separator>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ContextMenuPrimitive.Separator
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("-mx-1 my-1 h-px bg-border", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
 | 
			
		||||
 | 
			
		||||
const ContextMenuShortcut = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <span
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "ml-auto text-xs tracking-widest text-muted-foreground",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  ContextMenu,
 | 
			
		||||
  ContextMenuTrigger,
 | 
			
		||||
  ContextMenuContent,
 | 
			
		||||
  ContextMenuItem,
 | 
			
		||||
  ContextMenuCheckboxItem,
 | 
			
		||||
  ContextMenuRadioItem,
 | 
			
		||||
  ContextMenuLabel,
 | 
			
		||||
  ContextMenuSeparator,
 | 
			
		||||
  ContextMenuShortcut,
 | 
			
		||||
  ContextMenuGroup,
 | 
			
		||||
  ContextMenuPortal,
 | 
			
		||||
  ContextMenuSub,
 | 
			
		||||
  ContextMenuSubContent,
 | 
			
		||||
  ContextMenuSubTrigger,
 | 
			
		||||
  ContextMenuRadioGroup,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,122 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
 | 
			
		||||
import { Cross2Icon } from "@radix-ui/react-icons"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Dialog = DialogPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const DialogTrigger = DialogPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const DialogPortal = DialogPrimitive.Portal
 | 
			
		||||
 | 
			
		||||
const DialogClose = DialogPrimitive.Close
 | 
			
		||||
 | 
			
		||||
const DialogOverlay = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Overlay>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Overlay
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
 | 
			
		||||
 | 
			
		||||
const DialogContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <DialogPortal>
 | 
			
		||||
    <DialogOverlay />
 | 
			
		||||
    <DialogPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "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,
 | 
			
		||||
        "outline-none"
 | 
			
		||||
      )}
 | 
			
		||||
      onCloseAutoFocus={(event) => event.preventDefault()}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 | 
			
		||||
        <Cross2Icon className="h-4 w-4" />
 | 
			
		||||
        <span className="sr-only">Close</span>
 | 
			
		||||
      </DialogPrimitive.Close>
 | 
			
		||||
    </DialogPrimitive.Content>
 | 
			
		||||
  </DialogPortal>
 | 
			
		||||
))
 | 
			
		||||
DialogContent.displayName = DialogPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
const DialogHeader = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col space-y-1.5 text-center sm:text-left",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
DialogHeader.displayName = "DialogHeader"
 | 
			
		||||
 | 
			
		||||
const DialogFooter = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
DialogFooter.displayName = "DialogFooter"
 | 
			
		||||
 | 
			
		||||
const DialogTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "text-2xl font-semibold leading-none tracking-tight",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
 | 
			
		||||
 | 
			
		||||
const DialogDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogPortal,
 | 
			
		||||
  DialogOverlay,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,204 @@
 | 
			
		|||
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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      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
 | 
			
		||||
      )}
 | 
			
		||||
      onCloseAutoFocus={(e) => e.preventDefault()}
 | 
			
		||||
      {...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,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,176 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import {
 | 
			
		||||
  Controller,
 | 
			
		||||
  ControllerProps,
 | 
			
		||||
  FieldPath,
 | 
			
		||||
  FieldValues,
 | 
			
		||||
  FormProvider,
 | 
			
		||||
  useFormContext,
 | 
			
		||||
} from "react-hook-form"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { Label } from "@/components/ui/label"
 | 
			
		||||
 | 
			
		||||
const Form = FormProvider
 | 
			
		||||
 | 
			
		||||
type FormFieldContextValue<
 | 
			
		||||
  TFieldValues extends FieldValues = FieldValues,
 | 
			
		||||
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
			
		||||
> = {
 | 
			
		||||
  name: TName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
 | 
			
		||||
  {} as FormFieldContextValue
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const FormField = <
 | 
			
		||||
  TFieldValues extends FieldValues = FieldValues,
 | 
			
		||||
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
			
		||||
>({
 | 
			
		||||
  ...props
 | 
			
		||||
}: ControllerProps<TFieldValues, TName>) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormFieldContext.Provider value={{ name: props.name }}>
 | 
			
		||||
      <Controller {...props} />
 | 
			
		||||
    </FormFieldContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useFormField = () => {
 | 
			
		||||
  const fieldContext = React.useContext(FormFieldContext)
 | 
			
		||||
  const itemContext = React.useContext(FormItemContext)
 | 
			
		||||
  const { getFieldState, formState } = useFormContext()
 | 
			
		||||
 | 
			
		||||
  const fieldState = getFieldState(fieldContext.name, formState)
 | 
			
		||||
 | 
			
		||||
  if (!fieldContext) {
 | 
			
		||||
    throw new Error("useFormField should be used within <FormField>")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { id } = itemContext
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id,
 | 
			
		||||
    name: fieldContext.name,
 | 
			
		||||
    formItemId: `${id}-form-item`,
 | 
			
		||||
    formDescriptionId: `${id}-form-item-description`,
 | 
			
		||||
    formMessageId: `${id}-form-item-message`,
 | 
			
		||||
    ...fieldState,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FormItemContextValue = {
 | 
			
		||||
  id: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FormItemContext = React.createContext<FormItemContextValue>(
 | 
			
		||||
  {} as FormItemContextValue
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const FormItem = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const id = React.useId()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItemContext.Provider value={{ id }}>
 | 
			
		||||
      <div ref={ref} className={cn("space-y-2", className)} {...props} />
 | 
			
		||||
    </FormItemContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormItem.displayName = "FormItem"
 | 
			
		||||
 | 
			
		||||
const FormLabel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const { error, formItemId } = useFormField()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Label
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(error && "text-destructive", "text-sm", className)}
 | 
			
		||||
      htmlFor={formItemId}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormLabel.displayName = "FormLabel"
 | 
			
		||||
 | 
			
		||||
const FormControl = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof Slot>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof Slot>
 | 
			
		||||
>(({ ...props }, ref) => {
 | 
			
		||||
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Slot
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      id={formItemId}
 | 
			
		||||
      aria-describedby={
 | 
			
		||||
        !error
 | 
			
		||||
          ? `${formDescriptionId}`
 | 
			
		||||
          : `${formDescriptionId} ${formMessageId}`
 | 
			
		||||
      }
 | 
			
		||||
      aria-invalid={!!error}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormControl.displayName = "FormControl"
 | 
			
		||||
 | 
			
		||||
const FormDescription = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const { formDescriptionId } = useFormField()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <p
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      id={formDescriptionId}
 | 
			
		||||
      className={cn("text-[0.8rem] text-muted-foreground", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormDescription.displayName = "FormDescription"
 | 
			
		||||
 | 
			
		||||
const FormMessage = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, children, ...props }, ref) => {
 | 
			
		||||
  const { error, formMessageId } = useFormField()
 | 
			
		||||
  const body = error ? String(error?.message) : children
 | 
			
		||||
 | 
			
		||||
  if (!body) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <p
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      id={formMessageId}
 | 
			
		||||
      className={cn("text-[0.8rem] font-medium text-destructive", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {body}
 | 
			
		||||
    </p>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormMessage.displayName = "FormMessage"
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  useFormField,
 | 
			
		||||
  Form,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
  FormField,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
 | 
			
		||||
export interface InputProps
 | 
			
		||||
  extends React.InputHTMLAttributes<HTMLInputElement> {}
 | 
			
		||||
 | 
			
		||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
 | 
			
		||||
  ({ className, type, ...props }, ref) => {
 | 
			
		||||
    const updateAppState = useStore((state) => state.updateAppState)
 | 
			
		||||
 | 
			
		||||
    const handleOnFocus = () => {
 | 
			
		||||
      updateAppState({ disableShortCuts: true })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleOnBlur = () => {
 | 
			
		||||
      updateAppState({ disableShortCuts: false })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <input
 | 
			
		||||
        type={type}
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "flex h-8 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        autoComplete="off"
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        onFocus={handleOnFocus}
 | 
			
		||||
        onBlur={handleOnBlur}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Input.displayName = "Input"
 | 
			
		||||
 | 
			
		||||
export interface NumberInputProps extends InputProps {
 | 
			
		||||
  numberValue: number
 | 
			
		||||
  allowFloat: boolean
 | 
			
		||||
  onNumberValueChange: (value: number) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
 | 
			
		||||
  ({ numberValue, allowFloat, onNumberValueChange, ...rest }, ref) => {
 | 
			
		||||
    const [value, setValue] = React.useState<string>(numberValue.toString())
 | 
			
		||||
 | 
			
		||||
    React.useEffect(() => {
 | 
			
		||||
      if (value !== numberValue.toString() + ".") {
 | 
			
		||||
        setValue(numberValue.toString())
 | 
			
		||||
      }
 | 
			
		||||
    }, [numberValue])
 | 
			
		||||
 | 
			
		||||
    const onInput = (evt: React.FormEvent<HTMLInputElement>) => {
 | 
			
		||||
      const target = evt.target as HTMLInputElement
 | 
			
		||||
      let val = target.value
 | 
			
		||||
      if (allowFloat) {
 | 
			
		||||
        val = val.replace(/[^0-9.]/g, "").replace(/(\..*?)\..*/g, "$1")
 | 
			
		||||
        if (val.length === 0) {
 | 
			
		||||
          onNumberValueChange(0)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        // val = parseFloat(val).toFixed(2)
 | 
			
		||||
        onNumberValueChange(parseFloat(val))
 | 
			
		||||
      } else {
 | 
			
		||||
        val = val.replace(/\D/g, "")
 | 
			
		||||
        if (val.length === 0) {
 | 
			
		||||
          onNumberValueChange(0)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        onNumberValueChange(parseInt(val, 10))
 | 
			
		||||
      }
 | 
			
		||||
      setValue(val)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <Input ref={ref} value={value} onInput={onInput} {...rest} />
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export { Input, NumberInput }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const labelVariants = cva(
 | 
			
		||||
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
interface LabelProps {
 | 
			
		||||
  disabled?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Label = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
 | 
			
		||||
    VariantProps<typeof labelVariants> &
 | 
			
		||||
    LabelProps
 | 
			
		||||
>(({ className, disabled, ...props }, ref) => (
 | 
			
		||||
  <LabelPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      labelVariants(),
 | 
			
		||||
      className,
 | 
			
		||||
      disabled ? "opacity-50" : "",
 | 
			
		||||
      "select-none"
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
Label.displayName = LabelPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Label }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Popover = PopoverPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const PopoverTrigger = PopoverPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const PopoverContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof PopoverPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
 | 
			
		||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <PopoverPrimitive.Portal>
 | 
			
		||||
    <PopoverPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      align={align}
 | 
			
		||||
      tabIndex={-1}
 | 
			
		||||
      sideOffset={sideOffset}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
 | 
			
		||||
    />
 | 
			
		||||
  </PopoverPrimitive.Portal>
 | 
			
		||||
))
 | 
			
		||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Popover, PopoverTrigger, PopoverContent }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Progress = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ProgressPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
 | 
			
		||||
>(({ className, value, ...props }, ref) => (
 | 
			
		||||
  <ProgressPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ProgressPrimitive.Indicator
 | 
			
		||||
      className="h-full w-full flex-1 bg-primary transition-all"
 | 
			
		||||
      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
 | 
			
		||||
    />
 | 
			
		||||
  </ProgressPrimitive.Root>
 | 
			
		||||
))
 | 
			
		||||
Progress.displayName = ProgressPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Progress }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import { CheckIcon } from "@radix-ui/react-icons"
 | 
			
		||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const RadioGroup = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof RadioGroupPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <RadioGroupPrimitive.Root
 | 
			
		||||
      className={cn("grid gap-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
      ref={ref}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
const RadioGroupItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof RadioGroupPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <RadioGroupPrimitive.Item
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
 | 
			
		||||
        <CheckIcon className="h-3.5 w-3.5 fill-primary" />
 | 
			
		||||
      </RadioGroupPrimitive.Indicator>
 | 
			
		||||
    </RadioGroupPrimitive.Item>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
 | 
			
		||||
 | 
			
		||||
export { RadioGroup, RadioGroupItem }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const ScrollArea = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <ScrollAreaPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("relative overflow-hidden", className)}
 | 
			
		||||
    scrollHideDelay={400}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
 | 
			
		||||
      {children}
 | 
			
		||||
    </ScrollAreaPrimitive.Viewport>
 | 
			
		||||
    <ScrollBar />
 | 
			
		||||
    <ScrollAreaPrimitive.Corner />
 | 
			
		||||
  </ScrollAreaPrimitive.Root>
 | 
			
		||||
))
 | 
			
		||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
const ScrollBar = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
 | 
			
		||||
>(({ className, orientation = "vertical", ...props }, ref) => (
 | 
			
		||||
  <ScrollAreaPrimitive.ScrollAreaScrollbar
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    orientation={orientation}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex touch-none select-none transition-colors",
 | 
			
		||||
      orientation === "vertical" &&
 | 
			
		||||
        "h-full w-2.5 border-l border-l-transparent p-[1px]",
 | 
			
		||||
      orientation === "horizontal" &&
 | 
			
		||||
        "h-2.5 flex-col border-t border-t-transparent p-[1px]",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
 | 
			
		||||
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
 | 
			
		||||
))
 | 
			
		||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
 | 
			
		||||
 | 
			
		||||
export { ScrollArea, ScrollBar }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,164 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import {
 | 
			
		||||
  CaretSortIcon,
 | 
			
		||||
  CheckIcon,
 | 
			
		||||
  ChevronDownIcon,
 | 
			
		||||
  ChevronUpIcon,
 | 
			
		||||
} from "@radix-ui/react-icons"
 | 
			
		||||
import * as SelectPrimitive from "@radix-ui/react-select"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Select = SelectPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const SelectGroup = SelectPrimitive.Group
 | 
			
		||||
 | 
			
		||||
const SelectValue = SelectPrimitive.Value
 | 
			
		||||
 | 
			
		||||
const SelectTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.Trigger
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    {children}
 | 
			
		||||
    <SelectPrimitive.Icon asChild>
 | 
			
		||||
      <CaretSortIcon className="h-4 w-4 opacity-50" />
 | 
			
		||||
    </SelectPrimitive.Icon>
 | 
			
		||||
  </SelectPrimitive.Trigger>
 | 
			
		||||
))
 | 
			
		||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
 | 
			
		||||
 | 
			
		||||
const SelectScrollUpButton = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.ScrollUpButton
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex cursor-default items-center justify-center py-1",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ChevronUpIcon />
 | 
			
		||||
  </SelectPrimitive.ScrollUpButton>
 | 
			
		||||
))
 | 
			
		||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
 | 
			
		||||
 | 
			
		||||
const SelectScrollDownButton = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.ScrollDownButton
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex cursor-default items-center justify-center py-1",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ChevronDownIcon />
 | 
			
		||||
  </SelectPrimitive.ScrollDownButton>
 | 
			
		||||
))
 | 
			
		||||
SelectScrollDownButton.displayName =
 | 
			
		||||
  SelectPrimitive.ScrollDownButton.displayName
 | 
			
		||||
 | 
			
		||||
const SelectContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
 | 
			
		||||
>(({ className, children, position = "popper", ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.Portal>
 | 
			
		||||
    <SelectPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
 | 
			
		||||
        position === "popper" &&
 | 
			
		||||
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      position={position}
 | 
			
		||||
      onCloseAutoFocus={(event) => event.preventDefault()}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <SelectScrollUpButton />
 | 
			
		||||
      <SelectPrimitive.Viewport
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "p-1",
 | 
			
		||||
          position === "popper" &&
 | 
			
		||||
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </SelectPrimitive.Viewport>
 | 
			
		||||
      <SelectScrollDownButton />
 | 
			
		||||
    </SelectPrimitive.Content>
 | 
			
		||||
  </SelectPrimitive.Portal>
 | 
			
		||||
))
 | 
			
		||||
SelectContent.displayName = SelectPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
const SelectLabel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.Label>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.Label
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
 | 
			
		||||
 | 
			
		||||
const SelectItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
			
		||||
      <SelectPrimitive.ItemIndicator>
 | 
			
		||||
        <CheckIcon className="h-4 w-4" />
 | 
			
		||||
      </SelectPrimitive.ItemIndicator>
 | 
			
		||||
    </span>
 | 
			
		||||
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
 | 
			
		||||
  </SelectPrimitive.Item>
 | 
			
		||||
))
 | 
			
		||||
SelectItem.displayName = SelectPrimitive.Item.displayName
 | 
			
		||||
 | 
			
		||||
const SelectSeparator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SelectPrimitive.Separator>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SelectPrimitive.Separator
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectLabel,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectSeparator,
 | 
			
		||||
  SelectScrollUpButton,
 | 
			
		||||
  SelectScrollDownButton,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Separator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SeparatorPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
 | 
			
		||||
>(
 | 
			
		||||
  (
 | 
			
		||||
    { className, orientation = "horizontal", decorative = true, ...props },
 | 
			
		||||
    ref
 | 
			
		||||
  ) => (
 | 
			
		||||
    <SeparatorPrimitive.Root
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      decorative={decorative}
 | 
			
		||||
      orientation={orientation}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "shrink-0 bg-border",
 | 
			
		||||
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
)
 | 
			
		||||
Separator.displayName = SeparatorPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Separator }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Sheet = SheetPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const SheetTrigger = SheetPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const SheetClose = SheetPrimitive.Close
 | 
			
		||||
 | 
			
		||||
const SheetPortal = SheetPrimitive.Portal
 | 
			
		||||
 | 
			
		||||
const SheetOverlay = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Overlay>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Overlay
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed inset-0 bg-background/80 backdrop-blur-sm data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
    ref={ref}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
 | 
			
		||||
 | 
			
		||||
const sheetVariants = cva(
 | 
			
		||||
  "fixed gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-200 data-[state=open]:duration-300",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      side: {
 | 
			
		||||
        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
 | 
			
		||||
        bottom:
 | 
			
		||||
          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
 | 
			
		||||
        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
 | 
			
		||||
        right:
 | 
			
		||||
          "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      side: "right",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
interface SheetContentProps
 | 
			
		||||
  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
 | 
			
		||||
    VariantProps<typeof sheetVariants> {}
 | 
			
		||||
 | 
			
		||||
const SheetContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Content>,
 | 
			
		||||
  SheetContentProps
 | 
			
		||||
>(({ side = "right", className, children, ...props }, ref) => (
 | 
			
		||||
  <SheetPortal>
 | 
			
		||||
    <SheetOverlay />
 | 
			
		||||
    <SheetPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(sheetVariants({ side }), className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </SheetPrimitive.Content>
 | 
			
		||||
  </SheetPortal>
 | 
			
		||||
))
 | 
			
		||||
SheetContent.displayName = SheetPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
const SheetHeader = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col space-y-2 text-center sm:text-left",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
SheetHeader.displayName = "SheetHeader"
 | 
			
		||||
 | 
			
		||||
const SheetFooter = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
)
 | 
			
		||||
SheetFooter.displayName = "SheetFooter"
 | 
			
		||||
 | 
			
		||||
const SheetTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-lg font-semibold text-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
 | 
			
		||||
 | 
			
		||||
const SheetDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Sheet,
 | 
			
		||||
  SheetPortal,
 | 
			
		||||
  SheetOverlay,
 | 
			
		||||
  SheetTrigger,
 | 
			
		||||
  SheetClose,
 | 
			
		||||
  SheetContent,
 | 
			
		||||
  SheetHeader,
 | 
			
		||||
  SheetFooter,
 | 
			
		||||
  SheetTitle,
 | 
			
		||||
  SheetDescription,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as SliderPrimitive from "@radix-ui/react-slider"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Slider = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SliderPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SliderPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex w-full touch-none select-none items-center",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
 | 
			
		||||
      <SliderPrimitive.Range className="absolute h-full bg-primary data-[disabled]:cursor-not-allowed " />
 | 
			
		||||
    </SliderPrimitive.Track>
 | 
			
		||||
    <SliderPrimitive.Thumb
 | 
			
		||||
      tabIndex={-1}
 | 
			
		||||
      className="block h-4 w-4 rounded-full border border-primary/60 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring data-[disabled]:cursor-not-allowed"
 | 
			
		||||
    />
 | 
			
		||||
  </SliderPrimitive.Root>
 | 
			
		||||
))
 | 
			
		||||
Slider.displayName = SliderPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Slider }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Switch = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SwitchPrimitives.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SwitchPrimitives.Root
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
    ref={ref}
 | 
			
		||||
  >
 | 
			
		||||
    <SwitchPrimitives.Thumb
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  </SwitchPrimitives.Root>
 | 
			
		||||
))
 | 
			
		||||
Switch.displayName = SwitchPrimitives.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Switch }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Tabs = TabsPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const TabsList = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.List>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.List
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
TabsList.displayName = TabsPrimitive.List.displayName
 | 
			
		||||
 | 
			
		||||
const TabsTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.Trigger
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
 | 
			
		||||
 | 
			
		||||
const TabsContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
TabsContent.displayName = TabsPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { useStore } from "@/lib/states"
 | 
			
		||||
 | 
			
		||||
export interface TextareaProps
 | 
			
		||||
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
 | 
			
		||||
 | 
			
		||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
 | 
			
		||||
  ({ className, ...props }, ref) => {
 | 
			
		||||
    const updateAppState = useStore((state) => state.updateAppState)
 | 
			
		||||
 | 
			
		||||
    const handleOnFocus = () => {
 | 
			
		||||
      updateAppState({ disableShortCuts: true })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleOnBlur = () => {
 | 
			
		||||
      updateAppState({ disableShortCuts: false })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <textarea
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
          "overflow-auto",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        tabIndex={-1}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        onFocus={handleOnFocus}
 | 
			
		||||
        onBlur={handleOnBlur}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Textarea.displayName = "Textarea"
 | 
			
		||||
 | 
			
		||||
export { Textarea }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import { Cross2Icon } from "@radix-ui/react-icons"
 | 
			
		||||
import * as ToastPrimitives from "@radix-ui/react-toast"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const ToastProvider = ToastPrimitives.Provider
 | 
			
		||||
 | 
			
		||||
const ToastViewport = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Viewport>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Viewport
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
 | 
			
		||||
 | 
			
		||||
const toastVariants = cva(
 | 
			
		||||
  "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "border bg-background text-foreground",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "destructive group border-destructive bg-destructive text-destructive-foreground",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Toast = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
 | 
			
		||||
    VariantProps<typeof toastVariants>
 | 
			
		||||
>(({ className, variant, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastPrimitives.Root
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(toastVariants({ variant }), className)}
 | 
			
		||||
      tabIndex={-1}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
Toast.displayName = ToastPrimitives.Root.displayName
 | 
			
		||||
 | 
			
		||||
const ToastAction = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Action>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Action
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastAction.displayName = ToastPrimitives.Action.displayName
 | 
			
		||||
 | 
			
		||||
const ToastClose = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Close>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Close
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    toast-close=""
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <Cross2Icon className="h-4 w-4" />
 | 
			
		||||
  </ToastPrimitives.Close>
 | 
			
		||||
))
 | 
			
		||||
ToastClose.displayName = ToastPrimitives.Close.displayName
 | 
			
		||||
 | 
			
		||||
const ToastTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm font-semibold [&+div]:text-xs", className)}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
 | 
			
		||||
 | 
			
		||||
const ToastDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm opacity-90", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
    tabIndex={-1}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
 | 
			
		||||
 | 
			
		||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
 | 
			
		||||
 | 
			
		||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  type ToastProps,
 | 
			
		||||
  type ToastActionElement,
 | 
			
		||||
  ToastProvider,
 | 
			
		||||
  ToastViewport,
 | 
			
		||||
  Toast,
 | 
			
		||||
  ToastTitle,
 | 
			
		||||
  ToastDescription,
 | 
			
		||||
  ToastClose,
 | 
			
		||||
  ToastAction,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import {
 | 
			
		||||
  Toast,
 | 
			
		||||
  ToastClose,
 | 
			
		||||
  ToastDescription,
 | 
			
		||||
  ToastProvider,
 | 
			
		||||
  ToastTitle,
 | 
			
		||||
  ToastViewport,
 | 
			
		||||
} from "@/components/ui/toast"
 | 
			
		||||
import { useToast } from "@/components/ui/use-toast"
 | 
			
		||||
 | 
			
		||||
export function Toaster() {
 | 
			
		||||
  const { toasts } = useToast()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastProvider>
 | 
			
		||||
      {toasts.map(function ({ id, title, description, action, ...props }) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Toast key={id} {...props}>
 | 
			
		||||
            <div className="grid gap-1">
 | 
			
		||||
              {title && <ToastTitle>{title}</ToastTitle>}
 | 
			
		||||
              {description && (
 | 
			
		||||
                <ToastDescription>{description}</ToastDescription>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
            {action}
 | 
			
		||||
            <ToastClose />
 | 
			
		||||
          </Toast>
 | 
			
		||||
        )
 | 
			
		||||
      })}
 | 
			
		||||
      <ToastViewport />
 | 
			
		||||
    </ToastProvider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const toggleVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-transparent",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-3",
 | 
			
		||||
        sm: "h-8 px-2",
 | 
			
		||||
        lg: "h-10 px-3",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Toggle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TogglePrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
 | 
			
		||||
    VariantProps<typeof toggleVariants>
 | 
			
		||||
>(({ className, variant, size, ...props }, ref) => (
 | 
			
		||||
  <TogglePrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(toggleVariants({ variant, size, className }))}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
 | 
			
		||||
Toggle.displayName = TogglePrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Toggle, toggleVariants }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const TooltipProvider = TooltipPrimitive.Provider
 | 
			
		||||
 | 
			
		||||
const Tooltip = TooltipPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const TooltipTrigger = TooltipPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const TooltipContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TooltipPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
 | 
			
		||||
>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <TooltipPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    sideOffset={sideOffset}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "z-50 border overflow-hidden rounded-md bg-background text-foreground px-3 py-1.5 text-xs animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,192 @@
 | 
			
		|||
// Inspired by react-hot-toast library
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ToastActionElement,
 | 
			
		||||
  ToastProps,
 | 
			
		||||
} from "@/components/ui/toast"
 | 
			
		||||
 | 
			
		||||
const TOAST_LIMIT = 1
 | 
			
		||||
const TOAST_REMOVE_DELAY = 1000000
 | 
			
		||||
 | 
			
		||||
type ToasterToast = ToastProps & {
 | 
			
		||||
  id: string
 | 
			
		||||
  title?: React.ReactNode
 | 
			
		||||
  description?: React.ReactNode
 | 
			
		||||
  action?: ToastActionElement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const actionTypes = {
 | 
			
		||||
  ADD_TOAST: "ADD_TOAST",
 | 
			
		||||
  UPDATE_TOAST: "UPDATE_TOAST",
 | 
			
		||||
  DISMISS_TOAST: "DISMISS_TOAST",
 | 
			
		||||
  REMOVE_TOAST: "REMOVE_TOAST",
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
let count = 0
 | 
			
		||||
 | 
			
		||||
function genId() {
 | 
			
		||||
  count = (count + 1) % Number.MAX_VALUE
 | 
			
		||||
  return count.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActionType = typeof actionTypes
 | 
			
		||||
 | 
			
		||||
type Action =
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["ADD_TOAST"]
 | 
			
		||||
      toast: ToasterToast
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["UPDATE_TOAST"]
 | 
			
		||||
      toast: Partial<ToasterToast>
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["DISMISS_TOAST"]
 | 
			
		||||
      toastId?: ToasterToast["id"]
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["REMOVE_TOAST"]
 | 
			
		||||
      toastId?: ToasterToast["id"]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
  toasts: ToasterToast[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
 | 
			
		||||
 | 
			
		||||
const addToRemoveQueue = (toastId: string) => {
 | 
			
		||||
  if (toastTimeouts.has(toastId)) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const timeout = setTimeout(() => {
 | 
			
		||||
    toastTimeouts.delete(toastId)
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: "REMOVE_TOAST",
 | 
			
		||||
      toastId: toastId,
 | 
			
		||||
    })
 | 
			
		||||
  }, TOAST_REMOVE_DELAY)
 | 
			
		||||
 | 
			
		||||
  toastTimeouts.set(toastId, timeout)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const reducer = (state: State, action: Action): State => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case "ADD_TOAST":
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    case "UPDATE_TOAST":
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.map((t) =>
 | 
			
		||||
          t.id === action.toast.id ? { ...t, ...action.toast } : t
 | 
			
		||||
        ),
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    case "DISMISS_TOAST": {
 | 
			
		||||
      const { toastId } = action
 | 
			
		||||
 | 
			
		||||
      // ! Side effects ! - This could be extracted into a dismissToast() action,
 | 
			
		||||
      // but I'll keep it here for simplicity
 | 
			
		||||
      if (toastId) {
 | 
			
		||||
        addToRemoveQueue(toastId)
 | 
			
		||||
      } else {
 | 
			
		||||
        state.toasts.forEach((toast) => {
 | 
			
		||||
          addToRemoveQueue(toast.id)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.map((t) =>
 | 
			
		||||
          t.id === toastId || toastId === undefined
 | 
			
		||||
            ? {
 | 
			
		||||
                ...t,
 | 
			
		||||
                open: false,
 | 
			
		||||
              }
 | 
			
		||||
            : t
 | 
			
		||||
        ),
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    case "REMOVE_TOAST":
 | 
			
		||||
      if (action.toastId === undefined) {
 | 
			
		||||
        return {
 | 
			
		||||
          ...state,
 | 
			
		||||
          toasts: [],
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.filter((t) => t.id !== action.toastId),
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const listeners: Array<(state: State) => void> = []
 | 
			
		||||
 | 
			
		||||
let memoryState: State = { toasts: [] }
 | 
			
		||||
 | 
			
		||||
function dispatch(action: Action) {
 | 
			
		||||
  memoryState = reducer(memoryState, action)
 | 
			
		||||
  listeners.forEach((listener) => {
 | 
			
		||||
    listener(memoryState)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Toast = Omit<ToasterToast, "id">
 | 
			
		||||
 | 
			
		||||
function toast({ ...props }: Toast) {
 | 
			
		||||
  const id = genId()
 | 
			
		||||
 | 
			
		||||
  const update = (props: ToasterToast) =>
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: "UPDATE_TOAST",
 | 
			
		||||
      toast: { ...props, id },
 | 
			
		||||
    })
 | 
			
		||||
  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
 | 
			
		||||
 | 
			
		||||
  dispatch({
 | 
			
		||||
    type: "ADD_TOAST",
 | 
			
		||||
    toast: {
 | 
			
		||||
      ...props,
 | 
			
		||||
      id,
 | 
			
		||||
      open: true,
 | 
			
		||||
      onOpenChange: (open) => {
 | 
			
		||||
        if (!open) dismiss()
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id: id,
 | 
			
		||||
    dismiss,
 | 
			
		||||
    update,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useToast() {
 | 
			
		||||
  const [state, setState] = React.useState<State>(memoryState)
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    listeners.push(setState)
 | 
			
		||||
    return () => {
 | 
			
		||||
      const index = listeners.indexOf(setState)
 | 
			
		||||
      if (index > -1) {
 | 
			
		||||
        listeners.splice(index, 1)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [state])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...state,
 | 
			
		||||
    toast,
 | 
			
		||||
    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { useToast, toast }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
html { font-family: "Inter", "system-ui"; overflow: hidden; }
 | 
			
		||||
 | 
			
		||||
@supports (font-variation-settings: normal) {
 | 
			
		||||
  html { font-family: "Inter var", "system-ui"; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.react-transform-wrapper {
 | 
			
		||||
  display: grid !important;
 | 
			
		||||
  width: 100% !important;
 | 
			
		||||
  height: 100% !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.react-photo-album {
 | 
			
		||||
  padding: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.react-photo-album--photo {
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
  transition: transform 0.25s, visibility 0.25s ease-in;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.react-photo-album--photo:hover {
 | 
			
		||||
  border: 1px solid var(--border);
 | 
			
		||||
  transform: scale(1.03);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.icon-button-icon-wrapper svg {
 | 
			
		||||
  stroke-width: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    --background: 0 0% 100%;
 | 
			
		||||
    --foreground: 224 71.4% 4.1%;
 | 
			
		||||
 | 
			
		||||
    --card: 0 0% 100%;
 | 
			
		||||
    --card-foreground: 224 71.4% 4.1%;
 | 
			
		||||
 
 | 
			
		||||
    --popover: 0 0% 100%;
 | 
			
		||||
    --popover-foreground: 224 71.4% 4.1%;
 | 
			
		||||
 
 | 
			
		||||
    --primary: 48 100.0% 50.0%;
 | 
			
		||||
    --primary-foreground: 210 20% 98%;
 | 
			
		||||
 
 | 
			
		||||
    --secondary: 220 14.3% 95.9%;
 | 
			
		||||
    --secondary-foreground: 220.9 39.3% 11%;
 | 
			
		||||
 
 | 
			
		||||
    --muted: 220 14.3% 95.9%;
 | 
			
		||||
    --muted-foreground: 220 8.9% 46.1%;
 | 
			
		||||
 
 | 
			
		||||
    --accent: 220 14.3% 95.9%;
 | 
			
		||||
    --accent-foreground: 220.9 39.3% 11%;
 | 
			
		||||
 
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
    --destructive-foreground: 210 20% 98%;
 | 
			
		||||
 | 
			
		||||
    --border: 220 13% 91%;
 | 
			
		||||
    --input: 220 13% 91%;
 | 
			
		||||
    --ring: 224 71.4% 4.1%;
 | 
			
		||||
 
 | 
			
		||||
    --radius: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
  [data-theme='dark'] {
 | 
			
		||||
    --background: 240 10% 3.9%;
 | 
			
		||||
    --foreground: 0 0% 98%;
 | 
			
		||||
 
 | 
			
		||||
    --card: 224 71.4% 4.1%;
 | 
			
		||||
    --card-foreground: 210 20% 98%;
 | 
			
		||||
 
 | 
			
		||||
    --popover: 240 10% 3.9%;
 | 
			
		||||
    --popover-foreground: 0 0% 98%;;
 | 
			
		||||
 
 | 
			
		||||
    --primary: 48 100.0% 50.0%;
 | 
			
		||||
    --primary-foreground: 220.9 39.3% 11%;
 | 
			
		||||
 
 | 
			
		||||
    --secondary: 240 3.7% 15.9%;
 | 
			
		||||
    --secondary-foreground: 0 0% 98%;
 | 
			
		||||
 
 | 
			
		||||
    --muted: 240 3.7% 15.9%;
 | 
			
		||||
    --muted-foreground: 240 5% 64.9%;
 | 
			
		||||
 
 | 
			
		||||
    --accent: 240 3.7% 15.9%;
 | 
			
		||||
    --accent-foreground: 0 0% 98%;
 | 
			
		||||
 
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
    --destructive-foreground: 0 85.7% 97.3%;
 | 
			
		||||
 
 | 
			
		||||
    --border: 240 3.7% 15.9%;
 | 
			
		||||
    --input: 240 3.7% 15.9%;
 | 
			
		||||
    --ring: 240 4.9% 83.9%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import { DependencyList, useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export function useAsyncMemo<T>(
 | 
			
		||||
  factory: () => Promise<T> | undefined | null,
 | 
			
		||||
  deps: DependencyList
 | 
			
		||||
): T | undefined
 | 
			
		||||
export function useAsyncMemo<T>(
 | 
			
		||||
  factory: () => Promise<T> | undefined | null,
 | 
			
		||||
  deps: DependencyList,
 | 
			
		||||
  initial: T
 | 
			
		||||
): T
 | 
			
		||||
export function useAsyncMemo<T>(
 | 
			
		||||
  factory: () => Promise<T> | undefined | null,
 | 
			
		||||
  deps: DependencyList,
 | 
			
		||||
  initial?: T
 | 
			
		||||
) {
 | 
			
		||||
  const [val, setVal] = useState<T | undefined>(initial)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let cancel = false
 | 
			
		||||
    const promise = factory()
 | 
			
		||||
    if (promise === undefined || promise === null) return
 | 
			
		||||
    promise.then(v => {
 | 
			
		||||
      if (!cancel) {
 | 
			
		||||
        setVal(v)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return () => {
 | 
			
		||||
      cancel = true
 | 
			
		||||
    }
 | 
			
		||||
  }, deps)
 | 
			
		||||
  return val
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { useStore } from "@/lib/states"
 | 
			
		||||
import { useHotkeys } from "react-hotkeys-hook"
 | 
			
		||||
 | 
			
		||||
const useHotKey = (keys: string, callback: any, deps?: any[]) => {
 | 
			
		||||
  const disableShortCuts = useStore((state) => state.disableShortCuts)
 | 
			
		||||
 | 
			
		||||
  const ref = useHotkeys(keys, callback, { enabled: !disableShortCuts }, deps)
 | 
			
		||||
  return ref
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useHotKey
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import { useEffect, useState } from "react"
 | 
			
		||||
 | 
			
		||||
function useImage(file: File | null): [HTMLImageElement, boolean] {
 | 
			
		||||
  const [image] = useState(new Image())
 | 
			
		||||
  const [isLoaded, setIsLoaded] = useState(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!file) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    image.onload = () => {
 | 
			
		||||
      setIsLoaded(true)
 | 
			
		||||
    }
 | 
			
		||||
    setIsLoaded(false)
 | 
			
		||||
    image.src = URL.createObjectURL(file)
 | 
			
		||||
    return () => {
 | 
			
		||||
      image.onload = null
 | 
			
		||||
    }
 | 
			
		||||
  }, [file, image])
 | 
			
		||||
 | 
			
		||||
  return [image, isLoaded]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { useImage }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
import { API_ENDPOINT } from "@/lib/api"
 | 
			
		||||
import { useCallback, useEffect, useState } from "react"
 | 
			
		||||
 | 
			
		||||
export default function useInputImage() {
 | 
			
		||||
  const [inputImage, setInputImage] = useState<File | null>(null)
 | 
			
		||||
 | 
			
		||||
  const fetchInputImage = useCallback(() => {
 | 
			
		||||
    const headers = new Headers()
 | 
			
		||||
    headers.append("pragma", "no-cache")
 | 
			
		||||
    headers.append("cache-control", "no-cache")
 | 
			
		||||
 | 
			
		||||
    fetch(`${API_ENDPOINT}/inputimage`, { headers })
 | 
			
		||||
      .then(async (res) => {
 | 
			
		||||
        if (!res.ok) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const filename = res.headers
 | 
			
		||||
          .get("content-disposition")
 | 
			
		||||
          ?.split("filename=")[1]
 | 
			
		||||
          .split(";")[0]
 | 
			
		||||
 | 
			
		||||
        const data = await res.blob()
 | 
			
		||||
        if (data && data.type.startsWith("image")) {
 | 
			
		||||
          const userInput = new File(
 | 
			
		||||
            [data],
 | 
			
		||||
            filename !== undefined ? filename : "inputImage"
 | 
			
		||||
          )
 | 
			
		||||
          setInputImage(userInput)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.log(err)
 | 
			
		||||
      })
 | 
			
		||||
  }, [setInputImage])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchInputImage()
 | 
			
		||||
  }, [fetchInputImage])
 | 
			
		||||
 | 
			
		||||
  return inputImage
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
const useResolution = () => {
 | 
			
		||||
  const [width, setWidth] = useState(window.innerWidth)
 | 
			
		||||
 | 
			
		||||
  const windowSizeHandler = useCallback(() => {
 | 
			
		||||
    setWidth(window.innerWidth)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.addEventListener('resize', windowSizeHandler)
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('resize', windowSizeHandler)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (width < 768) {
 | 
			
		||||
    return 'mobile'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (width >= 768 && width < 1224) {
 | 
			
		||||
    return 'tablet'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (width >= 1224) {
 | 
			
		||||
    return 'desktop'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useResolution
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,231 @@
 | 
			
		|||
import {
 | 
			
		||||
  Filename,
 | 
			
		||||
  GenInfo,
 | 
			
		||||
  ModelInfo,
 | 
			
		||||
  PowerPaintTask,
 | 
			
		||||
  Rect,
 | 
			
		||||
  ServerConfig,
 | 
			
		||||
} from "@/lib/types"
 | 
			
		||||
import { Settings } from "@/lib/states"
 | 
			
		||||
import { convertToBase64, srcToFile } from "@/lib/utils"
 | 
			
		||||
import axios from "axios"
 | 
			
		||||
 | 
			
		||||
export const API_ENDPOINT = import.meta.env.DEV
 | 
			
		||||
  ? import.meta.env.VITE_BACKEND + "/api/v1"
 | 
			
		||||
  : "/api/v1"
 | 
			
		||||
 | 
			
		||||
const api = axios.create({
 | 
			
		||||
  baseURL: API_ENDPOINT,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default async function inpaint(
 | 
			
		||||
  imageFile: File,
 | 
			
		||||
  settings: Settings,
 | 
			
		||||
  croperRect: Rect,
 | 
			
		||||
  extenderState: Rect,
 | 
			
		||||
  mask: File | Blob,
 | 
			
		||||
  paintByExampleImage: File | null = null
 | 
			
		||||
) {
 | 
			
		||||
  const imageBase64 = await convertToBase64(imageFile)
 | 
			
		||||
  const maskBase64 = await convertToBase64(mask)
 | 
			
		||||
  const exampleImageBase64 = paintByExampleImage
 | 
			
		||||
    ? await convertToBase64(paintByExampleImage)
 | 
			
		||||
    : null
 | 
			
		||||
 | 
			
		||||
  const res = await fetch(`${API_ENDPOINT}/inpaint`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      image: imageBase64,
 | 
			
		||||
      mask: maskBase64,
 | 
			
		||||
      ldm_steps: settings.ldmSteps,
 | 
			
		||||
      ldm_sampler: settings.ldmSampler,
 | 
			
		||||
      zits_wireframe: settings.zitsWireframe,
 | 
			
		||||
      cv2_flag: settings.cv2Flag,
 | 
			
		||||
      cv2_radius: settings.cv2Radius,
 | 
			
		||||
      hd_strategy: "Crop",
 | 
			
		||||
      hd_strategy_crop_triger_size: 640,
 | 
			
		||||
      hd_strategy_crop_margin: 128,
 | 
			
		||||
      hd_trategy_resize_imit: 2048,
 | 
			
		||||
      prompt: settings.prompt,
 | 
			
		||||
      negative_prompt: settings.negativePrompt,
 | 
			
		||||
      use_croper: settings.showCropper,
 | 
			
		||||
      croper_x: croperRect.x,
 | 
			
		||||
      croper_y: croperRect.y,
 | 
			
		||||
      croper_height: croperRect.height,
 | 
			
		||||
      croper_width: croperRect.width,
 | 
			
		||||
      use_extender: settings.showExtender,
 | 
			
		||||
      extender_x: extenderState.x,
 | 
			
		||||
      extender_y: extenderState.y,
 | 
			
		||||
      extender_height: extenderState.height,
 | 
			
		||||
      extender_width: extenderState.width,
 | 
			
		||||
      sd_mask_blur: settings.sdMaskBlur,
 | 
			
		||||
      sd_strength: settings.sdStrength,
 | 
			
		||||
      sd_steps: settings.sdSteps,
 | 
			
		||||
      sd_guidance_scale: settings.sdGuidanceScale,
 | 
			
		||||
      sd_sampler: settings.sdSampler,
 | 
			
		||||
      sd_seed: settings.seedFixed ? settings.seed : -1,
 | 
			
		||||
      sd_match_histograms: settings.sdMatchHistograms,
 | 
			
		||||
      sd_freeu: settings.enableFreeu,
 | 
			
		||||
      sd_freeu_config: settings.freeuConfig,
 | 
			
		||||
      sd_lcm_lora: settings.enableLCMLora,
 | 
			
		||||
      paint_by_example_example_image: exampleImageBase64,
 | 
			
		||||
      p2p_image_guidance_scale: settings.p2pImageGuidanceScale,
 | 
			
		||||
      enable_controlnet: settings.enableControlnet,
 | 
			
		||||
      controlnet_conditioning_scale: settings.controlnetConditioningScale,
 | 
			
		||||
      controlnet_method: settings.controlnetMethod
 | 
			
		||||
        ? settings.controlnetMethod
 | 
			
		||||
        : "",
 | 
			
		||||
      powerpaint_task: settings.showExtender
 | 
			
		||||
        ? PowerPaintTask.outpainting
 | 
			
		||||
        : settings.powerpaintTask,
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  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(`Something went wrong: ${errors.errors}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getServerConfig(): Promise<ServerConfig> {
 | 
			
		||||
  const res = await api.get(`/server-config`)
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function switchModel(name: string): Promise<ModelInfo> {
 | 
			
		||||
  const res = await api.post(`/model`, { name })
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function switchPluginModel(
 | 
			
		||||
  plugin_name: string,
 | 
			
		||||
  model_name: string
 | 
			
		||||
) {
 | 
			
		||||
  return api.post(`/switch_plugin_model`, { plugin_name, model_name })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function currentModel(): Promise<ModelInfo> {
 | 
			
		||||
  const res = await api.get("/model")
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runPlugin(
 | 
			
		||||
  genMask: boolean,
 | 
			
		||||
  name: string,
 | 
			
		||||
  imageFile: File,
 | 
			
		||||
  upscale?: number,
 | 
			
		||||
  clicks?: number[][]
 | 
			
		||||
) {
 | 
			
		||||
  const imageBase64 = await convertToBase64(imageFile)
 | 
			
		||||
  const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image"
 | 
			
		||||
  const res = await fetch(`${API_ENDPOINT}/${p}`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      name,
 | 
			
		||||
      image: imageBase64,
 | 
			
		||||
      upscale,
 | 
			
		||||
      clicks,
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  if (res.ok) {
 | 
			
		||||
    const blob = await res.blob()
 | 
			
		||||
    return { blob: URL.createObjectURL(blob) }
 | 
			
		||||
  }
 | 
			
		||||
  const errMsg = await res.json()
 | 
			
		||||
  throw new Error(errMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getMediaFile(tab: string, filename: string) {
 | 
			
		||||
  const res = await fetch(
 | 
			
		||||
    `${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
 | 
			
		||||
      filename
 | 
			
		||||
    )}`,
 | 
			
		||||
    {
 | 
			
		||||
      method: "GET",
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  if (res.ok) {
 | 
			
		||||
    const blob = await res.blob()
 | 
			
		||||
    const file = new File([blob], filename, {
 | 
			
		||||
      type: res.headers.get("Content-Type") ?? "image/png",
 | 
			
		||||
    })
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
  const errMsg = await res.json()
 | 
			
		||||
  throw new Error(errMsg.errors)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getMedias(tab: string): Promise<Filename[]> {
 | 
			
		||||
  const res = await api.get(`medias`, { params: { tab } })
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function downloadToOutput(
 | 
			
		||||
  image: HTMLImageElement,
 | 
			
		||||
  filename: string,
 | 
			
		||||
  mimeType: string
 | 
			
		||||
) {
 | 
			
		||||
  const file = await srcToFile(image.src, filename, mimeType)
 | 
			
		||||
  const fd = new FormData()
 | 
			
		||||
  fd.append("file", file)
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(`${API_ENDPOINT}/save_image`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      body: fd,
 | 
			
		||||
    })
 | 
			
		||||
    if (!res.ok) {
 | 
			
		||||
      const errMsg = await res.text()
 | 
			
		||||
      throw new Error(errMsg)
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    throw new Error(`Something went wrong: ${error}`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getGenInfo(file: File): Promise<GenInfo> {
 | 
			
		||||
  const fd = new FormData()
 | 
			
		||||
  fd.append("file", file)
 | 
			
		||||
  const res = await api.post(`/gen-info`, fd)
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getSamplers(): Promise<string[]> {
 | 
			
		||||
  const res = await api.post("/samplers")
 | 
			
		||||
  return res.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function postAdjustMask(
 | 
			
		||||
  mask: File | Blob,
 | 
			
		||||
  operate: "expand" | "shrink" | "reverse",
 | 
			
		||||
  kernel_size: number
 | 
			
		||||
) {
 | 
			
		||||
  const maskBase64 = await convertToBase64(mask)
 | 
			
		||||
  const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      mask: maskBase64,
 | 
			
		||||
      operate: operate,
 | 
			
		||||
      kernel_size: kernel_size,
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  if (res.ok) {
 | 
			
		||||
    const blob = await res.blob()
 | 
			
		||||
    return blob
 | 
			
		||||
  }
 | 
			
		||||
  const errMsg = await res.json()
 | 
			
		||||
  throw new Error(errMsg)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
export const ACCENT_COLOR = "#ffcc00bb"
 | 
			
		||||
export const DEFAULT_BRUSH_SIZE = 40
 | 
			
		||||
export const MIN_BRUSH_SIZE = 3
 | 
			
		||||
export const MAX_BRUSH_SIZE = 200
 | 
			
		||||
export const MODEL_TYPE_INPAINT = "inpaint"
 | 
			
		||||
export const MODEL_TYPE_DIFFUSERS_SD = "diffusers_sd"
 | 
			
		||||
export const MODEL_TYPE_DIFFUSERS_SDXL = "diffusers_sdxl"
 | 
			
		||||
export const MODEL_TYPE_DIFFUSERS_SD_INPAINT = "diffusers_sd_inpaint"
 | 
			
		||||
export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
 | 
			
		||||
export const MODEL_TYPE_OTHER = "diffusers_other"
 | 
			
		||||
export const BRUSH_COLOR = "#ffcc00bb"
 | 
			
		||||
 | 
			
		||||
export const LDM = "ldm"
 | 
			
		||||
export const CV2 = "cv2"
 | 
			
		||||
 | 
			
		||||
export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
 | 
			
		||||
export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
 | 
			
		||||
export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"
 | 
			
		||||
export const POWERPAINT = "Sanster/PowerPaint-V1-stable-diffusion-inpainting"
 | 
			
		||||
export const ANYTEXT = "Sanster/AnyText"
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_NEGATIVE_PROMPT =
 | 
			
		||||
  "out of frame, lowres, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, disfigured, gross proportions, malformed limbs, watermark, signature"
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
export interface Filename {
 | 
			
		||||
  name: string
 | 
			
		||||
  height: number
 | 
			
		||||
  width: number
 | 
			
		||||
  ctime: number
 | 
			
		||||
  mtime: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PluginInfo {
 | 
			
		||||
  name: string
 | 
			
		||||
  support_gen_image: boolean
 | 
			
		||||
  support_gen_mask: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ServerConfig {
 | 
			
		||||
  plugins: PluginInfo[]
 | 
			
		||||
  modelInfos: ModelInfo[]
 | 
			
		||||
  removeBGModel: string
 | 
			
		||||
  removeBGModels: string[]
 | 
			
		||||
  realesrganModel: string
 | 
			
		||||
  realesrganModels: string[]
 | 
			
		||||
  interactiveSegModel: string
 | 
			
		||||
  interactiveSegModels: string[]
 | 
			
		||||
  enableFileManager: boolean
 | 
			
		||||
  enableAutoSaving: boolean
 | 
			
		||||
  enableControlnet: boolean
 | 
			
		||||
  controlnetMethod: string
 | 
			
		||||
  disableModelSwitch: boolean
 | 
			
		||||
  isDesktop: boolean
 | 
			
		||||
  samplers: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenInfo {
 | 
			
		||||
  prompt: string
 | 
			
		||||
  negative_prompt: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ModelInfo {
 | 
			
		||||
  name: string
 | 
			
		||||
  path: string
 | 
			
		||||
  model_type:
 | 
			
		||||
    | "inpaint"
 | 
			
		||||
    | "diffusers_sd"
 | 
			
		||||
    | "diffusers_sdxl"
 | 
			
		||||
    | "diffusers_sd_inpaint"
 | 
			
		||||
    | "diffusers_sdxl_inpaint"
 | 
			
		||||
    | "diffusers_other"
 | 
			
		||||
  support_strength: boolean
 | 
			
		||||
  support_outpainting: boolean
 | 
			
		||||
  support_controlnet: boolean
 | 
			
		||||
  controlnets: string[]
 | 
			
		||||
  support_freeu: boolean
 | 
			
		||||
  support_lcm_lora: boolean
 | 
			
		||||
  need_prompt: boolean
 | 
			
		||||
  is_single_file_diffusers: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PluginName {
 | 
			
		||||
  RemoveBG = "RemoveBG",
 | 
			
		||||
  AnimeSeg = "AnimeSeg",
 | 
			
		||||
  RealESRGAN = "RealESRGAN",
 | 
			
		||||
  GFPGAN = "GFPGAN",
 | 
			
		||||
  RestoreFormer = "RestoreFormer",
 | 
			
		||||
  InteractiveSeg = "InteractiveSeg",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PluginParams {
 | 
			
		||||
  upscale: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum SortBy {
 | 
			
		||||
  NAME = "name",
 | 
			
		||||
  CTIME = "ctime",
 | 
			
		||||
  MTIME = "mtime",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum SortOrder {
 | 
			
		||||
  DESCENDING = "desc",
 | 
			
		||||
  ASCENDING = "asc",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum LDMSampler {
 | 
			
		||||
  ddim = "ddim",
 | 
			
		||||
  plms = "plms",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum CV2Flag {
 | 
			
		||||
  INPAINT_NS = "INPAINT_NS",
 | 
			
		||||
  INPAINT_TELEA = "INPAINT_TELEA",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Rect {
 | 
			
		||||
  x: number
 | 
			
		||||
  y: number
 | 
			
		||||
  width: number
 | 
			
		||||
  height: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FreeuConfig {
 | 
			
		||||
  s1: number
 | 
			
		||||
  s2: number
 | 
			
		||||
  b1: number
 | 
			
		||||
  b2: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Point {
 | 
			
		||||
  x: number
 | 
			
		||||
  y: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Line {
 | 
			
		||||
  size?: number
 | 
			
		||||
  pts: Point[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type LineGroup = Array<Line>
 | 
			
		||||
 | 
			
		||||
export interface Size {
 | 
			
		||||
  width: number
 | 
			
		||||
  height: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ExtenderDirection {
 | 
			
		||||
  x = "x",
 | 
			
		||||
  y = "y",
 | 
			
		||||
  xy = "xy",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PowerPaintTask {
 | 
			
		||||
  text_guided = "text-guided",
 | 
			
		||||
  shape_guided = "shape-guided",
 | 
			
		||||
  object_remove = "object-remove",
 | 
			
		||||
  outpainting = "outpainting",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AdjustMaskOperate = "expand" | "shrink" | "reverse"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,246 @@
 | 
			
		|||
import { type ClassValue, clsx } from "clsx"
 | 
			
		||||
import { SyntheticEvent } from "react"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
import { LineGroup } from "./types"
 | 
			
		||||
import { BRUSH_COLOR } from "./const"
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function keepGUIAlive() {
 | 
			
		||||
  async function getRequest(url = "") {
 | 
			
		||||
    const response = await fetch(url, {
 | 
			
		||||
      method: "GET",
 | 
			
		||||
      cache: "no-cache",
 | 
			
		||||
    })
 | 
			
		||||
    return response.json()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const keepAliveServer = () => {
 | 
			
		||||
    const url = document.location
 | 
			
		||||
    const route = "/flaskwebgui-keep-server-alive"
 | 
			
		||||
    getRequest(url + route).then((data) => {
 | 
			
		||||
      return data
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const intervalRequest = 3 * 1000
 | 
			
		||||
  keepAliveServer()
 | 
			
		||||
  setInterval(keepAliveServer, intervalRequest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function dataURItoBlob(dataURI: string) {
 | 
			
		||||
  const mime = dataURI.split(",")[0].split(":")[1].split(";")[0]
 | 
			
		||||
  const binary = atob(dataURI.split(",")[1])
 | 
			
		||||
  const array = []
 | 
			
		||||
  for (let i = 0; i < binary.length; i += 1) {
 | 
			
		||||
    array.push(binary.charCodeAt(i))
 | 
			
		||||
  }
 | 
			
		||||
  return new Blob([new Uint8Array(array)], { type: mime })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function loadImage(image: HTMLImageElement, src: string) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const initSRC = image.src
 | 
			
		||||
    const img = image
 | 
			
		||||
    img.onload = resolve
 | 
			
		||||
    img.onerror = (err) => {
 | 
			
		||||
      img.src = initSRC
 | 
			
		||||
      reject(err)
 | 
			
		||||
    }
 | 
			
		||||
    img.src = src
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function blobToImage(blob: Blob) {
 | 
			
		||||
  const dataURL = URL.createObjectURL(blob)
 | 
			
		||||
  const newImage = new Image()
 | 
			
		||||
  await loadImage(newImage, dataURL)
 | 
			
		||||
  return newImage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canvasToImage(
 | 
			
		||||
  canvas: HTMLCanvasElement
 | 
			
		||||
): Promise<HTMLImageElement> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const image = new Image()
 | 
			
		||||
 | 
			
		||||
    image.addEventListener("load", () => {
 | 
			
		||||
      resolve(image)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    image.addEventListener("error", (error) => {
 | 
			
		||||
      reject(error)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    image.src = canvas.toDataURL()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fileToImage(file: File): Promise<HTMLImageElement> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const reader = new FileReader()
 | 
			
		||||
    reader.onload = () => {
 | 
			
		||||
      const image = new Image()
 | 
			
		||||
      image.onload = () => {
 | 
			
		||||
        resolve(image)
 | 
			
		||||
      }
 | 
			
		||||
      image.onerror = () => {
 | 
			
		||||
        reject("无法加载图像。")
 | 
			
		||||
      }
 | 
			
		||||
      image.src = reader.result as string
 | 
			
		||||
    }
 | 
			
		||||
    reader.onerror = () => {
 | 
			
		||||
      reject("无法读取文件。")
 | 
			
		||||
    }
 | 
			
		||||
    reader.readAsDataURL(file)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function srcToFile(src: string, fileName: string, mimeType: string) {
 | 
			
		||||
  return fetch(src)
 | 
			
		||||
    .then(function (res) {
 | 
			
		||||
      return res.arrayBuffer()
 | 
			
		||||
    })
 | 
			
		||||
    .then(function (buf) {
 | 
			
		||||
      return new File([buf], fileName, { type: mimeType })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function askWritePermission() {
 | 
			
		||||
  try {
 | 
			
		||||
    // The clipboard-write permission is granted automatically to pages
 | 
			
		||||
    // when they are the active tab. So it's not required, but it's more safe.
 | 
			
		||||
    const { state } = await navigator.permissions.query({
 | 
			
		||||
      name: "clipboard-write" as PermissionName,
 | 
			
		||||
    })
 | 
			
		||||
    return state === "granted"
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // Browser compatibility / Security error (ONLY HTTPS) ...
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function canvasToBlob(canvas: HTMLCanvasElement, mime: string): Promise<any> {
 | 
			
		||||
  return new Promise((resolve, reject) =>
 | 
			
		||||
    canvas.toBlob(async (d) => {
 | 
			
		||||
      if (d) {
 | 
			
		||||
        resolve(d)
 | 
			
		||||
      } else {
 | 
			
		||||
        reject(new Error("Expected toBlob() to be defined"))
 | 
			
		||||
      }
 | 
			
		||||
    }, mime)
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setToClipboard = async (blob: any) => {
 | 
			
		||||
  const data = [new ClipboardItem({ [blob.type]: blob })]
 | 
			
		||||
  await navigator.clipboard.write(data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isRightClick(ev: SyntheticEvent) {
 | 
			
		||||
  const mouseEvent = ev.nativeEvent as MouseEvent
 | 
			
		||||
  return mouseEvent.button === 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMidClick(ev: SyntheticEvent) {
 | 
			
		||||
  const mouseEvent = ev.nativeEvent as MouseEvent
 | 
			
		||||
  return mouseEvent.button === 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function copyCanvasImage(canvas: HTMLCanvasElement) {
 | 
			
		||||
  const blob = await canvasToBlob(canvas, "image/png")
 | 
			
		||||
  try {
 | 
			
		||||
    await setToClipboard(blob)
 | 
			
		||||
  } catch {
 | 
			
		||||
    console.log("Copy image failed!")
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function downloadImage(uri: string, name: string) {
 | 
			
		||||
  const link = document.createElement("a")
 | 
			
		||||
  link.href = uri
 | 
			
		||||
  link.download = name
 | 
			
		||||
 | 
			
		||||
  // this is necessary as link.click() does not work on the latest firefox
 | 
			
		||||
  link.dispatchEvent(
 | 
			
		||||
    new MouseEvent("click", {
 | 
			
		||||
      bubbles: true,
 | 
			
		||||
      cancelable: true,
 | 
			
		||||
      view: window,
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    // For Firefox it is necessary to delay revoking the ObjectURL
 | 
			
		||||
    // window.URL.revokeObjectURL(base64)
 | 
			
		||||
    link.remove()
 | 
			
		||||
  }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mouseXY(ev: SyntheticEvent) {
 | 
			
		||||
  const mouseEvent = ev.nativeEvent as MouseEvent
 | 
			
		||||
  return { x: mouseEvent.offsetX, y: mouseEvent.offsetY }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function drawLines(
 | 
			
		||||
  ctx: CanvasRenderingContext2D,
 | 
			
		||||
  lines: LineGroup,
 | 
			
		||||
  color = BRUSH_COLOR
 | 
			
		||||
) {
 | 
			
		||||
  ctx.strokeStyle = color
 | 
			
		||||
  ctx.lineCap = "round"
 | 
			
		||||
  ctx.lineJoin = "round"
 | 
			
		||||
 | 
			
		||||
  lines.forEach((line) => {
 | 
			
		||||
    if (!line?.pts.length || !line.size) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    ctx.lineWidth = line.size
 | 
			
		||||
    ctx.beginPath()
 | 
			
		||||
    ctx.moveTo(line.pts[0].x, line.pts[0].y)
 | 
			
		||||
    line.pts.forEach((pt) => ctx.lineTo(pt.x, pt.y))
 | 
			
		||||
    ctx.stroke()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const generateMask = (
 | 
			
		||||
  imageWidth: number,
 | 
			
		||||
  imageHeight: number,
 | 
			
		||||
  lineGroups: LineGroup[],
 | 
			
		||||
  maskImages: HTMLImageElement[] = [],
 | 
			
		||||
  lineGroupsColor: string = "white"
 | 
			
		||||
): HTMLCanvasElement => {
 | 
			
		||||
  const maskCanvas = document.createElement("canvas")
 | 
			
		||||
  maskCanvas.width = imageWidth
 | 
			
		||||
  maskCanvas.height = imageHeight
 | 
			
		||||
  const ctx = maskCanvas.getContext("2d")
 | 
			
		||||
  if (!ctx) {
 | 
			
		||||
    throw new Error("could not retrieve mask canvas")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  maskImages.forEach((maskImage) => {
 | 
			
		||||
    ctx.drawImage(maskImage, 0, 0, imageWidth, imageHeight)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  lineGroups.forEach((lineGroup) => {
 | 
			
		||||
    drawLines(ctx, lineGroup, lineGroupsColor)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return maskCanvas
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const convertToBase64 = (fileOrBlob: File | Blob): Promise<string> => {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const reader = new FileReader()
 | 
			
		||||
    reader.onload = (event) => {
 | 
			
		||||
      const base64String = event.target?.result as string
 | 
			
		||||
      resolve(base64String)
 | 
			
		||||
    }
 | 
			
		||||
    reader.onerror = (error) => {
 | 
			
		||||
      reject(error)
 | 
			
		||||
    }
 | 
			
		||||
    reader.readAsDataURL(fileOrBlob)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import React from "react"
 | 
			
		||||
import ReactDOM from "react-dom/client"
 | 
			
		||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 | 
			
		||||
import "inter-ui/inter.css"
 | 
			
		||||
import App from "./App.tsx"
 | 
			
		||||
import "./globals.css"
 | 
			
		||||
import { ThemeProvider } from "next-themes"
 | 
			
		||||
import { TooltipProvider } from "./components/ui/tooltip.tsx"
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient()
 | 
			
		||||
 | 
			
		||||
ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <QueryClientProvider client={queryClient}>
 | 
			
		||||
      <ThemeProvider defaultTheme="dark" disableTransitionOnChange>
 | 
			
		||||
        <TooltipProvider>
 | 
			
		||||
          <App />
 | 
			
		||||
        </TooltipProvider>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </QueryClientProvider>
 | 
			
		||||
  </React.StrictMode>
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/// <reference types="vite/client" />
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
/** @type {import('tailwindcss').Config} */
 | 
			
		||||
module.exports = {
 | 
			
		||||
  darkMode: ["class"],
 | 
			
		||||
  content: [
 | 
			
		||||
    "./pages/**/*.{ts,tsx}",
 | 
			
		||||
    "./components/**/*.{ts,tsx}",
 | 
			
		||||
    "./app/**/*.{ts,tsx}",
 | 
			
		||||
    "./src/**/*.{ts,tsx}",
 | 
			
		||||
  ],
 | 
			
		||||
  theme: {
 | 
			
		||||
    container: {
 | 
			
		||||
      center: true,
 | 
			
		||||
      padding: "2rem",
 | 
			
		||||
      screens: {
 | 
			
		||||
        "2xl": "1400px",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        border: "hsl(var(--border))",
 | 
			
		||||
        input: "hsl(var(--input))",
 | 
			
		||||
        ring: "hsl(var(--ring))",
 | 
			
		||||
        background: "hsl(var(--background))",
 | 
			
		||||
        foreground: "hsl(var(--foreground))",
 | 
			
		||||
        primary: {
 | 
			
		||||
          DEFAULT: "hsl(var(--primary))",
 | 
			
		||||
          foreground: "hsl(var(--primary-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        secondary: {
 | 
			
		||||
          DEFAULT: "hsl(var(--secondary))",
 | 
			
		||||
          foreground: "hsl(var(--secondary-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        destructive: {
 | 
			
		||||
          DEFAULT: "hsl(var(--destructive))",
 | 
			
		||||
          foreground: "hsl(var(--destructive-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        muted: {
 | 
			
		||||
          DEFAULT: "hsl(var(--muted))",
 | 
			
		||||
          foreground: "hsl(var(--muted-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        accent: {
 | 
			
		||||
          DEFAULT: "hsl(var(--accent))",
 | 
			
		||||
          foreground: "hsl(var(--accent-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        popover: {
 | 
			
		||||
          DEFAULT: "hsl(var(--popover))",
 | 
			
		||||
          foreground: "hsl(var(--popover-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        card: {
 | 
			
		||||
          DEFAULT: "hsl(var(--card))",
 | 
			
		||||
          foreground: "hsl(var(--card-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      borderRadius: {
 | 
			
		||||
        lg: "var(--radius)",
 | 
			
		||||
        md: "calc(var(--radius) - 2px)",
 | 
			
		||||
        sm: "calc(var(--radius) - 4px)",
 | 
			
		||||
      },
 | 
			
		||||
      keyframes: {
 | 
			
		||||
        "accordion-down": {
 | 
			
		||||
          from: { height: 0 },
 | 
			
		||||
          to: { height: "var(--radix-accordion-content-height)" },
 | 
			
		||||
        },
 | 
			
		||||
        "accordion-up": {
 | 
			
		||||
          from: { height: "var(--radix-accordion-content-height)" },
 | 
			
		||||
          to: { height: 0 },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      animation: {
 | 
			
		||||
        "accordion-down": "accordion-down 0.2s ease-out",
 | 
			
		||||
        "accordion-up": "accordion-up 0.2s ease-out",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [require("tailwindcss-animate")],
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
 | 
			
		||||
    /* Bundler mode */
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "jsx": "react-jsx",
 | 
			
		||||
 | 
			
		||||
    /* Linting */
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "noUnusedParameters": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": [
 | 
			
		||||
        "./src/*"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src"],
 | 
			
		||||
  "references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "allowSyntheticDefaultImports": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import path from "path"
 | 
			
		||||
import react from "@vitejs/plugin-react"
 | 
			
		||||
import { defineConfig } from "vite"
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [react()],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      "@": path.resolve(__dirname, "./src"),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
		Reference in New Issue