new web init
This commit is contained in:
parent
a5c241ac02
commit
04c5dfece8
18
web_app/.eslintrc.cjs
Normal file
18
web_app/.eslintrc.cjs
Normal file
@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
24
web_app/.gitignore
vendored
Normal file
24
web_app/.gitignore
vendored
Normal file
@ -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?
|
30
web_app/README.md
Normal file
30
web_app/README.md
Normal file
@ -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
|
16
web_app/components.json
Normal file
16
web_app/components.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
13
web_app/index.html
Normal file
13
web_app/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5775
web_app/package-lock.json
generated
Normal file
5775
web_app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
web_app/package.json
Normal file
65
web_app/package.json
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
|
"@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",
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"flexsearch": "^0.7.21",
|
||||||
|
"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-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",
|
||||||
|
"tailwind-merge": "^2.0.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/flexsearch": "^0.7.3",
|
||||||
|
"@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"
|
||||||
|
}
|
||||||
|
}
|
6
web_app/postcss.config.js
Normal file
6
web_app/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
173
web_app/src/App.tsx
Normal file
173
web_app/src/App.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import { useRecoilState, useSetRecoilState } from "recoil"
|
||||||
|
import { fileState, serverConfigState } from "@/lib/store"
|
||||||
|
import useInputImage from "@/hooks/useInputImage"
|
||||||
|
import { keepGUIAlive } from "@/lib/utils"
|
||||||
|
import { getServerConfig, isDesktop } from "@/lib/api"
|
||||||
|
import Header from "@/components/Header"
|
||||||
|
import Workspace from "@/components/Workspace"
|
||||||
|
import FileSelect from "@/components/FileSelect"
|
||||||
|
import { Toaster } from "./components/ui/toaster"
|
||||||
|
|
||||||
|
const SUPPORTED_FILE_TYPE = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/bmp",
|
||||||
|
"image/tiff",
|
||||||
|
]
|
||||||
|
function Home() {
|
||||||
|
const [file, setFile] = useRecoilState(fileState)
|
||||||
|
const userInputImage = useInputImage()
|
||||||
|
const setServerConfigState = useSetRecoilState(serverConfigState)
|
||||||
|
|
||||||
|
// Set Input Image
|
||||||
|
useEffect(() => {
|
||||||
|
setFile(userInputImage)
|
||||||
|
}, [userInputImage, setFile])
|
||||||
|
|
||||||
|
// Keeping GUI Window Open
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const isRunDesktop = await isDesktop().then((res) => res.text())
|
||||||
|
if (isRunDesktop === "True") {
|
||||||
|
keepGUIAlive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchServerConfig = async () => {
|
||||||
|
const serverConfig = await getServerConfig().then((res) => res.json())
|
||||||
|
console.log(serverConfig)
|
||||||
|
setServerConfigState(serverConfig)
|
||||||
|
}
|
||||||
|
fetchServerConfig()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const workspaceId = useMemo(() => {
|
||||||
|
return nanoid()
|
||||||
|
}, [file])
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
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
|
||||||
|
if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
|
||||||
|
setIsDragging(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDragOut = useCallback((event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
dragCounter.current -= 1
|
||||||
|
if (dragCounter.current > 0) return
|
||||||
|
setIsDragging(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDrop = useCallback((event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
setIsDragging(false)
|
||||||
|
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">
|
||||||
|
<Toaster />
|
||||||
|
<Header />
|
||||||
|
<Workspace key={workspaceId} />
|
||||||
|
{!file ? (
|
||||||
|
<FileSelect
|
||||||
|
onSelection={async (f) => {
|
||||||
|
setFile(f)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
1704
web_app/src/components/Editor.tsx
Normal file
1704
web_app/src/components/Editor.tsx
Normal file
File diff suppressed because it is too large
Load Diff
321
web_app/src/components/FileManager.tsx
Normal file
321
web_app/src/components/FileManager.tsx
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import {
|
||||||
|
SyntheticEvent,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
FormEvent,
|
||||||
|
} from "react"
|
||||||
|
import _ from "lodash"
|
||||||
|
import { useRecoilState } from "recoil"
|
||||||
|
import PhotoAlbum from "react-photo-album"
|
||||||
|
import {
|
||||||
|
BarsArrowDownIcon,
|
||||||
|
BarsArrowUpIcon,
|
||||||
|
FolderIcon,
|
||||||
|
} from "@heroicons/react/24/outline"
|
||||||
|
import {
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
ViewHorizontalIcon,
|
||||||
|
ViewGridIcon,
|
||||||
|
} from "@radix-ui/react-icons"
|
||||||
|
import { useDebounce, useToggle } from "react-use"
|
||||||
|
import FlexSearch from "flexsearch/dist/flexsearch.bundle.js"
|
||||||
|
import {
|
||||||
|
fileManagerLayout,
|
||||||
|
fileManagerSearchText,
|
||||||
|
fileManagerSortBy,
|
||||||
|
fileManagerSortOrder,
|
||||||
|
SortBy,
|
||||||
|
SortOrder,
|
||||||
|
} from "@/lib/store"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
import { 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 { useHotkeys } from "react-hotkeys-hook"
|
||||||
|
|
||||||
|
interface Photo {
|
||||||
|
src: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Filename {
|
||||||
|
name: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
ctime: number
|
||||||
|
mtime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const SORT_BY_NAME = "Name"
|
||||||
|
const SORT_BY_CREATED_TIME = "Created time"
|
||||||
|
const SORT_BY_MODIFIED_TIME = "Modified time"
|
||||||
|
|
||||||
|
const IMAGE_TAB = "image"
|
||||||
|
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)
|
||||||
|
|
||||||
|
useHotkeys("f", () => {
|
||||||
|
toggleOpen()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const [scrollTop, setScrollTop] = useState(0)
|
||||||
|
const [closeScrollTop, setCloseScrollTop] = useState(0)
|
||||||
|
|
||||||
|
const [sortBy, setSortBy] = useRecoilState<SortBy>(fileManagerSortBy)
|
||||||
|
const [sortOrder, setSortOrder] = useRecoilState(fileManagerSortOrder)
|
||||||
|
const [layout, setLayout] = useRecoilState(fileManagerLayout)
|
||||||
|
const [debouncedSearchText, setDebouncedSearchText] = useRecoilState(
|
||||||
|
fileManagerSearchText
|
||||||
|
)
|
||||||
|
const ref = useRef(null)
|
||||||
|
const [searchText, setSearchText] = useState(debouncedSearchText)
|
||||||
|
const [tab, setTab] = useState(IMAGE_TAB)
|
||||||
|
const [photos, setPhotos] = useState<Photo[]>([])
|
||||||
|
|
||||||
|
const [, cancel] = useDebounce(
|
||||||
|
() => {
|
||||||
|
setDebouncedSearchText(searchText)
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
[searchText]
|
||||||
|
)
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (!open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const filenames = await getMedias(tab)
|
||||||
|
let filteredFilenames = filenames
|
||||||
|
if (debouncedSearchText) {
|
||||||
|
const index = new FlexSearch.Index({
|
||||||
|
tokenize: "forward",
|
||||||
|
minlength: 1,
|
||||||
|
})
|
||||||
|
filenames.forEach((filename: Filename, id: number) =>
|
||||||
|
index.add(id, filename.name)
|
||||||
|
)
|
||||||
|
const results: FlexSearch.IndexSearchResult =
|
||||||
|
index.search(debouncedSearchText)
|
||||||
|
filteredFilenames = results.map(
|
||||||
|
(id: FlexSearch.Id) => filenames[id as number]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredFilenames = _.orderBy(filteredFilenames, sortBy, sortOrder)
|
||||||
|
|
||||||
|
const newPhotos = filteredFilenames.map((filename: Filename) => {
|
||||||
|
const width = photoWidth
|
||||||
|
const height = filename.height * (width / filename.width)
|
||||||
|
const src = `${API_ENDPOINT}/media_thumbnail/${tab}/${filename.name}?width=${width}&height=${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()
|
||||||
|
}, [tab, debouncedSearchText, sortBy, sortOrder, photoWidth, open])
|
||||||
|
|
||||||
|
const onScroll = (event: SyntheticEvent) => {
|
||||||
|
setScrollTop(event.currentTarget.scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = ({ index }: { index: number }) => {
|
||||||
|
toggleOpen()
|
||||||
|
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={() => {
|
||||||
|
setLayout("rows")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ViewHorizontalIcon
|
||||||
|
className={layout !== "rows" ? "opacity-50" : ""}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
tooltip="Grid layout"
|
||||||
|
onClick={() => {
|
||||||
|
setLayout("masonry")
|
||||||
|
}}
|
||||||
|
className={layout !== "masonry" ? "opacity-50" : ""}
|
||||||
|
>
|
||||||
|
<ViewGridIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={toggleOpen}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<IconButton tooltip="File Manager">
|
||||||
|
<FolderIcon />
|
||||||
|
</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={searchText}
|
||||||
|
className="w-[250px] pl-[30px]"
|
||||||
|
tabIndex={-1}
|
||||||
|
onInput={(evt: FormEvent<HTMLInputElement>) => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
const target = evt.target as HTMLInputElement
|
||||||
|
setSearchText(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[sortBy]}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
switch (val) {
|
||||||
|
case SORT_BY_NAME:
|
||||||
|
setSortBy(SortBy.NAME)
|
||||||
|
break
|
||||||
|
case SORT_BY_CREATED_TIME:
|
||||||
|
setSortBy(SortBy.CTIME)
|
||||||
|
break
|
||||||
|
case SORT_BY_MODIFIED_TIME:
|
||||||
|
setSortBy(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>
|
||||||
|
|
||||||
|
{sortOrder === SortOrder.DESCENDING ? (
|
||||||
|
<IconButton
|
||||||
|
tooltip="Descending Order"
|
||||||
|
onClick={() => {
|
||||||
|
setSortOrder(SortOrder.ASCENDING)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BarsArrowDownIcon />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
tooltip="Ascending Order"
|
||||||
|
onClick={() => {
|
||||||
|
setSortOrder(SortOrder.DESCENDING)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BarsArrowUpIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea
|
||||||
|
className="w-full h-full rounded-md"
|
||||||
|
onScroll={onScroll}
|
||||||
|
ref={onRefChange}
|
||||||
|
>
|
||||||
|
<PhotoAlbum
|
||||||
|
layout={layout}
|
||||||
|
photos={photos}
|
||||||
|
spacing={12}
|
||||||
|
padding={0}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
71
web_app/src/components/FileSelect.tsx
Normal file
71
web_app/src/components/FileSelect.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { 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 ">
|
||||||
|
<label
|
||||||
|
htmlFor={uploadElemId}
|
||||||
|
className="grid cursor-pointer border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:cursor-pointer hover:bg-primary hover:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
173
web_app/src/components/Header.tsx
Normal file
173
web_app/src/components/Header.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { FolderIcon, PhotoIcon } from "@heroicons/react/24/outline"
|
||||||
|
import { PlayIcon } from "@radix-ui/react-icons"
|
||||||
|
import React, { useCallback, useState } from "react"
|
||||||
|
import { useRecoilState, useRecoilValue } from "recoil"
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook"
|
||||||
|
import {
|
||||||
|
enableFileManagerState,
|
||||||
|
fileState,
|
||||||
|
isInpaintingState,
|
||||||
|
isPix2PixState,
|
||||||
|
isSDState,
|
||||||
|
maskState,
|
||||||
|
runManuallyState,
|
||||||
|
showFileManagerState,
|
||||||
|
} from "@/lib/store"
|
||||||
|
import { Button, IconButton, ImageUploadButton } from "@/components/ui/button"
|
||||||
|
import Shortcuts from "@/components/Shortcuts"
|
||||||
|
// import SettingIcon from "../Settings/SettingIcon"
|
||||||
|
// import PromptInput from "./PromptInput"
|
||||||
|
// import CoffeeIcon from '../CoffeeIcon/CoffeeIcon'
|
||||||
|
import emitter, {
|
||||||
|
DREAM_BUTTON_MOUSE_ENTER,
|
||||||
|
DREAM_BUTTON_MOUSE_LEAVE,
|
||||||
|
EVENT_CUSTOM_MASK,
|
||||||
|
RERUN_LAST_MASK,
|
||||||
|
} from "@/lib/event"
|
||||||
|
import { useImage } from "@/hooks/useImage"
|
||||||
|
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||||
|
import PromptInput from "./PromptInput"
|
||||||
|
import { RotateCw } from "lucide-react"
|
||||||
|
import FileManager from "./FileManager"
|
||||||
|
import { getMediaFile } from "@/lib/api"
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const isInpainting = useRecoilValue(isInpaintingState)
|
||||||
|
const [file, setFile] = useRecoilState(fileState)
|
||||||
|
const [mask, setMask] = useRecoilState(maskState)
|
||||||
|
const [maskImage, maskImageLoaded] = useImage(mask)
|
||||||
|
const isSD = useRecoilValue(isSDState)
|
||||||
|
const isPix2Pix = useRecoilValue(isPix2PixState)
|
||||||
|
const runManually = useRecoilValue(runManuallyState)
|
||||||
|
const [openMaskPopover, setOpenMaskPopover] = useState(false)
|
||||||
|
const enableFileManager = useRecoilValue(enableFileManagerState)
|
||||||
|
|
||||||
|
const handleRerunLastMask = useCallback(() => {
|
||||||
|
emitter.emit(RERUN_LAST_MASK)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onRerunMouseEnter = () => {
|
||||||
|
emitter.emit(DREAM_BUTTON_MOUSE_ENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRerunMouseLeave = () => {
|
||||||
|
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
"r",
|
||||||
|
() => {
|
||||||
|
if (!isInpainting) {
|
||||||
|
handleRerunLastMask()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
[isInpainting, handleRerunLastMask]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 backdrop-filter backdrop-blur-md border-b">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{enableFileManager ? (
|
||||||
|
<FileManager
|
||||||
|
photoWidth={512}
|
||||||
|
onPhotoClick={async (tab: string, filename: string) => {
|
||||||
|
const newFile = await getMediaFile(tab, filename)
|
||||||
|
setFile(newFile)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ImageUploadButton
|
||||||
|
disabled={isInpainting}
|
||||||
|
tooltip="Upload image"
|
||||||
|
onFileUpload={(file) => {
|
||||||
|
setFile(file)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PhotoIcon />
|
||||||
|
</ImageUploadButton>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
visibility: file ? "visible" : "hidden",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageUploadButton
|
||||||
|
disabled={isInpainting}
|
||||||
|
tooltip="Upload custom mask"
|
||||||
|
onFileUpload={(file) => {
|
||||||
|
setMask(file)
|
||||||
|
console.info("Send custom mask")
|
||||||
|
if (!runManually) {
|
||||||
|
emitter.emit(EVENT_CUSTOM_MASK, { mask: file })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>M</div>
|
||||||
|
</ImageUploadButton>
|
||||||
|
|
||||||
|
{mask ? (
|
||||||
|
<Popover open={openMaskPopover}>
|
||||||
|
<PopoverTrigger
|
||||||
|
className="btn-primary side-panel-trigger"
|
||||||
|
onMouseEnter={() => setOpenMaskPopover(true)}
|
||||||
|
onMouseLeave={() => setOpenMaskPopover(false)}
|
||||||
|
style={{
|
||||||
|
visibility: mask ? "visible" : "hidden",
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (mask) {
|
||||||
|
emitter.emit(EVENT_CUSTOM_MASK, { mask })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton tooltip="Run custom mask">
|
||||||
|
<PlayIcon />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
{maskImageLoaded ? (
|
||||||
|
<img src={maskImage.src} alt="Custom mask" />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
disabled={isInpainting}
|
||||||
|
tooltip="Rerun last mask"
|
||||||
|
onClick={handleRerunLastMask}
|
||||||
|
onMouseEnter={onRerunMouseEnter}
|
||||||
|
onMouseLeave={onRerunMouseLeave}
|
||||||
|
>
|
||||||
|
<RotateCw />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSD ? <PromptInput /> : <></>}
|
||||||
|
|
||||||
|
<div className="header-icons-wrapper">
|
||||||
|
{/* <CoffeeIcon /> */}
|
||||||
|
<div className="header-icons">
|
||||||
|
<Shortcuts />
|
||||||
|
{/* <SettingIcon /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
66
web_app/src/components/PromptInput.tsx
Normal file
66
web_app/src/components/PromptInput.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React, { FormEvent } from "react"
|
||||||
|
import { useRecoilState, useRecoilValue } from "recoil"
|
||||||
|
import emitter, {
|
||||||
|
DREAM_BUTTON_MOUSE_ENTER,
|
||||||
|
DREAM_BUTTON_MOUSE_LEAVE,
|
||||||
|
EVENT_PROMPT,
|
||||||
|
} from "@/lib/event"
|
||||||
|
import { appState, isInpaintingState, propmtState } from "@/lib/store"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
|
||||||
|
const PromptInput = () => {
|
||||||
|
const app = useRecoilValue(appState)
|
||||||
|
const [prompt, setPrompt] = useRecoilState(propmtState)
|
||||||
|
const isInpainting = useRecoilValue(isInpaintingState)
|
||||||
|
|
||||||
|
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
const target = evt.target as HTMLInputElement
|
||||||
|
setPrompt(target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRepaintClick = () => {
|
||||||
|
if (prompt.length !== 0 && !app.isInpainting) {
|
||||||
|
emitter.emit(EVENT_PROMPT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !isInpainting) {
|
||||||
|
handleRepaintClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseEnter = () => {
|
||||||
|
emitter.emit(DREAM_BUTTON_MOUSE_ENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Input
|
||||||
|
className="min-w-[600px]"
|
||||||
|
value={prompt}
|
||||||
|
onInput={handleOnInput}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
placeholder="I want to repaint of..."
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRepaintClick}
|
||||||
|
disabled={prompt.length === 0 || app.isInpainting}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
>
|
||||||
|
Dream
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PromptInput
|
97
web_app/src/components/Shortcuts.tsx
Normal file
97
web_app/src/components/Shortcuts.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { Keyboard } from "lucide-react"
|
||||||
|
import useHotKey from "@/hooks/useHotkey"
|
||||||
|
import { IconButton } from "@/components/ui/button"
|
||||||
|
import { useToggle } from "@uidotdev/usehooks"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "./ui/dialog"
|
||||||
|
|
||||||
|
interface ShortcutProps {
|
||||||
|
content: string
|
||||||
|
keys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShortCut(props: ShortcutProps) {
|
||||||
|
const { content, keys } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="shortcut-description">{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 isWindows = function () {
|
||||||
|
return /windows|win32/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>
|
||||||
|
<IconButton tooltip="Hotkeys">
|
||||||
|
<Keyboard />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Hotkeys</DialogTitle>
|
||||||
|
<DialogDescription 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="Multi-Stroke Drawing"
|
||||||
|
keys={[`Hold ${CmdOrCtrl()}`]}
|
||||||
|
/>
|
||||||
|
<ShortCut content="Cancel Drawing" keys={["Esc"]} />
|
||||||
|
|
||||||
|
<ShortCut content="Rerun last mask" keys={["R"]} />
|
||||||
|
<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"]} />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Shortcuts
|
114
web_app/src/components/Workspace.tsx
Normal file
114
web_app/src/components/Workspace.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect } from "react"
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"
|
||||||
|
import Editor from "./Editor"
|
||||||
|
// import SettingModal from "./Settings/SettingsModal"
|
||||||
|
// import Toast from "./shared/Toast"
|
||||||
|
import {
|
||||||
|
AIModel,
|
||||||
|
fileState,
|
||||||
|
isPaintByExampleState,
|
||||||
|
isPix2PixState,
|
||||||
|
isSDState,
|
||||||
|
settingState,
|
||||||
|
showFileManagerState,
|
||||||
|
toastState,
|
||||||
|
} from "@/lib/store"
|
||||||
|
import {
|
||||||
|
currentModel,
|
||||||
|
getMediaFile,
|
||||||
|
modelDownloaded,
|
||||||
|
switchModel,
|
||||||
|
} from "@/lib/api"
|
||||||
|
// import SidePanel from "./SidePanel/SidePanel"
|
||||||
|
// import PESidePanel from "./SidePanel/PESidePanel"
|
||||||
|
// import FileManager from "./FileManager/FileManager"
|
||||||
|
// import P2PSidePanel from "./SidePanel/P2PSidePanel"
|
||||||
|
// import Plugins from "./Plugins/Plugins"
|
||||||
|
// import Flex from "./shared/Layout"
|
||||||
|
// import ImageSize from "./ImageSize/ImageSize"
|
||||||
|
|
||||||
|
const Workspace = () => {
|
||||||
|
const setFile = useSetRecoilState(fileState)
|
||||||
|
const [settings, setSettingState] = useRecoilState(settingState)
|
||||||
|
const [toastVal, setToastState] = useRecoilState(toastState)
|
||||||
|
const isSD = useRecoilValue(isSDState)
|
||||||
|
const isPaintByExample = useRecoilValue(isPaintByExampleState)
|
||||||
|
const isPix2Pix = useRecoilValue(isPix2PixState)
|
||||||
|
|
||||||
|
const onSettingClose = async () => {
|
||||||
|
const curModel = await currentModel().then((res) => res.text())
|
||||||
|
if (curModel === settings.model) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const downloaded = await modelDownloaded(settings.model).then((res) =>
|
||||||
|
res.text()
|
||||||
|
)
|
||||||
|
|
||||||
|
const { model } = settings
|
||||||
|
|
||||||
|
let loadingMessage = `Switching to ${model} model`
|
||||||
|
let loadingDuration = 3000
|
||||||
|
if (downloaded === "False") {
|
||||||
|
loadingMessage = `Downloading ${model} model, this may take a while`
|
||||||
|
loadingDuration = 9999999999
|
||||||
|
}
|
||||||
|
|
||||||
|
setToastState({
|
||||||
|
open: true,
|
||||||
|
desc: loadingMessage,
|
||||||
|
state: "loading",
|
||||||
|
duration: loadingDuration,
|
||||||
|
})
|
||||||
|
|
||||||
|
switchModel(model)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
setToastState({
|
||||||
|
open: true,
|
||||||
|
desc: `Switch to ${model} model success`,
|
||||||
|
state: "success",
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error("Server error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastState({
|
||||||
|
open: true,
|
||||||
|
desc: `Switch to ${model} model failed`,
|
||||||
|
state: "error",
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
setSettingState((old) => {
|
||||||
|
return { ...old, model: curModel as AIModel }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
currentModel()
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((model) => {
|
||||||
|
setSettingState((old) => {
|
||||||
|
return { ...old, model: model as AIModel }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [setSettingState])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* {isSD ? <SidePanel /> : <></>}
|
||||||
|
{isPaintByExample ? <PESidePanel /> : <></>}
|
||||||
|
{isPix2Pix ? <P2PSidePanel /> : <></>}
|
||||||
|
<Flex style={{ position: "absolute", top: 68, left: 24, gap: 12 }}>
|
||||||
|
<Plugins />
|
||||||
|
<ImageSize />
|
||||||
|
</Flex>
|
||||||
|
{/* <SettingModal onClose={onSettingClose} /> */}
|
||||||
|
<Editor />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Workspace
|
73
web_app/src/components/theme-provider.tsx
Normal file
73
web_app/src/components/theme-provider.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ThemeProviderState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
|
||||||
|
root.classList.add(systemTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme)
|
||||||
|
setTheme(theme)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
55
web_app/src/components/ui/accordion.tsx
Normal file
55
web_app/src/components/ui/accordion.tsx
Normal file
@ -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(
|
||||||
|
"flex flex-1 items-center justify-between py-4 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 }
|
123
web_app/src/components/ui/button.tsx
Normal file
123
web_app/src/components/ui/button.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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,
|
||||||
|
TooltipProvider,
|
||||||
|
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 }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export interface IconButtonProps extends ButtonProps {
|
||||||
|
tooltip: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconButton = (props: IconButtonProps) => {
|
||||||
|
const { tooltip, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" {...rest} asChild>
|
||||||
|
<div className="p-[8px]">{children}</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}>{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 }
|
120
web_app/src/components/ui/dialog.tsx
Normal file
120
web_app/src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...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-lg 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,
|
||||||
|
}
|
25
web_app/src/components/ui/input.tsx
Normal file
25
web_app/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 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}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
24
web_app/src/components/ui/label.tsx
Normal file
24
web_app/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
29
web_app/src/components/ui/popover.tsx
Normal file
29
web_app/src/components/ui/popover.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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}
|
||||||
|
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 }
|
46
web_app/src/components/ui/scroll-area.tsx
Normal file
46
web_app/src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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)}
|
||||||
|
{...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 }
|
162
web_app/src/components/ui/select.tsx
Normal file
162
web_app/src/components/ui/select.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...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}
|
||||||
|
{...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,
|
||||||
|
}
|
26
web_app/src/components/ui/slider.tsx
Normal file
26
web_app/src/components/ui/slider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
))
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Slider }
|
27
web_app/src/components/ui/switch.tsx
Normal file
27
web_app/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...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 }
|
53
web_app/src/components/ui/tabs.tsx
Normal file
53
web_app/src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...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
|
||||||
|
)}
|
||||||
|
{...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
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
24
web_app/src/components/ui/textarea.tsx
Normal file
24
web_app/src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
127
web_app/src/components/ui/toast.tsx
Normal file
127
web_app/src/components/ui/toast.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
|
)}
|
||||||
|
{...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)}
|
||||||
|
{...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
|
||||||
|
)}
|
||||||
|
{...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=""
|
||||||
|
{...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)}
|
||||||
|
{...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}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
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,
|
||||||
|
}
|
33
web_app/src/components/ui/toaster.tsx
Normal file
33
web_app/src/components/ui/toaster.tsx
Normal file
@ -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>
|
||||||
|
)
|
||||||
|
}
|
43
web_app/src/components/ui/toggle.tsx
Normal file
43
web_app/src/components/ui/toggle.tsx
Normal file
@ -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 }
|
28
web_app/src/components/ui/tooltip.tsx
Normal file
28
web_app/src/components/ui/tooltip.tsx
Normal file
@ -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 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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 }
|
192
web_app/src/components/ui/use-toast.ts
Normal file
192
web_app/src/components/ui/use-toast.ts
Normal file
@ -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 }
|
100
web_app/src/globals.css
Normal file
100
web_app/src/globals.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: 220.9 39.3% 11%;
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 224 71.4% 4.1%;
|
||||||
|
--foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--card: 224 71.4% 4.1%;
|
||||||
|
--card-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--popover: 224 71.4% 4.1%;
|
||||||
|
--popover-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--primary: 210 20% 98%;
|
||||||
|
--primary-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--muted: 215 27.9% 16.9%;
|
||||||
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
|
||||||
|
--accent: 215 27.9% 16.9%;
|
||||||
|
--accent-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--border: 215 27.9% 16.9%;
|
||||||
|
--input: 215 27.9% 16.9%;
|
||||||
|
--ring: 216 12.2% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
33
web_app/src/hooks/useAsyncMemo.tsx
Normal file
33
web_app/src/hooks/useAsyncMemo.tsx
Normal file
@ -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
|
||||||
|
}
|
22
web_app/src/hooks/useHotkey.tsx
Normal file
22
web_app/src/hooks/useHotkey.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Options, useHotkeys } from "react-hotkeys-hook"
|
||||||
|
import { useRecoilValue } from "recoil"
|
||||||
|
import { appState } from "@/lib/store"
|
||||||
|
|
||||||
|
const useHotKey = (
|
||||||
|
keys: string,
|
||||||
|
callback: any,
|
||||||
|
options?: Options,
|
||||||
|
deps?: any[]
|
||||||
|
) => {
|
||||||
|
const app = useRecoilValue(appState)
|
||||||
|
|
||||||
|
const ref = useHotkeys(
|
||||||
|
keys,
|
||||||
|
callback,
|
||||||
|
{ ...options, enabled: !app.disableShortCuts },
|
||||||
|
deps
|
||||||
|
)
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useHotKey
|
24
web_app/src/hooks/useImage.tsx
Normal file
24
web_app/src/hooks/useImage.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
function useImage(file?: File): [HTMLImageElement, boolean] {
|
||||||
|
const [image] = useState(new Image())
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (file === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
image.onload = () => {
|
||||||
|
setIsLoaded(true)
|
||||||
|
}
|
||||||
|
setIsLoaded(false)
|
||||||
|
image.src = URL.createObjectURL(file)
|
||||||
|
return () => {
|
||||||
|
image.onload = null
|
||||||
|
}
|
||||||
|
}, [file, image])
|
||||||
|
|
||||||
|
return [image, isLoaded]
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useImage }
|
34
web_app/src/hooks/useInputImage.tsx
Normal file
34
web_app/src/hooks/useInputImage.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { API_ENDPOINT } from "@/lib/api"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export default function useInputImage() {
|
||||||
|
const [inputImage, setInputImage] = useState<File>()
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [setInputImage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInputImage()
|
||||||
|
}, [fetchInputImage])
|
||||||
|
|
||||||
|
return inputImage
|
||||||
|
}
|
31
web_app/src/hooks/useResolution.tsx
Normal file
31
web_app/src/hooks/useResolution.tsx
Normal file
@ -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
|
270
web_app/src/lib/api.ts
Normal file
270
web_app/src/lib/api.ts
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import { PluginName } from "@/lib/types"
|
||||||
|
import { ControlNetMethodMap, Rect, Settings } from "@/lib/store"
|
||||||
|
import { dataURItoBlob, loadImage, srcToFile } from "@/lib/utils"
|
||||||
|
|
||||||
|
export const API_ENDPOINT = import.meta.env.VITE_BACKEND
|
||||||
|
? import.meta.env.VITE_BACKEND
|
||||||
|
: ""
|
||||||
|
|
||||||
|
export default async function inpaint(
|
||||||
|
imageFile: File,
|
||||||
|
settings: Settings,
|
||||||
|
croperRect: Rect,
|
||||||
|
prompt?: string,
|
||||||
|
negativePrompt?: string,
|
||||||
|
seed?: number,
|
||||||
|
maskBase64?: string,
|
||||||
|
customMask?: File,
|
||||||
|
paintByExampleImage?: File
|
||||||
|
) {
|
||||||
|
// 1080, 2000, Original
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("image", imageFile)
|
||||||
|
if (maskBase64 !== undefined) {
|
||||||
|
fd.append("mask", dataURItoBlob(maskBase64))
|
||||||
|
} else if (customMask !== undefined) {
|
||||||
|
fd.append("mask", customMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hdSettings = settings.hdSettings[settings.model]
|
||||||
|
fd.append("ldmSteps", settings.ldmSteps.toString())
|
||||||
|
fd.append("ldmSampler", settings.ldmSampler.toString())
|
||||||
|
fd.append("zitsWireframe", settings.zitsWireframe.toString())
|
||||||
|
fd.append("hdStrategy", hdSettings.hdStrategy)
|
||||||
|
fd.append("hdStrategyCropMargin", hdSettings.hdStrategyCropMargin.toString())
|
||||||
|
fd.append(
|
||||||
|
"hdStrategyCropTrigerSize",
|
||||||
|
hdSettings.hdStrategyCropTrigerSize.toString()
|
||||||
|
)
|
||||||
|
fd.append(
|
||||||
|
"hdStrategyResizeLimit",
|
||||||
|
hdSettings.hdStrategyResizeLimit.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
fd.append("prompt", prompt === undefined ? "" : prompt)
|
||||||
|
fd.append(
|
||||||
|
"negativePrompt",
|
||||||
|
negativePrompt === undefined ? "" : negativePrompt
|
||||||
|
)
|
||||||
|
fd.append("croperX", croperRect.x.toString())
|
||||||
|
fd.append("croperY", croperRect.y.toString())
|
||||||
|
fd.append("croperHeight", croperRect.height.toString())
|
||||||
|
fd.append("croperWidth", croperRect.width.toString())
|
||||||
|
fd.append("useCroper", settings.showCroper ? "true" : "false")
|
||||||
|
|
||||||
|
fd.append("sdMaskBlur", settings.sdMaskBlur.toString())
|
||||||
|
fd.append("sdStrength", settings.sdStrength.toString())
|
||||||
|
fd.append("sdSteps", settings.sdSteps.toString())
|
||||||
|
fd.append("sdGuidanceScale", settings.sdGuidanceScale.toString())
|
||||||
|
fd.append("sdSampler", settings.sdSampler.toString())
|
||||||
|
fd.append("sdSeed", seed ? seed.toString() : "-1")
|
||||||
|
fd.append("sdMatchHistograms", settings.sdMatchHistograms ? "true" : "false")
|
||||||
|
fd.append("sdScale", (settings.sdScale / 100).toString())
|
||||||
|
|
||||||
|
fd.append("cv2Radius", settings.cv2Radius.toString())
|
||||||
|
fd.append("cv2Flag", settings.cv2Flag.toString())
|
||||||
|
|
||||||
|
fd.append("paintByExampleSteps", settings.paintByExampleSteps.toString())
|
||||||
|
fd.append(
|
||||||
|
"paintByExampleGuidanceScale",
|
||||||
|
settings.paintByExampleGuidanceScale.toString()
|
||||||
|
)
|
||||||
|
fd.append("paintByExampleSeed", seed ? seed.toString() : "-1")
|
||||||
|
fd.append(
|
||||||
|
"paintByExampleMaskBlur",
|
||||||
|
settings.paintByExampleMaskBlur.toString()
|
||||||
|
)
|
||||||
|
fd.append(
|
||||||
|
"paintByExampleMatchHistograms",
|
||||||
|
settings.paintByExampleMatchHistograms ? "true" : "false"
|
||||||
|
)
|
||||||
|
// TODO: resize image's shortest_edge to 224 before pass to backend, save network time?
|
||||||
|
// https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPImageProcessor
|
||||||
|
if (paintByExampleImage) {
|
||||||
|
fd.append("paintByExampleImage", paintByExampleImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstructPix2Pix
|
||||||
|
fd.append("p2pSteps", settings.p2pSteps.toString())
|
||||||
|
fd.append("p2pImageGuidanceScale", settings.p2pImageGuidanceScale.toString())
|
||||||
|
fd.append("p2pGuidanceScale", settings.p2pGuidanceScale.toString())
|
||||||
|
|
||||||
|
// ControlNet
|
||||||
|
fd.append(
|
||||||
|
"controlnet_conditioning_scale",
|
||||||
|
settings.controlnetConditioningScale.toString()
|
||||||
|
)
|
||||||
|
fd.append(
|
||||||
|
"controlnet_method",
|
||||||
|
ControlNetMethodMap[settings.controlnetMethod.toString()]
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob()
|
||||||
|
const newSeed = res.headers.get("x-seed")
|
||||||
|
return { blob: URL.createObjectURL(blob), seed: newSeed }
|
||||||
|
}
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Something went wrong: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerConfig() {
|
||||||
|
return fetch(`${API_ENDPOINT}/server_config`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchModel(name: string) {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("name", name)
|
||||||
|
return fetch(`${API_ENDPOINT}/model`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentModel() {
|
||||||
|
return fetch(`${API_ENDPOINT}/model`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDesktop() {
|
||||||
|
return fetch(`${API_ENDPOINT}/is_desktop`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelDownloaded(name: string) {
|
||||||
|
return fetch(`${API_ENDPOINT}/model_downloaded/${name}`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPlugin(
|
||||||
|
name: string,
|
||||||
|
imageFile: File,
|
||||||
|
upscale?: number,
|
||||||
|
maskFile?: File | null,
|
||||||
|
clicks?: number[][]
|
||||||
|
) {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("name", name)
|
||||||
|
fd.append("image", imageFile)
|
||||||
|
if (upscale) {
|
||||||
|
fd.append("upscale", upscale.toString())
|
||||||
|
}
|
||||||
|
if (clicks) {
|
||||||
|
fd.append("clicks", JSON.stringify(clicks))
|
||||||
|
}
|
||||||
|
if (maskFile) {
|
||||||
|
fd.append("mask", maskFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_ENDPOINT}/run_plugin`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob()
|
||||||
|
return { blob: URL.createObjectURL(blob) }
|
||||||
|
}
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Something went wrong: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMediaFile(tab: string, filename: string) {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_ENDPOINT}/media/${tab}/${encodeURIComponent(filename)}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob()
|
||||||
|
const file = new File([blob], filename)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMedias(tab: string) {
|
||||||
|
const res = await fetch(`${API_ENDPOINT}/medias/${tab}`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const filenames = await res.json()
|
||||||
|
return filenames
|
||||||
|
}
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadToOutput(
|
||||||
|
image: HTMLImageElement,
|
||||||
|
filename: string,
|
||||||
|
mimeType: string
|
||||||
|
) {
|
||||||
|
const file = await srcToFile(image.src, filename, mimeType)
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("image", file)
|
||||||
|
fd.append("filename", filename)
|
||||||
|
|
||||||
|
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 makeGif(
|
||||||
|
originFile: File,
|
||||||
|
cleanImage: HTMLImageElement,
|
||||||
|
filename: string,
|
||||||
|
mimeType: string
|
||||||
|
) {
|
||||||
|
const cleanFile = await srcToFile(cleanImage.src, filename, mimeType)
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append("name", PluginName.MakeGIF)
|
||||||
|
fd.append("image", originFile)
|
||||||
|
fd.append("clean_img", cleanFile)
|
||||||
|
fd.append("filename", filename)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_ENDPOINT}/run_plugin`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await res.blob()
|
||||||
|
const newImage = new Image()
|
||||||
|
await loadImage(newImage, URL.createObjectURL(blob))
|
||||||
|
return newImage
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Something went wrong: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
22
web_app/src/lib/event.ts
Normal file
22
web_app/src/lib/event.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import mitt from "mitt"
|
||||||
|
|
||||||
|
export const EVENT_PROMPT = "prompt"
|
||||||
|
|
||||||
|
export const EVENT_CUSTOM_MASK = "custom_mask"
|
||||||
|
export interface CustomMaskEventData {
|
||||||
|
mask: File
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EVENT_PAINT_BY_EXAMPLE = "paint_by_example"
|
||||||
|
export interface PaintByExampleEventData {
|
||||||
|
image: File
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RERUN_LAST_MASK = "rerun_last_mask"
|
||||||
|
|
||||||
|
export const DREAM_BUTTON_MOUSE_ENTER = "dream_button_mouse_enter"
|
||||||
|
export const DREAM_BUTTON_MOUSE_LEAVE = "dream_btoon_mouse_leave"
|
||||||
|
|
||||||
|
const emitter = mitt()
|
||||||
|
|
||||||
|
export default emitter
|
902
web_app/src/lib/store.ts
Normal file
902
web_app/src/lib/store.ts
Normal file
@ -0,0 +1,902 @@
|
|||||||
|
import { atom, selector } from "recoil"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
|
export enum HDStrategy {
|
||||||
|
ORIGINAL = "Original",
|
||||||
|
RESIZE = "Resize",
|
||||||
|
CROP = "Crop",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LDMSampler {
|
||||||
|
ddim = "ddim",
|
||||||
|
plms = "plms",
|
||||||
|
}
|
||||||
|
|
||||||
|
function strEnum<T extends string>(o: Array<T>): { [K in T]: K } {
|
||||||
|
return o.reduce((res, key) => {
|
||||||
|
res[key] = key
|
||||||
|
return res
|
||||||
|
}, Object.create(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AIModel {
|
||||||
|
LAMA = "lama",
|
||||||
|
LDM = "ldm",
|
||||||
|
ZITS = "zits",
|
||||||
|
MAT = "mat",
|
||||||
|
FCF = "fcf",
|
||||||
|
SD15 = "sd1.5",
|
||||||
|
ANYTHING4 = "anything4",
|
||||||
|
REALISTIC_VISION_1_4 = "realisticVision1.4",
|
||||||
|
SD2 = "sd2",
|
||||||
|
CV2 = "cv2",
|
||||||
|
Mange = "manga",
|
||||||
|
PAINT_BY_EXAMPLE = "paint_by_example",
|
||||||
|
PIX2PIX = "instruct_pix2pix",
|
||||||
|
KANDINSKY22 = "kandinsky2.2",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ControlNetMethod {
|
||||||
|
canny = "canny",
|
||||||
|
inpaint = "inpaint",
|
||||||
|
openpose = "openpose",
|
||||||
|
depth = "depth",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ControlNetMethodMap: any = {
|
||||||
|
canny: "control_v11p_sd15_canny",
|
||||||
|
inpaint: "control_v11p_sd15_inpaint",
|
||||||
|
openpose: "control_v11p_sd15_openpose",
|
||||||
|
depth: "control_v11f1p_sd15_depth",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ControlNetMethodMap2: any = {
|
||||||
|
control_v11p_sd15_canny: "canny",
|
||||||
|
control_v11p_sd15_inpaint: "inpaint",
|
||||||
|
control_v11p_sd15_openpose: "openpose",
|
||||||
|
control_v11f1p_sd15_depth: "depth",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const maskState = atom<File | undefined>({
|
||||||
|
key: "maskState",
|
||||||
|
default: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const paintByExampleImageState = atom<File | undefined>({
|
||||||
|
key: "paintByExampleImageState",
|
||||||
|
default: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface Rect {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
file: File | undefined
|
||||||
|
imageHeight: number
|
||||||
|
imageWidth: number
|
||||||
|
disableShortCuts: boolean
|
||||||
|
isInpainting: boolean
|
||||||
|
isDisableModelSwitch: boolean
|
||||||
|
isEnableAutoSaving: boolean
|
||||||
|
isInteractiveSeg: boolean
|
||||||
|
isInteractiveSegRunning: boolean
|
||||||
|
interactiveSegClicks: number[][]
|
||||||
|
enableFileManager: boolean
|
||||||
|
gifImage: HTMLImageElement | undefined
|
||||||
|
brushSize: number
|
||||||
|
isControlNet: boolean
|
||||||
|
controlNetMethod: string
|
||||||
|
plugins: string[]
|
||||||
|
isPluginRunning: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appState = atom<AppState>({
|
||||||
|
key: "appState",
|
||||||
|
default: {
|
||||||
|
file: undefined,
|
||||||
|
imageHeight: 0,
|
||||||
|
imageWidth: 0,
|
||||||
|
disableShortCuts: false,
|
||||||
|
isInpainting: false,
|
||||||
|
isDisableModelSwitch: false,
|
||||||
|
isEnableAutoSaving: false,
|
||||||
|
isInteractiveSeg: false,
|
||||||
|
isInteractiveSegRunning: false,
|
||||||
|
interactiveSegClicks: [],
|
||||||
|
enableFileManager: false,
|
||||||
|
gifImage: undefined,
|
||||||
|
brushSize: 40,
|
||||||
|
isControlNet: false,
|
||||||
|
controlNetMethod: ControlNetMethod.canny,
|
||||||
|
plugins: [],
|
||||||
|
isPluginRunning: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const propmtState = atom<string>({
|
||||||
|
key: "promptState",
|
||||||
|
default: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const negativePropmtState = atom<string>({
|
||||||
|
key: "negativePromptState",
|
||||||
|
default: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isInpaintingState = selector({
|
||||||
|
key: "isInpainting",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isInpainting
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isInpainting: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isPluginRunningState = selector({
|
||||||
|
key: "isPluginRunningState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isPluginRunning
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isPluginRunning: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverConfigState = selector({
|
||||||
|
key: "serverConfigState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return {
|
||||||
|
isControlNet: app.isControlNet,
|
||||||
|
controlNetMethod: app.controlNetMethod,
|
||||||
|
isDisableModelSwitchState: app.isDisableModelSwitch,
|
||||||
|
isEnableAutoSaving: app.isEnableAutoSaving,
|
||||||
|
enableFileManager: app.enableFileManager,
|
||||||
|
plugins: app.plugins,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
const methodShortName = ControlNetMethodMap2[newValue.controlNetMethod]
|
||||||
|
set(appState, { ...app, ...newValue, controlnetMethod: methodShortName })
|
||||||
|
|
||||||
|
const setting = get(settingState)
|
||||||
|
set(settingState, {
|
||||||
|
...setting,
|
||||||
|
controlnetMethod: methodShortName,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const brushSizeState = selector({
|
||||||
|
key: "brushSizeState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.brushSize
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, brushSize: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const imageHeightState = selector({
|
||||||
|
key: "imageHeightState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.imageHeight
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, imageHeight: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const imageWidthState = selector({
|
||||||
|
key: "imageWidthState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.imageWidth
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, imageWidth: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const enableFileManagerState = selector({
|
||||||
|
key: "enableFileManagerState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.enableFileManager
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, enableFileManager: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const gifImageState = selector({
|
||||||
|
key: "gifImageState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.gifImage
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, gifImage: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileState = selector({
|
||||||
|
key: "fileState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.file
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, {
|
||||||
|
...app,
|
||||||
|
file: newValue,
|
||||||
|
interactiveSegClicks: [],
|
||||||
|
isInteractiveSeg: false,
|
||||||
|
isInteractiveSegRunning: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const setting = get(settingState)
|
||||||
|
set(settingState, {
|
||||||
|
...setting,
|
||||||
|
sdScale: 100,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isInteractiveSegState = selector({
|
||||||
|
key: "isInteractiveSegState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isInteractiveSeg
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isInteractiveSeg: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isInteractiveSegRunningState = selector({
|
||||||
|
key: "isInteractiveSegRunningState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isInteractiveSegRunning
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isInteractiveSegRunning: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isProcessingState = selector({
|
||||||
|
key: "isProcessingState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return (
|
||||||
|
app.isInteractiveSegRunning || app.isPluginRunning || app.isInpainting
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const interactiveSegClicksState = selector({
|
||||||
|
key: "interactiveSegClicksState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.interactiveSegClicks
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, interactiveSegClicks: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isDisableModelSwitchState = selector({
|
||||||
|
key: "isDisableModelSwitchState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isDisableModelSwitch
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isDisableModelSwitch: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isControlNetState = selector({
|
||||||
|
key: "isControlNetState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isControlNet
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isControlNet: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isEnableAutoSavingState = selector({
|
||||||
|
key: "isEnableAutoSavingState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.isEnableAutoSaving
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, isEnableAutoSaving: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const croperState = atom<Rect>({
|
||||||
|
key: "croperState",
|
||||||
|
default: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SIDE_PANEL_TAB = strEnum(["inpainting", "outpainting"])
|
||||||
|
export type SIDE_PANEL_TAB_TYPE = keyof typeof SIDE_PANEL_TAB
|
||||||
|
|
||||||
|
export interface SidePanelState {
|
||||||
|
tab: SIDE_PANEL_TAB_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sidePanelTabState = atom<SidePanelState>({
|
||||||
|
key: "sidePanelTabState",
|
||||||
|
default: {
|
||||||
|
tab: SIDE_PANEL_TAB.inpainting,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const croperX = selector({
|
||||||
|
key: "croperX",
|
||||||
|
get: ({ get }) => get(croperState).x,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(croperState)
|
||||||
|
set(croperState, { ...rect, x: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const croperY = selector({
|
||||||
|
key: "croperY",
|
||||||
|
get: ({ get }) => get(croperState).y,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(croperState)
|
||||||
|
set(croperState, { ...rect, y: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const croperHeight = selector({
|
||||||
|
key: "croperHeight",
|
||||||
|
get: ({ get }) => get(croperState).height,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(croperState)
|
||||||
|
set(croperState, { ...rect, height: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const croperWidth = selector({
|
||||||
|
key: "croperWidth",
|
||||||
|
get: ({ get }) => get(croperState).width,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(croperState)
|
||||||
|
set(croperState, { ...rect, width: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const extenderState = atom<Rect>({
|
||||||
|
key: "extenderState",
|
||||||
|
default: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const extenderX = selector({
|
||||||
|
key: "extenderX",
|
||||||
|
get: ({ get }) => get(extenderState).x,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(extenderState)
|
||||||
|
set(extenderState, { ...rect, x: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const extenderY = selector({
|
||||||
|
key: "extenderY",
|
||||||
|
get: ({ get }) => get(extenderState).y,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(extenderState)
|
||||||
|
set(extenderState, { ...rect, y: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const extenderHeight = selector({
|
||||||
|
key: "extenderHeight",
|
||||||
|
get: ({ get }) => get(extenderState).height,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(extenderState)
|
||||||
|
set(extenderState, { ...rect, height: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const extenderWidth = selector({
|
||||||
|
key: "extenderWidth",
|
||||||
|
get: ({ get }) => get(extenderState).width,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const rect = get(extenderState)
|
||||||
|
set(extenderState, { ...rect, width: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ToastAtomState {
|
||||||
|
open: boolean
|
||||||
|
desc: string
|
||||||
|
state: ToastState
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toastState = atom<ToastAtomState>({
|
||||||
|
key: "toastState",
|
||||||
|
default: {
|
||||||
|
open: false,
|
||||||
|
desc: "",
|
||||||
|
state: "default",
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const shortcutsState = atom<boolean>({
|
||||||
|
key: "shortcutsState",
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface HDSettings {
|
||||||
|
hdStrategy: HDStrategy
|
||||||
|
hdStrategyResizeLimit: number
|
||||||
|
hdStrategyCropTrigerSize: number
|
||||||
|
hdStrategyCropMargin: number
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelsHDSettings = { [key in AIModel]: HDSettings }
|
||||||
|
|
||||||
|
export enum CV2Flag {
|
||||||
|
INPAINT_NS = "INPAINT_NS",
|
||||||
|
INPAINT_TELEA = "INPAINT_TELEA",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
show: boolean
|
||||||
|
showCroper: boolean
|
||||||
|
downloadMask: boolean
|
||||||
|
graduallyInpainting: boolean
|
||||||
|
runInpaintingManually: boolean
|
||||||
|
model: AIModel
|
||||||
|
hdSettings: ModelsHDSettings
|
||||||
|
|
||||||
|
// For LDM
|
||||||
|
ldmSteps: number
|
||||||
|
ldmSampler: LDMSampler
|
||||||
|
|
||||||
|
// For ZITS
|
||||||
|
zitsWireframe: boolean
|
||||||
|
|
||||||
|
// For SD
|
||||||
|
sdMaskBlur: number
|
||||||
|
sdMode: SDMode
|
||||||
|
sdStrength: number
|
||||||
|
sdSteps: number
|
||||||
|
sdGuidanceScale: number
|
||||||
|
sdSampler: SDSampler
|
||||||
|
sdSeed: number
|
||||||
|
sdSeedFixed: boolean // true: use sdSeed, false: random generate seed on backend
|
||||||
|
sdNumSamples: number
|
||||||
|
sdMatchHistograms: boolean
|
||||||
|
sdScale: number
|
||||||
|
|
||||||
|
// For OpenCV2
|
||||||
|
cv2Radius: number
|
||||||
|
cv2Flag: CV2Flag
|
||||||
|
|
||||||
|
// Paint by Example
|
||||||
|
paintByExampleSteps: number
|
||||||
|
paintByExampleGuidanceScale: number
|
||||||
|
paintByExampleSeed: number
|
||||||
|
paintByExampleSeedFixed: boolean
|
||||||
|
paintByExampleMaskBlur: number
|
||||||
|
paintByExampleMatchHistograms: boolean
|
||||||
|
|
||||||
|
// InstructPix2Pix
|
||||||
|
p2pSteps: number
|
||||||
|
p2pImageGuidanceScale: number
|
||||||
|
p2pGuidanceScale: number
|
||||||
|
|
||||||
|
// ControlNet
|
||||||
|
controlnetConditioningScale: number
|
||||||
|
controlnetMethod: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHDSettings: ModelsHDSettings = {
|
||||||
|
[AIModel.LAMA]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 2048,
|
||||||
|
hdStrategyCropTrigerSize: 800,
|
||||||
|
hdStrategyCropMargin: 196,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.LDM]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 1080,
|
||||||
|
hdStrategyCropTrigerSize: 1080,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.ZITS]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 1024,
|
||||||
|
hdStrategyCropTrigerSize: 1024,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.MAT]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 1024,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.FCF]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 512,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.SD15]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.ANYTHING4]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.REALISTIC_VISION_1_4]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.SD2]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.PAINT_BY_EXAMPLE]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.PIX2PIX]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
[AIModel.Mange]: {
|
||||||
|
hdStrategy: HDStrategy.CROP,
|
||||||
|
hdStrategyResizeLimit: 1280,
|
||||||
|
hdStrategyCropTrigerSize: 1024,
|
||||||
|
hdStrategyCropMargin: 196,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.CV2]: {
|
||||||
|
hdStrategy: HDStrategy.RESIZE,
|
||||||
|
hdStrategyResizeLimit: 1080,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[AIModel.KANDINSKY22]: {
|
||||||
|
hdStrategy: HDStrategy.ORIGINAL,
|
||||||
|
hdStrategyResizeLimit: 768,
|
||||||
|
hdStrategyCropTrigerSize: 512,
|
||||||
|
hdStrategyCropMargin: 128,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SDSampler {
|
||||||
|
ddim = "ddim",
|
||||||
|
pndm = "pndm",
|
||||||
|
klms = "k_lms",
|
||||||
|
kEuler = "k_euler",
|
||||||
|
kEulerA = "k_euler_a",
|
||||||
|
dpmPlusPlus = "dpm++",
|
||||||
|
uni_pc = "uni_pc",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SDMode {
|
||||||
|
text2img = "text2img",
|
||||||
|
img2img = "img2img",
|
||||||
|
inpainting = "inpainting",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingStateDefault: Settings = {
|
||||||
|
show: false,
|
||||||
|
showCroper: false,
|
||||||
|
downloadMask: false,
|
||||||
|
graduallyInpainting: true,
|
||||||
|
runInpaintingManually: false,
|
||||||
|
model: AIModel.LAMA,
|
||||||
|
hdSettings: defaultHDSettings,
|
||||||
|
|
||||||
|
ldmSteps: 25,
|
||||||
|
ldmSampler: LDMSampler.plms,
|
||||||
|
|
||||||
|
zitsWireframe: true,
|
||||||
|
|
||||||
|
// SD
|
||||||
|
sdMaskBlur: 5,
|
||||||
|
sdMode: SDMode.inpainting,
|
||||||
|
sdStrength: 0.75,
|
||||||
|
sdSteps: 50,
|
||||||
|
sdGuidanceScale: 7.5,
|
||||||
|
sdSampler: SDSampler.uni_pc,
|
||||||
|
sdSeed: 42,
|
||||||
|
sdSeedFixed: false,
|
||||||
|
sdNumSamples: 1,
|
||||||
|
sdMatchHistograms: false,
|
||||||
|
sdScale: 100,
|
||||||
|
|
||||||
|
// CV2
|
||||||
|
cv2Radius: 5,
|
||||||
|
cv2Flag: CV2Flag.INPAINT_NS,
|
||||||
|
|
||||||
|
// Paint by Example
|
||||||
|
paintByExampleSteps: 50,
|
||||||
|
paintByExampleGuidanceScale: 7.5,
|
||||||
|
paintByExampleSeed: 42,
|
||||||
|
paintByExampleMaskBlur: 5,
|
||||||
|
paintByExampleSeedFixed: false,
|
||||||
|
paintByExampleMatchHistograms: false,
|
||||||
|
|
||||||
|
// InstructPix2Pix
|
||||||
|
p2pSteps: 50,
|
||||||
|
p2pImageGuidanceScale: 1.5,
|
||||||
|
p2pGuidanceScale: 7.5,
|
||||||
|
|
||||||
|
// ControlNet
|
||||||
|
controlnetConditioningScale: 0.4,
|
||||||
|
controlnetMethod: ControlNetMethod.canny,
|
||||||
|
}
|
||||||
|
|
||||||
|
const localStorageEffect =
|
||||||
|
(key: string) =>
|
||||||
|
({ setSelf, onSet }: any) => {
|
||||||
|
const savedValue = localStorage.getItem(key)
|
||||||
|
if (savedValue != null) {
|
||||||
|
const storageSettings = JSON.parse(savedValue)
|
||||||
|
storageSettings.show = false
|
||||||
|
|
||||||
|
const restored = _.merge(
|
||||||
|
_.cloneDeep(settingStateDefault),
|
||||||
|
storageSettings
|
||||||
|
)
|
||||||
|
setSelf(restored)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSet((newValue: Settings, val: string, isReset: boolean) =>
|
||||||
|
isReset
|
||||||
|
? localStorage.removeItem(key)
|
||||||
|
: localStorage.setItem(key, JSON.stringify(newValue))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROOT_STATE_KEY = "settingsState4"
|
||||||
|
// Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized
|
||||||
|
// https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence
|
||||||
|
export const settingState = atom<Settings>({
|
||||||
|
key: ROOT_STATE_KEY,
|
||||||
|
default: settingStateDefault,
|
||||||
|
effects: [localStorageEffect(ROOT_STATE_KEY)],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const seedState = selector({
|
||||||
|
key: "seed",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
switch (settings.model) {
|
||||||
|
case AIModel.PAINT_BY_EXAMPLE:
|
||||||
|
return settings.paintByExampleSeedFixed
|
||||||
|
? settings.paintByExampleSeed
|
||||||
|
: -1
|
||||||
|
default:
|
||||||
|
return settings.sdSeedFixed ? settings.sdSeed : -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
switch (settings.model) {
|
||||||
|
case AIModel.PAINT_BY_EXAMPLE:
|
||||||
|
if (!settings.paintByExampleSeedFixed) {
|
||||||
|
set(settingState, { ...settings, paintByExampleSeed: newValue })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (!settings.sdSeedFixed) {
|
||||||
|
set(settingState, { ...settings, sdSeed: newValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hdSettingsState = selector({
|
||||||
|
key: "hdSettings",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
return settings.hdSettings[settings.model]
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
const hdSettings = settings.hdSettings[settings.model]
|
||||||
|
const newHDSettings = { ...hdSettings, ...newValue }
|
||||||
|
|
||||||
|
set(settingState, {
|
||||||
|
...settings,
|
||||||
|
hdSettings: { ...settings.hdSettings, [settings.model]: newHDSettings },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isSDState = selector({
|
||||||
|
key: "isSD",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
return (
|
||||||
|
settings.model === AIModel.SD15 ||
|
||||||
|
settings.model === AIModel.SD2 ||
|
||||||
|
settings.model === AIModel.ANYTHING4 ||
|
||||||
|
settings.model === AIModel.REALISTIC_VISION_1_4 ||
|
||||||
|
settings.model === AIModel.KANDINSKY22
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isPaintByExampleState = selector({
|
||||||
|
key: "isPaintByExampleState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
return settings.model === AIModel.PAINT_BY_EXAMPLE
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isPix2PixState = selector({
|
||||||
|
key: "isPix2PixState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
return settings.model === AIModel.PIX2PIX
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const runManuallyState = selector({
|
||||||
|
key: "runManuallyState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const settings = get(settingState)
|
||||||
|
const isSD = get(isSDState)
|
||||||
|
const isPaintByExample = get(isPaintByExampleState)
|
||||||
|
const isPix2Pix = get(isPix2PixState)
|
||||||
|
return (
|
||||||
|
settings.runInpaintingManually || isSD || isPaintByExample || isPix2Pix
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isDiffusionModelsState = selector({
|
||||||
|
key: "isDiffusionModelsState",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const isSD = get(isSDState)
|
||||||
|
const isPaintByExample = get(isPaintByExampleState)
|
||||||
|
const isPix2Pix = get(isPix2PixState)
|
||||||
|
return isSD || isPaintByExample || isPix2Pix
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export enum SortBy {
|
||||||
|
NAME = "name",
|
||||||
|
CTIME = "ctime",
|
||||||
|
MTIME = "mtime",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortOrder {
|
||||||
|
DESCENDING = "desc",
|
||||||
|
ASCENDING = "asc",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileManagerState {
|
||||||
|
sortBy: SortBy
|
||||||
|
sortOrder: SortOrder
|
||||||
|
layout: "rows" | "masonry"
|
||||||
|
searchText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILE_MANAGER_STATE_KEY = "fileManagerState"
|
||||||
|
|
||||||
|
export const fileManagerState = atom<FileManagerState>({
|
||||||
|
key: FILE_MANAGER_STATE_KEY,
|
||||||
|
default: {
|
||||||
|
sortBy: SortBy.CTIME,
|
||||||
|
sortOrder: SortOrder.DESCENDING,
|
||||||
|
layout: "masonry",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
effects: [localStorageEffect(FILE_MANAGER_STATE_KEY)],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileManagerSortBy = selector({
|
||||||
|
key: "fileManagerSortBy",
|
||||||
|
get: ({ get }) => get(fileManagerState).sortBy,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const val = get(fileManagerState)
|
||||||
|
set(fileManagerState, { ...val, sortBy: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileManagerSortOrder = selector({
|
||||||
|
key: "fileManagerSortOrder",
|
||||||
|
get: ({ get }) => get(fileManagerState).sortOrder,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const val = get(fileManagerState)
|
||||||
|
set(fileManagerState, { ...val, sortOrder: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileManagerLayout = selector({
|
||||||
|
key: "fileManagerLayout",
|
||||||
|
get: ({ get }) => get(fileManagerState).layout,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const val = get(fileManagerState)
|
||||||
|
set(fileManagerState, { ...val, layout: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileManagerSearchText = selector({
|
||||||
|
key: "fileManagerSearchText",
|
||||||
|
get: ({ get }) => get(fileManagerState).searchText,
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const val = get(fileManagerState)
|
||||||
|
set(fileManagerState, { ...val, searchText: newValue })
|
||||||
|
},
|
||||||
|
})
|
9
web_app/src/lib/types.ts
Normal file
9
web_app/src/lib/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export enum PluginName {
|
||||||
|
RemoveBG = "RemoveBG",
|
||||||
|
AnimeSeg = "AnimeSeg",
|
||||||
|
RealESRGAN = "RealESRGAN",
|
||||||
|
GFPGAN = "GFPGAN",
|
||||||
|
RestoreFormer = "RestoreFormer",
|
||||||
|
InteractiveSeg = "InteractiveSeg",
|
||||||
|
MakeGIF = "MakeGIF",
|
||||||
|
}
|
133
web_app/src/lib/utils.ts
Normal file
133
web_app/src/lib/utils.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { SyntheticEvent } from "react"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
16
web_app/src/main.tsx
Normal file
16
web_app/src/main.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from "react"
|
||||||
|
import ReactDOM from "react-dom/client"
|
||||||
|
import { RecoilRoot } from "recoil"
|
||||||
|
import App from "./App.tsx"
|
||||||
|
import "./globals.css"
|
||||||
|
import { ThemeProvider } from "./components/theme-provider.tsx"
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
|
<RecoilRoot>
|
||||||
|
<App />
|
||||||
|
</RecoilRoot>
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
1
web_app/src/vite-env.d.ts
vendored
Normal file
1
web_app/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
76
web_app/tailwind.config.js
Normal file
76
web_app/tailwind.config.js
Normal file
@ -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")],
|
||||||
|
}
|
32
web_app/tsconfig.json
Normal file
32
web_app/tsconfig.json
Normal file
@ -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" }]
|
||||||
|
}
|
10
web_app/tsconfig.node.json
Normal file
10
web_app/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
12
web_app/vite.config.ts
Normal file
12
web_app/vite.config.ts
Normal file
@ -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
Block a user