diff --git a/custom-demo/front-end/package-lock.json b/custom-demo/front-end/package-lock.json
index a6e54bf..a63a9ef 100644
--- a/custom-demo/front-end/package-lock.json
+++ b/custom-demo/front-end/package-lock.json
@@ -10,6 +10,9 @@
       "dependencies": {
         "@heroicons/react": "^2.0.18",
         "@hookform/resolvers": "^3.3.2",
+        "@mantine/core": "^8.2.1",
+        "@mantine/hooks": "^8.2.1",
+        "@mantine/notifications": "^8.2.1",
         "@radix-ui/react-accordion": "^1.1.2",
         "@radix-ui/react-alert-dialog": "^1.0.5",
         "@radix-ui/react-context-menu": "^2.1.5",
@@ -1043,28 +1046,46 @@
       }
     },
     "node_modules/@floating-ui/core": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
-      "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+      "license": "MIT",
       "dependencies": {
-        "@floating-ui/utils": "^0.1.3"
+        "@floating-ui/utils": "^0.2.10"
       }
     },
     "node_modules/@floating-ui/dom": {
-      "version": "1.5.3",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
-      "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
+      "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
+      "license": "MIT",
       "dependencies": {
-        "@floating-ui/core": "^1.4.2",
-        "@floating-ui/utils": "^0.1.3"
+        "@floating-ui/core": "^1.7.3",
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/react": {
+      "version": "0.26.28",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
+      "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/react-dom": "^2.1.2",
+        "@floating-ui/utils": "^0.2.8",
+        "tabbable": "^6.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
       }
     },
     "node_modules/@floating-ui/react-dom": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz",
-      "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==",
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
+      "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
+      "license": "MIT",
       "dependencies": {
-        "@floating-ui/dom": "^1.5.1"
+        "@floating-ui/dom": "^1.7.3"
       },
       "peerDependencies": {
         "react": ">=16.8.0",
@@ -1072,9 +1093,10 @@
       }
     },
     "node_modules/@floating-ui/utils": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
-      "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+      "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+      "license": "MIT"
     },
     "node_modules/@heroicons/react": {
       "version": "2.0.18",
@@ -1168,6 +1190,105 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@mantine/core": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.2.2.tgz",
+      "integrity": "sha512-+WnqII3zSD72F+7GLcRXZ/MyO4r7A4JM/yWkCSclxR4LeRQ5bd4HBRXkvXRMZP28UeL2b5X9Re2Sig3KVGDBeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/react": "^0.26.28",
+        "clsx": "^2.1.1",
+        "react-number-format": "^5.4.3",
+        "react-remove-scroll": "^2.6.2",
+        "react-textarea-autosize": "8.5.9",
+        "type-fest": "^4.27.0"
+      },
+      "peerDependencies": {
+        "@mantine/hooks": "8.2.2",
+        "react": "^18.x || ^19.x",
+        "react-dom": "^18.x || ^19.x"
+      }
+    },
+    "node_modules/@mantine/core/node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@mantine/core/node_modules/react-remove-scroll": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+      "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+      "license": "MIT",
+      "dependencies": {
+        "react-remove-scroll-bar": "^2.3.7",
+        "react-style-singleton": "^2.2.3",
+        "tslib": "^2.1.0",
+        "use-callback-ref": "^1.3.3",
+        "use-sidecar": "^1.1.3"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mantine/core/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@mantine/hooks": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.2.tgz",
+      "integrity": "sha512-fjt0+pc1UxJIIUswu4ur72qVH+/UoFxyYmqWexuHJTOvuB86M//KUvXpFyhJcTdEENBHg2k1fyMpWmgg1VOZ5w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^18.x || ^19.x"
+      }
+    },
+    "node_modules/@mantine/notifications": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.2.2.tgz",
+      "integrity": "sha512-XThhzNomF6vQtGqEnjCP0nidP34uSTmNLDkyo7ITqVEe9/KAMqf6PcWZ20xShokVtXGE/L9fq1jOhdCZ9eLVxg==",
+      "license": "MIT",
+      "dependencies": {
+        "@mantine/store": "8.2.2",
+        "react-transition-group": "4.4.5"
+      },
+      "peerDependencies": {
+        "@mantine/core": "8.2.2",
+        "@mantine/hooks": "8.2.2",
+        "react": "^18.x || ^19.x",
+        "react-dom": "^18.x || ^19.x"
+      }
+    },
+    "node_modules/@mantine/store": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.2.2.tgz",
+      "integrity": "sha512-4uvXAuCxPCOLRBgyy0tuIhm8cWsX8odcxVSc6lNWT5K0rT04gvB96I27MWThyGGLqB/BfON3VcBZ1dIMzt7k7w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^18.x || ^19.x"
+      }
+    },
     "node_modules/@next/env": {
       "version": "14.0.3",
       "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz",
@@ -2643,9 +2764,10 @@
       ]
     },
     "node_modules/@socket.io/component-emitter": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
-      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
     },
     "node_modules/@swc/core": {
       "version": "1.3.96",
@@ -3929,6 +4051,16 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.4.588",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz",
@@ -3936,21 +4068,23 @@
       "dev": true
     },
     "node_modules/engine.io-client": {
-      "version": "6.5.3",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
-      "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+      "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1",
         "engine.io-parser": "~5.2.1",
-        "ws": "~8.11.0",
-        "xmlhttprequest-ssl": "~2.0.0"
+        "ws": "~8.17.1",
+        "xmlhttprequest-ssl": "~2.1.1"
       }
     },
     "node_modules/engine.io-parser": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
-      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       }
@@ -4492,7 +4626,8 @@
     "node_modules/hamt_plus": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
-      "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
+      "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==",
+      "license": "MIT"
     },
     "node_modules/has-flag": {
       "version": "4.0.0",
@@ -4593,14 +4728,6 @@
         "node": ">=20.0.0"
       }
     },
-    "node_modules/invariant": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
-      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
-      "dependencies": {
-        "loose-envify": "^1.0.0"
-      }
-    },
     "node_modules/is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -5327,6 +5454,17 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -5407,6 +5545,22 @@
         "react-dom": ">=16.8.1"
       }
     },
+    "node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "license": "MIT"
+    },
+    "node_modules/react-number-format": {
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz",
+      "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/react-photo-album": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/react-photo-album/-/react-photo-album-2.3.0.tgz",
@@ -5452,19 +5606,20 @@
       }
     },
     "node_modules/react-remove-scroll-bar": {
-      "version": "2.3.4",
-      "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
-      "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+      "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+      "license": "MIT",
       "dependencies": {
-        "react-style-singleton": "^2.2.1",
+        "react-style-singleton": "^2.2.2",
         "tslib": "^2.0.0"
       },
       "engines": {
         "node": ">=10"
       },
       "peerDependencies": {
-        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -5473,20 +5628,20 @@
       }
     },
     "node_modules/react-style-singleton": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
-      "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+      "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+      "license": "MIT",
       "dependencies": {
         "get-nonce": "^1.0.0",
-        "invariant": "^2.2.4",
         "tslib": "^2.0.0"
       },
       "engines": {
         "node": ">=10"
       },
       "peerDependencies": {
-        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -5494,6 +5649,39 @@
         }
       }
     },
+    "node_modules/react-textarea-autosize": {
+      "version": "8.5.9",
+      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
+      "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.13",
+        "use-composed-ref": "^1.3.0",
+        "use-latest": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
     "node_modules/react-universal-interface": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@@ -5564,6 +5752,7 @@
       "version": "0.7.7",
       "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
       "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
+      "license": "MIT",
       "dependencies": {
         "hamt_plus": "1.0.2"
       },
@@ -5769,13 +5958,14 @@
       }
     },
     "node_modules/socket.io-client": {
-      "version": "4.7.2",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
-      "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+      "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.2",
-        "engine.io-client": "~6.5.2",
+        "engine.io-client": "~6.6.1",
         "socket.io-parser": "~4.2.4"
       },
       "engines": {
@@ -5786,6 +5976,7 @@
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
       "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "license": "MIT",
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
         "debug": "~4.3.1"
@@ -5974,6 +6165,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/tabbable": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+      "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+      "license": "MIT"
+    },
     "node_modules/tailwind-merge": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.0.0.tgz",
@@ -6219,9 +6416,10 @@
       }
     },
     "node_modules/use-callback-ref": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
-      "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+      "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+      "license": "MIT",
       "dependencies": {
         "tslib": "^2.0.0"
       },
@@ -6229,8 +6427,53 @@
         "node": ">=10"
       },
       "peerDependencies": {
-        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/use-composed-ref": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
+      "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
+      "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/use-latest": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
+      "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "use-isomorphic-layout-effect": "^1.1.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -6239,9 +6482,10 @@
       }
     },
     "node_modules/use-sidecar": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
-      "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+      "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+      "license": "MIT",
       "dependencies": {
         "detect-node-es": "^1.1.0",
         "tslib": "^2.0.0"
@@ -6250,8 +6494,8 @@
         "node": ">=10"
       },
       "peerDependencies": {
-        "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -6361,15 +6605,16 @@
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
     "node_modules/ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
       },
       "peerDependencies": {
         "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
+        "utf-8-validate": ">=5.0.2"
       },
       "peerDependenciesMeta": {
         "bufferutil": {
@@ -6381,9 +6626,9 @@
       }
     },
     "node_modules/xmlhttprequest-ssl": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
-      "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+      "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
       "engines": {
         "node": ">=0.4.0"
       }
@@ -6415,23 +6660,30 @@
       }
     },
     "node_modules/zod": {
-      "version": "3.22.4",
-      "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
-      "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }
     },
     "node_modules/zundo": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/zundo/-/zundo-2.0.0.tgz",
-      "integrity": "sha512-XzKDyunmyxvQHKDjgTmOClOQscJAm5NAa1iEazR0DilvV/uwCjnDwlHJuJ+GmG/oj5RMjzsD0ptghZzjEj1w4g==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/zundo/-/zundo-2.3.0.tgz",
+      "integrity": "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ==",
+      "license": "MIT",
       "funding": {
         "type": "individual",
         "url": "https://github.com/sponsors/charkour"
       },
       "peerDependencies": {
-        "zustand": "^4.3.0"
+        "zustand": "^4.3.0 || ^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "zustand": {
+          "optional": false
+        }
       }
     },
     "node_modules/zustand": {
diff --git a/custom-demo/front-end/package.json b/custom-demo/front-end/package.json
index ba5cb30..ee912b7 100644
--- a/custom-demo/front-end/package.json
+++ b/custom-demo/front-end/package.json
@@ -10,6 +10,9 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@mantine/core": "^8.2.1",
+		"@mantine/hooks": "^8.2.1",
+		"@mantine/notifications": "^8.2.1",
     "@heroicons/react": "^2.0.18",
     "@hookform/resolvers": "^3.3.2",
     "@radix-ui/react-accordion": "^1.1.2",
diff --git a/custom-demo/front-end/src/App.tsx b/custom-demo/front-end/src/App.tsx
index 7b57f6a..c83b794 100644
--- a/custom-demo/front-end/src/App.tsx
+++ b/custom-demo/front-end/src/App.tsx
@@ -1,168 +1,10 @@
-import { useCallback, useEffect, useRef } from "react";
+import { UploadDraw } from "./features/upload-draw";
 
-import useInputImage from "@/hooks/useInputImage";
-import { keepGUIAlive } from "@/lib/utils";
-import { getServerConfig } from "@/lib/api";
-import Header from "@/components/Header";
-import Workspace from "@/components/Workspace";
-import FileSelect from "@/components/FileSelect";
-import { Toaster } from "./components/ui/toaster";
-import { useStore } from "./lib/states";
-import { useWindowSize } from "react-use";
-
-const SUPPORTED_FILE_TYPE = [
-	"image/jpeg",
-	"image/png",
-	"image/webp",
-	"image/bmp",
-	"image/tiff",
-];
 function Home() {
-	const [file, updateAppState, setServerConfig, setFile] = useStore(
-		(state) => [
-			state.file,
-			state.updateAppState,
-			state.setServerConfig,
-			state.setFile,
-		],
-	);
-
-	const userInputImage = useInputImage();
-
-	const windowSize = useWindowSize();
-
-	useEffect(() => {
-		if (userInputImage) {
-			setFile(userInputImage);
-		}
-	}, [userInputImage, setFile]);
-
-	useEffect(() => {
-		updateAppState({ windowSize });
-	}, [windowSize]);
-
-	useEffect(() => {
-		const fetchServerConfig = async () => {
-			const serverConfig = await getServerConfig();
-			setServerConfig(serverConfig);
-			if (serverConfig.isDesktop) {
-				// Keeping GUI Window Open
-				keepGUIAlive();
-			}
-		};
-		fetchServerConfig();
-	}, []);
-
-	const dragCounter = useRef(0);
-
-	const handleDrag = useCallback((event: any) => {
-		event.preventDefault();
-		event.stopPropagation();
-	}, []);
-
-	const handleDragIn = useCallback((event: any) => {
-		event.preventDefault();
-		event.stopPropagation();
-		dragCounter.current += 1;
-	}, []);
-
-	const handleDragOut = useCallback((event: any) => {
-		event.preventDefault();
-		event.stopPropagation();
-		dragCounter.current -= 1;
-		if (dragCounter.current > 0) return;
-	}, []);
-
-	const handleDrop = useCallback((event: any) => {
-		event.preventDefault();
-		event.stopPropagation();
-		if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
-			if (event.dataTransfer.files.length > 1) {
-				// setToastState({
-				//   open: true,
-				//   desc: "Please drag and drop only one file",
-				//   state: "error",
-				//   duration: 3000,
-				// })
-			} else {
-				const dragFile = event.dataTransfer.files[0];
-				const fileType = dragFile.type;
-				if (SUPPORTED_FILE_TYPE.includes(fileType)) {
-					setFile(dragFile);
-				} else {
-					// setToastState({
-					//   open: true,
-					//   desc: "Please drag and drop an image file",
-					//   state: "error",
-					//   duration: 3000,
-					// })
-				}
-			}
-			event.dataTransfer.clearData();
-		}
-	}, []);
-
-	const onPaste = useCallback((event: any) => {
-		// TODO: when sd side panel open, ctrl+v not work
-		// https://htmldom.dev/paste-an-image-from-the-clipboard/
-		if (!event.clipboardData) {
-			return;
-		}
-		const clipboardItems = event.clipboardData.items;
-		const items: DataTransferItem[] = [].slice
-			.call(clipboardItems)
-			.filter((item: DataTransferItem) => {
-				// Filter the image items only
-				return item.type.indexOf("image") !== -1;
-			});
-
-		if (items.length === 0) {
-			return;
-		}
-
-		event.preventDefault();
-		event.stopPropagation();
-
-		// TODO: add confirm dialog
-
-		const item = items[0];
-		// Get the blob of image
-		const blob = item.getAsFile();
-		if (blob) {
-			setFile(blob);
-		}
-	}, []);
-
-	useEffect(() => {
-		window.addEventListener("dragenter", handleDragIn);
-		window.addEventListener("dragleave", handleDragOut);
-		window.addEventListener("dragover", handleDrag);
-		window.addEventListener("drop", handleDrop);
-		window.addEventListener("paste", onPaste);
-		return function cleanUp() {
-			window.removeEventListener("dragenter", handleDragIn);
-			window.removeEventListener("dragleave", handleDragOut);
-			window.removeEventListener("dragover", handleDrag);
-			window.removeEventListener("drop", handleDrop);
-			window.removeEventListener("paste", onPaste);
-		};
-	});
-
 	return (
-		
-			
-			
-			
-			{!file ? (
-				 {
-						setFile(f);
-					}}
-				/>
-			) : (
-				<>>
-			)}
-		
+		
+			
+		
 	);
 }
 
diff --git a/custom-demo/front-end/src/features/upload-draw/UploadDraw.tsx b/custom-demo/front-end/src/features/upload-draw/UploadDraw.tsx
new file mode 100644
index 0000000..42d4231
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/UploadDraw.tsx
@@ -0,0 +1,175 @@
+import { useCallback, useEffect, useRef } from "react";
+import { useStore } from "./lib/states";
+
+import useInputImage from "./hooks/useInputImage";
+import { keepGUIAlive } from "./lib/utils";
+import { getServerConfig } from "./lib/api";
+
+import Header from "./components/Header";
+import FileSelect from "./components/FileSelect";
+import Editor from "./components/Editor";
+import ImageSize from "./components/ImageSize";
+
+import { useWindowSize } from "react-use";
+
+const SUPPORTED_FILE_TYPE = [
+	"image/jpeg",
+	"image/png",
+	"image/webp",
+	"image/bmp",
+	"image/tiff",
+];
+
+const UploadDraw = () => {
+	const [file, updateAppState, setServerConfig, setFile] = useStore(
+		(state) => [
+			state.file,
+			state.updateAppState,
+			state.setServerConfig,
+			state.setFile,
+		],
+	);
+
+	const userInputImage = useInputImage();
+
+	const windowSize = useWindowSize();
+
+	useEffect(() => {
+		if (userInputImage) {
+			setFile(userInputImage);
+		}
+	}, [userInputImage, setFile]);
+
+	useEffect(() => {
+		updateAppState({ windowSize });
+	}, [windowSize]);
+
+	useEffect(() => {
+		const fetchServerConfig = async () => {
+			const serverConfig = await getServerConfig();
+			setServerConfig(serverConfig);
+			if (serverConfig.isDesktop) {
+				// Keeping GUI Window Open
+				keepGUIAlive();
+			}
+		};
+		fetchServerConfig();
+	}, []);
+
+	const dragCounter = useRef(0);
+
+	const handleDrag = useCallback((event: any) => {
+		event.preventDefault();
+		event.stopPropagation();
+	}, []);
+
+	const handleDragIn = useCallback((event: any) => {
+		event.preventDefault();
+		event.stopPropagation();
+		dragCounter.current += 1;
+	}, []);
+
+	const handleDragOut = useCallback((event: any) => {
+		event.preventDefault();
+		event.stopPropagation();
+		dragCounter.current -= 1;
+		if (dragCounter.current > 0) return;
+	}, []);
+
+	const handleDrop = useCallback((event: any) => {
+		event.preventDefault();
+		event.stopPropagation();
+		if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
+			if (event.dataTransfer.files.length > 1) {
+				// setToastState({
+				//   open: true,
+				//   desc: "Please drag and drop only one file",
+				//   state: "error",
+				//   duration: 3000,
+				// })
+			} else {
+				const dragFile = event.dataTransfer.files[0];
+				const fileType = dragFile.type;
+				if (SUPPORTED_FILE_TYPE.includes(fileType)) {
+					setFile(dragFile);
+				} else {
+					// setToastState({
+					//   open: true,
+					//   desc: "Please drag and drop an image file",
+					//   state: "error",
+					//   duration: 3000,
+					// })
+				}
+			}
+			event.dataTransfer.clearData();
+		}
+	}, []);
+
+	const onPaste = useCallback((event: any) => {
+		// TODO: when sd side panel open, ctrl+v not work
+		// https://htmldom.dev/paste-an-image-from-the-clipboard/
+		if (!event.clipboardData) {
+			return;
+		}
+		const clipboardItems = event.clipboardData.items;
+		const items: DataTransferItem[] = [].slice
+			.call(clipboardItems)
+			.filter((item: DataTransferItem) => {
+				// Filter the image items only
+				return item.type.indexOf("image") !== -1;
+			});
+
+		if (items.length === 0) {
+			return;
+		}
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		// TODO: add confirm dialog
+
+		const item = items[0];
+		// Get the blob of image
+		const blob = item.getAsFile();
+		if (blob) {
+			setFile(blob);
+		}
+	}, []);
+
+	useEffect(() => {
+		window.addEventListener("dragenter", handleDragIn);
+		window.addEventListener("dragleave", handleDragOut);
+		window.addEventListener("dragover", handleDrag);
+		window.addEventListener("drop", handleDrop);
+		window.addEventListener("paste", onPaste);
+		return function cleanUp() {
+			window.removeEventListener("dragenter", handleDragIn);
+			window.removeEventListener("dragleave", handleDragOut);
+			window.removeEventListener("dragover", handleDrag);
+			window.removeEventListener("drop", handleDrop);
+			window.removeEventListener("paste", onPaste);
+		};
+	});
+
+	return (
+		
+			
+			
+				
+			
+			{file ?  : <>>}
+
+			{!file ? (
+				 {
+						setFile(f);
+					}}
+				/>
+			) : (
+				<>>
+			)}
+		
+	);
+};
+
+export default UploadDraw;
diff --git a/custom-demo/front-end/src/features/upload-draw/components/Editor.tsx b/custom-demo/front-end/src/features/upload-draw/components/Editor.tsx
new file mode 100644
index 0000000..343bde7
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/components/Editor.tsx
@@ -0,0 +1,1051 @@
+import {
+	SyntheticEvent,
+	useCallback,
+	useEffect,
+	useRef,
+	useState,
+} from "react";
+import {
+	ReactZoomPanPinchContentRef,
+	TransformComponent,
+	TransformWrapper,
+} from "react-zoom-pan-pinch";
+import { useKeyPressEvent } from "react-use";
+import { downloadToOutput, runPlugin } from "../lib/api";
+import {
+	askWritePermission,
+	cn,
+	copyCanvasImage,
+	downloadImage,
+	drawLines,
+	generateMask,
+	isMidClick,
+	isRightClick,
+	mouseXY,
+	srcToFile,
+} from "../lib/utils";
+import { Eraser, Eye, Redo, Undo, Expand, Download, Send } from "lucide-react";
+import { useImage } from "../hooks/useImage";
+import { PluginName } from "../lib/types";
+import { useStore } from "../lib/states";
+import useHotKey from "../hooks/useHotkey";
+import { MAX_BRUSH_SIZE, MIN_BRUSH_SIZE } from "../lib/const";
+import { ActionIcon, Slider, Tooltip } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+
+const TOOLBAR_HEIGHT = 200;
+const COMPARE_SLIDER_DURATION_MS = 300;
+
+interface EditorProps {
+	file: File;
+}
+
+export default function Editor(props: EditorProps) {
+	const { file } = props;
+
+	const [
+		disableShortCuts,
+		windowSize,
+		isInpainting,
+		imageWidth,
+		imageHeight,
+		settings,
+		enableAutoSaving,
+		setImageSize,
+		setBaseBrushSize,
+		interactiveSegState,
+		updateInteractiveSegState,
+		handleCanvasMouseDown,
+		handleCanvasMouseMove,
+		undo,
+		redo,
+		undoDisabled,
+		redoDisabled,
+		isProcessing,
+		updateAppState,
+		runMannually,
+		runInpainting,
+		submitMaskImage,
+		isCropperExtenderResizing,
+		decreaseBaseBrushSize,
+		increaseBaseBrushSize,
+	] = useStore((state) => [
+		state.disableShortCuts,
+		state.windowSize,
+		state.isInpainting,
+		state.imageWidth,
+		state.imageHeight,
+		state.settings,
+		state.serverConfig.enableAutoSaving,
+		state.setImageSize,
+		state.setBaseBrushSize,
+		state.interactiveSegState,
+		state.updateInteractiveSegState,
+		state.handleCanvasMouseDown,
+		state.handleCanvasMouseMove,
+		state.undo,
+		state.redo,
+		state.undoDisabled(),
+		state.redoDisabled(),
+		state.getIsProcessing(),
+		state.updateAppState,
+		state.runMannually(),
+		state.runInpainting,
+		state.submitMaskImage,
+		state.isCropperExtenderResizing,
+		state.decreaseBaseBrushSize,
+		state.increaseBaseBrushSize,
+	]);
+	const baseBrushSize = useStore((state) => state.editorState.baseBrushSize);
+	const brushSize = useStore((state) => state.getBrushSize());
+	const renders = useStore((state) => state.editorState.renders);
+	const extraMasks = useStore((state) => state.editorState.extraMasks);
+	const temporaryMasks = useStore(
+		(state) => state.editorState.temporaryMasks,
+	);
+	const lineGroups = useStore((state) => state.editorState.lineGroups);
+	const curLineGroup = useStore((state) => state.editorState.curLineGroup);
+
+	// Local State
+	const [showOriginal, setShowOriginal] = useState(false);
+	const [original, isOriginalLoaded] = useImage(file);
+	const [context, setContext] = useState();
+	const [imageContext, setImageContext] =
+		useState();
+	const [{ x, y }, setCoords] = useState({ x: -1, y: -1 });
+	const [showBrush, setShowBrush] = useState(false);
+	const [showRefBrush, setShowRefBrush] = useState(false);
+	const [isPanning, setIsPanning] = useState(false);
+
+	const [scale, setScale] = useState(1);
+	const [panned, setPanned] = useState(false);
+	const [minScale, setMinScale] = useState(1.0);
+	const windowCenterX = windowSize.width / 2;
+	const windowCenterY = windowSize.height / 2;
+	const viewportRef = useRef(null);
+	// Indicates that the image has been loaded and is centered on first load
+	const [initialCentered, setInitialCentered] = useState(false);
+
+	const [isDraging, setIsDraging] = useState(false);
+
+	const [sliderPos, setSliderPos] = useState(0);
+	const [isChangingBrushSizeByWheel, setIsChangingBrushSizeByWheel] =
+		useState(false);
+
+	const hadDrawSomething = useCallback(() => {
+		return curLineGroup.length !== 0;
+	}, [curLineGroup]);
+
+	useEffect(() => {
+		if (
+			!imageContext ||
+			!isOriginalLoaded ||
+			imageWidth === 0 ||
+			imageHeight === 0
+		) {
+			return;
+		}
+		const render =
+			renders.length === 0 ? original : renders[renders.length - 1];
+		imageContext.canvas.width = imageWidth;
+		imageContext.canvas.height = imageHeight;
+
+		imageContext.clearRect(
+			0,
+			0,
+			imageContext.canvas.width,
+			imageContext.canvas.height,
+		);
+		imageContext.drawImage(render, 0, 0, imageWidth, imageHeight);
+	}, [
+		renders,
+		original,
+		isOriginalLoaded,
+		imageContext,
+		imageHeight,
+		imageWidth,
+	]);
+
+	useEffect(() => {
+		if (
+			!context ||
+			!isOriginalLoaded ||
+			imageWidth === 0 ||
+			imageHeight === 0
+		) {
+			return;
+		}
+		context.canvas.width = imageWidth;
+		context.canvas.height = imageHeight;
+		context.clearRect(0, 0, context.canvas.width, context.canvas.height);
+		temporaryMasks.forEach((maskImage) => {
+			context.drawImage(maskImage, 0, 0, imageWidth, imageHeight);
+		});
+		extraMasks.forEach((maskImage) => {
+			context.drawImage(maskImage, 0, 0, imageWidth, imageHeight);
+		});
+
+		if (
+			interactiveSegState.isInteractiveSeg &&
+			interactiveSegState.tmpInteractiveSegMask
+		) {
+			context.drawImage(
+				interactiveSegState.tmpInteractiveSegMask,
+				0,
+				0,
+				imageWidth,
+				imageHeight,
+			);
+		}
+		drawLines(context, curLineGroup);
+	}, [
+		temporaryMasks,
+		extraMasks,
+		isOriginalLoaded,
+		interactiveSegState,
+		context,
+		curLineGroup,
+		imageHeight,
+		imageWidth,
+	]);
+
+	const getCurrentRender = useCallback(async () => {
+		let targetFile = file;
+		if (renders.length > 0) {
+			const lastRender = renders[renders.length - 1];
+			targetFile = await srcToFile(
+				lastRender.currentSrc,
+				file.name,
+				file.type,
+			);
+		}
+		return targetFile;
+	}, [file, renders]);
+
+	const hadRunInpainting = () => {
+		return renders.length !== 0;
+	};
+
+	const getCurrentWidthHeight = useCallback(() => {
+		let width = 512;
+		let height = 512;
+		if (!isOriginalLoaded) {
+			return [width, height];
+		}
+		if (renders.length === 0) {
+			width = original.naturalWidth;
+			height = original.naturalHeight;
+		} else if (renders.length !== 0) {
+			width = renders[renders.length - 1].width;
+			height = renders[renders.length - 1].height;
+		}
+
+		return [width, height];
+	}, [original, isOriginalLoaded, renders]);
+
+	// Draw once the original image is loaded
+	useEffect(() => {
+		if (!isOriginalLoaded) {
+			return;
+		}
+
+		const [width, height] = getCurrentWidthHeight();
+		if (width !== imageWidth || height !== imageHeight) {
+			setImageSize(width, height);
+		}
+
+		const rW = windowSize.width / width;
+		const rH = (windowSize.height - TOOLBAR_HEIGHT) / height;
+
+		let s = 1.0;
+		if (rW < 1 || rH < 1) {
+			s = Math.min(rW, rH);
+		}
+		setMinScale(s);
+		setScale(s);
+
+		console.log(
+			`[on file load] image size: ${width}x${height}, scale: ${s}, initialCentered: ${initialCentered}`,
+		);
+
+		if (context?.canvas) {
+			console.log("[on file load] set canvas size");
+			if (width != context.canvas.width) {
+				context.canvas.width = width;
+			}
+			if (height != context.canvas.height) {
+				context.canvas.height = height;
+			}
+		}
+
+		if (!initialCentered) {
+			// 防止每次擦除以后图片 zoom 还原
+			viewportRef.current?.centerView(s, 1);
+			console.log("[on file load] centerView");
+			setInitialCentered(true);
+		}
+	}, [
+		viewportRef,
+		imageHeight,
+		imageWidth,
+		original,
+		isOriginalLoaded,
+		windowSize,
+		initialCentered,
+		getCurrentWidthHeight,
+	]);
+
+	useEffect(() => {
+		console.log("[useEffect] centerView");
+		// render 改变尺寸以后,undo/redo 重新 center
+		viewportRef?.current?.centerView(minScale, 1);
+	}, [imageHeight, imageWidth, viewportRef, minScale]);
+
+	// Zoom reset
+	const resetZoom = useCallback(() => {
+		if (!minScale || !windowSize) {
+			return;
+		}
+		const viewport = viewportRef.current;
+		if (!viewport) {
+			return;
+		}
+		const offsetX = (windowSize.width - imageWidth * minScale) / 2;
+		const offsetY = (windowSize.height - imageHeight * minScale) / 2;
+		viewport.setTransform(offsetX, offsetY, minScale, 200, "easeOutQuad");
+		if (viewport.instance.transformState.scale) {
+			viewport.instance.transformState.scale = minScale;
+		}
+
+		setScale(minScale);
+		setPanned(false);
+	}, [
+		viewportRef,
+		windowSize,
+		imageHeight,
+		imageWidth,
+		windowSize.height,
+		minScale,
+	]);
+
+	useEffect(() => {
+		window.addEventListener("resize", () => {
+			resetZoom();
+		});
+		return () => {
+			window.removeEventListener("resize", () => {
+				resetZoom();
+			});
+		};
+	}, [windowSize, resetZoom]);
+
+	const handleEscPressed = () => {
+		if (isProcessing) {
+			return;
+		}
+
+		if (isDraging) {
+			setIsDraging(false);
+		} else {
+			resetZoom();
+		}
+	};
+
+	useHotKey("Escape", handleEscPressed, [
+		isDraging,
+		isInpainting,
+		resetZoom,
+		// drawOnCurrentRender,
+	]);
+
+	const onMouseMove = (ev: SyntheticEvent) => {
+		const mouseEvent = ev.nativeEvent as MouseEvent;
+		setCoords({ x: mouseEvent.pageX, y: mouseEvent.pageY });
+	};
+
+	const onMouseDrag = (ev: SyntheticEvent) => {
+		if (isProcessing) {
+			return;
+		}
+
+		if (interactiveSegState.isInteractiveSeg) {
+			return;
+		}
+		if (isPanning) {
+			return;
+		}
+		if (!isDraging) {
+			return;
+		}
+		if (curLineGroup.length === 0) {
+			return;
+		}
+
+		handleCanvasMouseMove(mouseXY(ev));
+	};
+
+	const runInteractiveSeg = async (newClicks: number[][]) => {
+		updateAppState({ isPluginRunning: true });
+		const targetFile = await getCurrentRender();
+		try {
+			const res = await runPlugin(
+				true,
+				PluginName.InteractiveSeg,
+				targetFile,
+				undefined,
+				newClicks,
+			);
+			const { blob } = res;
+			const img = new Image();
+			img.onload = () => {
+				updateInteractiveSegState({ tmpInteractiveSegMask: img });
+			};
+			img.src = blob;
+		} catch (e: any) {
+			notifications.show({
+				color: "red",
+				title: "Error",
+				message: e.message ? e.message : e.toString(),
+			});
+		}
+		updateAppState({ isPluginRunning: false });
+	};
+
+	const onPointerUp = (ev: SyntheticEvent) => {
+		if (isMidClick(ev)) {
+			setIsPanning(false);
+			return;
+		}
+		if (!hadDrawSomething()) {
+			return;
+		}
+		if (interactiveSegState.isInteractiveSeg) {
+			return;
+		}
+		if (isPanning) {
+			return;
+		}
+		if (!original.src) {
+			return;
+		}
+		const canvas = context?.canvas;
+		if (!canvas) {
+			return;
+		}
+		if (isInpainting) {
+			return;
+		}
+		if (!isDraging) {
+			return;
+		}
+
+		if (runMannually) {
+			setIsDraging(false);
+		}
+		// else {
+		//   runInpainting()
+		// }
+	};
+
+	const onCanvasMouseUp = (ev: SyntheticEvent) => {
+		setIsDraging(false);
+		if (interactiveSegState.isInteractiveSeg) {
+			const xy = mouseXY(ev);
+			const newClicks: number[][] = [...interactiveSegState.clicks];
+			if (isRightClick(ev)) {
+				newClicks.push([xy.x, xy.y, 0, newClicks.length]);
+			} else {
+				newClicks.push([xy.x, xy.y, 1, newClicks.length]);
+			}
+			runInteractiveSeg(newClicks);
+			updateInteractiveSegState({ clicks: newClicks });
+		}
+	};
+
+	const onMouseDown = (ev: SyntheticEvent) => {
+		if (isProcessing) {
+			return;
+		}
+		if (interactiveSegState.isInteractiveSeg) {
+			return;
+		}
+		if (isPanning) {
+			return;
+		}
+		if (!isOriginalLoaded) {
+			return;
+		}
+		const canvas = context?.canvas;
+		if (!canvas) {
+			return;
+		}
+
+		if (isRightClick(ev)) {
+			return;
+		}
+
+		if (isMidClick(ev)) {
+			setIsPanning(true);
+			return;
+		}
+
+		setIsDraging(true);
+		handleCanvasMouseDown(mouseXY(ev));
+	};
+
+	const handleUndo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
+		keyboardEvent.preventDefault();
+		undo();
+	};
+	useHotKey("meta+z,ctrl+z", handleUndo);
+
+	const handleRedo = (keyboardEvent: KeyboardEvent | SyntheticEvent) => {
+		keyboardEvent.preventDefault();
+		redo();
+	};
+	useHotKey("shift+ctrl+z,shift+meta+z", handleRedo);
+
+	useKeyPressEvent(
+		"Tab",
+		(ev) => {
+			ev?.preventDefault();
+			ev?.stopPropagation();
+			if (hadRunInpainting()) {
+				setShowOriginal(() => {
+					window.setTimeout(() => {
+						setSliderPos(100);
+					}, 10);
+					return true;
+				});
+			}
+		},
+		(ev) => {
+			ev?.preventDefault();
+			ev?.stopPropagation();
+			if (hadRunInpainting()) {
+				window.setTimeout(() => {
+					setSliderPos(0);
+				}, 10);
+				window.setTimeout(() => {
+					setShowOriginal(false);
+				}, COMPARE_SLIDER_DURATION_MS);
+			}
+		},
+	);
+
+	const download = useCallback(async () => {
+		if (file === undefined) {
+			return;
+		}
+		if (enableAutoSaving && renders.length > 0) {
+			try {
+				await downloadToOutput(
+					renders[renders.length - 1],
+					file.name,
+					file.type,
+				);
+
+				notifications.show({
+					title: "Success",
+					message: "Save image success",
+				});
+			} catch (e: any) {
+				notifications.show({
+					color: "red",
+					title: "Error",
+					message: e.message ? e.message : e.toString(),
+				});
+			}
+			return;
+		}
+
+		// TODO: download to output directory
+		const name = file.name.replace(/(\.[\w\d_-]+)$/i, "_cleanup$1");
+		const curRender = renders[renders.length - 1];
+		downloadImage(curRender.currentSrc, name);
+		if (settings.enableDownloadMask) {
+			let maskFileName = file.name.replace(/(\.[\w\d_-]+)$/i, "_mask$1");
+			maskFileName = maskFileName.replace(/\.[^/.]+$/, ".jpg");
+
+			const maskCanvas = generateMask(
+				imageWidth,
+				imageHeight,
+				lineGroups,
+			);
+			// Create a link
+			const aDownloadLink = document.createElement("a");
+			// Add the name of the file to the link
+			aDownloadLink.download = maskFileName;
+			// Attach the data to the link
+			aDownloadLink.href = maskCanvas.toDataURL("image/jpeg");
+			// Get the code to click the download link
+			aDownloadLink.click();
+		}
+	}, [
+		file,
+		enableAutoSaving,
+		renders,
+		settings,
+		imageHeight,
+		imageWidth,
+		lineGroups,
+	]);
+
+	useHotKey("meta+s,ctrl+s", download);
+
+	const toggleShowBrush = (newState: boolean) => {
+		if (
+			newState !== showBrush &&
+			!isPanning &&
+			!isCropperExtenderResizing
+		) {
+			setShowBrush(newState);
+		}
+	};
+
+	const getCursor = useCallback(() => {
+		if (isProcessing) {
+			return "default";
+		}
+		if (isPanning) {
+			return "grab";
+		}
+		if (showBrush) {
+			return "none";
+		}
+		return undefined;
+	}, [showBrush, isPanning, isProcessing]);
+
+	// Short Cut
+	useHotKey(
+		"[",
+		() => {
+			decreaseBaseBrushSize();
+		},
+		[decreaseBaseBrushSize],
+	);
+
+	useHotKey(
+		"]",
+		() => {
+			increaseBaseBrushSize();
+		},
+		[increaseBaseBrushSize],
+	);
+
+	// Manual Inpainting Hotkey
+	useHotKey(
+		"shift+r",
+		() => {
+			if (runMannually && hadDrawSomething()) {
+				runInpainting();
+			}
+		},
+		[runMannually, runInpainting, hadDrawSomething],
+	);
+
+	useHotKey(
+		"ctrl+c,meta+c",
+		async () => {
+			const hasPermission = await askWritePermission();
+			if (hasPermission && renders.length > 0) {
+				if (context?.canvas) {
+					await copyCanvasImage(context?.canvas);
+					notifications.show({
+						color: "red",
+						title: "Success",
+						message: "Copy inpainting result to clipboard",
+					});
+				}
+			}
+		},
+		[renders, context],
+	);
+
+	// Toggle clean/zoom tool on spacebar.
+	useKeyPressEvent(
+		" ",
+		(ev) => {
+			if (!disableShortCuts) {
+				ev?.preventDefault();
+				ev?.stopPropagation();
+				setShowBrush(false);
+				setIsPanning(true);
+			}
+		},
+		(ev) => {
+			if (!disableShortCuts) {
+				ev?.preventDefault();
+				ev?.stopPropagation();
+				setShowBrush(true);
+				setIsPanning(false);
+			}
+		},
+	);
+
+	useKeyPressEvent(
+		"Alt",
+		(ev) => {
+			if (!disableShortCuts) {
+				ev?.preventDefault();
+				ev?.stopPropagation();
+				setIsChangingBrushSizeByWheel(true);
+			}
+		},
+		(ev) => {
+			if (!disableShortCuts) {
+				ev?.preventDefault();
+				ev?.stopPropagation();
+				setIsChangingBrushSizeByWheel(false);
+			}
+		},
+	);
+	// Short Cut
+
+	const getCurScale = (): number => {
+		let s = minScale;
+		if (viewportRef.current?.instance?.transformState.scale !== undefined) {
+			s = viewportRef.current?.instance?.transformState.scale;
+		}
+		return s!;
+	};
+
+	const getBrushStyle = (_x: number, _y: number) => {
+		const curScale = getCurScale();
+		return {
+			width: `${brushSize * curScale}px`,
+			height: `${brushSize * curScale}px`,
+			left: `${_x}px`,
+			top: `${_y}px`,
+			transform: "translate(-50%, -50%)",
+		};
+	};
+
+	const renderBrush = (style: any) => {
+		return (
+			
+		);
+	};
+
+	const handleSliderChange = (value: number) => {
+		setBaseBrushSize(value);
+
+		if (!showRefBrush) {
+			setShowRefBrush(true);
+			window.setTimeout(() => {
+				setShowRefBrush(false);
+			}, 10000);
+		}
+	};
+
+	const renderInteractiveSegCursor = () => {
+		return (
+			
+				CursorArrowRaysIcon
+			
+		);
+	};
+
+	const renderCanvas = () => {
+		return (
+			 {
+					if (r) {
+						viewportRef.current = r;
+					}
+				}}
+				panning={{ disabled: !isPanning, velocityDisabled: true }}
+				wheel={{
+					step: 0.05,
+					wheelDisabled: isChangingBrushSizeByWheel,
+				}}
+				centerZoomedOut
+				alignmentAnimation={{ disabled: true }}
+				centerOnInit
+				limitToBounds={false}
+				doubleClick={{ disabled: true }}
+				initialScale={minScale}
+				minScale={minScale * 0.3}
+				onPanning={() => {
+					if (!panned) {
+						setPanned(true);
+					}
+				}}
+				onZoom={(ref) => {
+					setScale(ref.state.scale);
+				}}
+			>
+				
+					
+						
 
+				
+			
+		);
+	};
+
+	const handleScroll = (event: React.WheelEvent) => {
+		if (!isChangingBrushSizeByWheel) {
+			return;
+		}
+
+		const { deltaY } = event;
+		if (deltaY > 0) {
+			increaseBaseBrushSize();
+		} else if (deltaY < 0) {
+			decreaseBaseBrushSize();
+		}
+	};
+
+	return (
+		
+			{renderCanvas()}
+			{showBrush &&
+				!isInpainting &&
+				!isPanning &&
+				(interactiveSegState.isInteractiveSeg
+					? renderInteractiveSegCursor()
+					: renderBrush(getBrushStyle(x, y)))}
+
+			{showRefBrush &&
+				renderBrush(getBrushStyle(windowCenterX, windowCenterY))}
+
+			
+				
 handleSliderChange(vals)}
+					onClick={() => setShowRefBrush(false)}
+				/>
+
+				
+					{/* Reset zoom & pan */}
+					
+						
+							
+								
+							
+						
+					
+
+					{/* Undo */}
+					
+						
+							
+								
+							
+						
+					
+
+					{/* Redo */}
+					
+						
+							
+								
+							
+						
+					
+
+					{/* Show original image (press & hold) */}
+					
+						 {
+								ev.preventDefault();
+								setShowOriginal(() => {
+									window.setTimeout(() => {
+										setSliderPos(100);
+									}, 10);
+									return true;
+								});
+							}}
+							onPointerUp={() => {
+								window.setTimeout(() => {
+									// 防止快速点击 show original image 按钮时图片消失
+									setSliderPos(0);
+								}, 10);
+
+								window.setTimeout(() => {
+									setShowOriginal(false);
+								}, COMPARE_SLIDER_DURATION_MS);
+							}}
+							disabled={renders.length === 0}
+						>
+							
+								
+							
+						
+					
+
+					{/* Save Image */}
+					
+						
+							
+								
+							
+						
+					
+
+					{/* Run Inpainting */}
+					
+						 {
+								runInpainting();
+							}}
+						>
+							
+								
+							
+						
+					
+
+					{/* Run Custom Submit */}
+					
+						 {
+								submitMaskImage();
+							}}
+						>
+							
+								
+							
+						
+					
+				
 
+			
+		
 
+	);
+}
diff --git a/custom-demo/front-end/src/features/upload-draw/components/FileSelect.tsx b/custom-demo/front-end/src/features/upload-draw/components/FileSelect.tsx
new file mode 100644
index 0000000..b36a8dd
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/components/FileSelect.tsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import useResolution from "../hooks/useResolution";
+
+type FileSelectProps = {
+	onSelection: (file: File) => void;
+};
+
+export default function FileSelect(props: FileSelectProps) {
+	const { onSelection } = props;
+
+	const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`);
+
+	const resolution = useResolution();
+
+	function onFileSelected(file: File) {
+		if (!file) {
+			return;
+		}
+		// Skip non-image files
+		const isImage = file.type.match("image.*");
+		if (!isImage) {
+			return;
+		}
+		try {
+			// Check if file is larger than 20mb
+			if (file.size > 20 * 1024 * 1024) {
+				throw new Error("file too large");
+			}
+			onSelection(file);
+		} catch (e) {
+			// eslint-disable-next-line
+			alert(`error: ${(e as any).message}`);
+		}
+	}
+
+	return (
+		
+			
+		
 
+	);
+}
diff --git a/custom-demo/front-end/src/features/upload-draw/components/Header.tsx b/custom-demo/front-end/src/features/upload-draw/components/Header.tsx
new file mode 100644
index 0000000..156d6dd
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/components/Header.tsx
@@ -0,0 +1,87 @@
+import { RotateCw, Image } from "lucide-react";
+import { ActionIcon, Tooltip } from "@mantine/core";
+import { useStore } from "../lib/states";
+
+const Header = () => {
+	const [
+		file,
+		isInpainting,
+		model,
+		setFile,
+		runInpainting,
+		showPrevMask,
+		hidePrevMask,
+	] = useStore((state) => [
+		state.file,
+		state.isInpainting,
+		state.settings.model,
+		state.setFile,
+		state.runInpainting,
+		state.showPrevMask,
+		state.hidePrevMask,
+	]);
+
+	const handleRerunLastMask = () => {
+		runInpainting();
+	};
+
+	const onRerunMouseEnter = () => {
+		showPrevMask();
+	};
+
+	const onRerunMouseLeave = () => {
+		hidePrevMask();
+	};
+
+	return (
+		
+	);
+};
+
+export default Header;
diff --git a/custom-demo/front-end/src/features/upload-draw/components/ImageSize.tsx b/custom-demo/front-end/src/features/upload-draw/components/ImageSize.tsx
new file mode 100644
index 0000000..2f74010
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/components/ImageSize.tsx
@@ -0,0 +1,20 @@
+import { useStore } from "../lib/states";
+
+const ImageSize = () => {
+	const [imageWidth, imageHeight] = useStore((state) => [
+		state.imageWidth,
+		state.imageHeight,
+	]);
+
+	if (!imageWidth || !imageHeight) {
+		return null;
+	}
+
+	return (
+		
+			{imageWidth}x{imageHeight}
+		
+	);
+};
+
+export default ImageSize;
diff --git a/custom-demo/front-end/src/features/upload-draw/hooks/useHotkey.tsx b/custom-demo/front-end/src/features/upload-draw/hooks/useHotkey.tsx
new file mode 100644
index 0000000..0a212c5
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/hooks/useHotkey.tsx
@@ -0,0 +1,16 @@
+import { useStore } from "../lib/states";
+import { useHotkeys } from "react-hotkeys-hook";
+
+const useHotKey = (keys: string, callback: any, deps?: any[]) => {
+	const disableShortCuts = useStore((state) => state.disableShortCuts);
+
+	const ref = useHotkeys(
+		keys,
+		callback,
+		{ enabled: !disableShortCuts },
+		deps,
+	);
+	return ref;
+};
+
+export default useHotKey;
diff --git a/custom-demo/front-end/src/features/upload-draw/hooks/useImage.tsx b/custom-demo/front-end/src/features/upload-draw/hooks/useImage.tsx
new file mode 100644
index 0000000..11a7d3b
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/hooks/useImage.tsx
@@ -0,0 +1,24 @@
+import { useEffect, useState } from "react"
+
+function useImage(file: File | null): [HTMLImageElement, boolean] {
+  const [image] = useState(new Image())
+  const [isLoaded, setIsLoaded] = useState(false)
+
+  useEffect(() => {
+    if (!file) {
+      return
+    }
+    image.onload = () => {
+      setIsLoaded(true)
+    }
+    setIsLoaded(false)
+    image.src = URL.createObjectURL(file)
+    return () => {
+      image.onload = null
+    }
+  }, [file, image])
+
+  return [image, isLoaded]
+}
+
+export { useImage }
diff --git a/custom-demo/front-end/src/features/upload-draw/hooks/useInputImage.tsx b/custom-demo/front-end/src/features/upload-draw/hooks/useInputImage.tsx
new file mode 100644
index 0000000..2d1b1f2
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/hooks/useInputImage.tsx
@@ -0,0 +1,41 @@
+import { API_ENDPOINT } from "../lib/api";
+import { useCallback, useEffect, useState } from "react";
+
+export default function useInputImage() {
+	const [inputImage, setInputImage] = useState(null);
+
+	const fetchInputImage = useCallback(() => {
+		const headers = new Headers();
+		headers.append("pragma", "no-cache");
+		headers.append("cache-control", "no-cache");
+
+		fetch(`${API_ENDPOINT}/inputimage`, { headers })
+			.then(async (res) => {
+				if (!res.ok) {
+					return;
+				}
+				const filename = res.headers
+					.get("content-disposition")
+					?.split("filename=")[1]
+					.split(";")[0];
+
+				const data = await res.blob();
+				if (data && data.type.startsWith("image")) {
+					const userInput = new File(
+						[data],
+						filename !== undefined ? filename : "inputImage",
+					);
+					setInputImage(userInput);
+				}
+			})
+			.catch((err) => {
+				console.log(err);
+			});
+	}, [setInputImage]);
+
+	useEffect(() => {
+		fetchInputImage();
+	}, [fetchInputImage]);
+
+	return inputImage;
+}
diff --git a/custom-demo/front-end/src/features/upload-draw/hooks/useResolution.tsx b/custom-demo/front-end/src/features/upload-draw/hooks/useResolution.tsx
new file mode 100644
index 0000000..39de67e
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/hooks/useResolution.tsx
@@ -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
diff --git a/custom-demo/front-end/src/features/upload-draw/index.ts b/custom-demo/front-end/src/features/upload-draw/index.ts
new file mode 100644
index 0000000..3575de5
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/index.ts
@@ -0,0 +1 @@
+export { default as UploadDraw } from "./UploadDraw";
diff --git a/custom-demo/front-end/src/features/upload-draw/lib/api.ts b/custom-demo/front-end/src/features/upload-draw/lib/api.ts
new file mode 100644
index 0000000..aa558a6
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/lib/api.ts
@@ -0,0 +1,248 @@
+import {
+	Filename,
+	ModelInfo,
+	PowerPaintTask,
+	Rect,
+	ServerConfig,
+} from "./types";
+import { Settings } from "./states";
+import { convertToBase64, srcToFile } from "./utils";
+import axios from "axios";
+
+export const API_ENDPOINT = import.meta.env.DEV
+	? import.meta.env.VITE_BACKEND + "/api/v1"
+	: "/api/v1";
+
+const api = axios.create({
+	baseURL: API_ENDPOINT,
+});
+
+export default async function inpaint(
+	imageFile: File,
+	settings: Settings,
+	croperRect: Rect,
+	extenderState: Rect,
+	mask: File | Blob,
+	paintByExampleImage: File | null = null,
+) {
+	const imageBase64 = await convertToBase64(imageFile);
+	const maskBase64 = await convertToBase64(mask);
+	const exampleImageBase64 = paintByExampleImage
+		? await convertToBase64(paintByExampleImage)
+		: null;
+
+	const res = await fetch(`${API_ENDPOINT}/inpaint`, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/json",
+		},
+		body: JSON.stringify({
+			image: imageBase64,
+			mask: maskBase64,
+			ldm_steps: settings.ldmSteps,
+			ldm_sampler: settings.ldmSampler,
+			zits_wireframe: settings.zitsWireframe,
+			cv2_flag: settings.cv2Flag,
+			cv2_radius: settings.cv2Radius,
+			hd_strategy: "Crop",
+			hd_strategy_crop_triger_size: 640,
+			hd_strategy_crop_margin: 128,
+			hd_trategy_resize_imit: 2048,
+			prompt: settings.prompt,
+			negative_prompt: settings.negativePrompt,
+			use_croper: settings.showCropper,
+			croper_x: croperRect.x,
+			croper_y: croperRect.y,
+			croper_height: croperRect.height,
+			croper_width: croperRect.width,
+			use_extender: settings.showExtender,
+			extender_x: extenderState.x,
+			extender_y: extenderState.y,
+			extender_height: extenderState.height,
+			extender_width: extenderState.width,
+			sd_mask_blur: settings.sdMaskBlur,
+			sd_strength: settings.sdStrength,
+			sd_steps: settings.sdSteps,
+			sd_guidance_scale: settings.sdGuidanceScale,
+			sd_sampler: settings.sdSampler,
+			sd_seed: settings.seedFixed ? settings.seed : -1,
+			sd_match_histograms: settings.sdMatchHistograms,
+			sd_freeu: settings.enableFreeu,
+			sd_freeu_config: settings.freeuConfig,
+			sd_lcm_lora: settings.enableLCMLora,
+			paint_by_example_example_image: exampleImageBase64,
+			p2p_image_guidance_scale: settings.p2pImageGuidanceScale,
+			enable_controlnet: settings.enableControlnet,
+			controlnet_conditioning_scale: settings.controlnetConditioningScale,
+			controlnet_method: settings.controlnetMethod
+				? settings.controlnetMethod
+				: "",
+			powerpaint_task: settings.showExtender
+				? PowerPaintTask.outpainting
+				: settings.powerpaintTask,
+		}),
+	});
+	if (res.ok) {
+		const blob = await res.blob();
+		return {
+			blob: URL.createObjectURL(blob),
+			seed: res.headers.get("X-Seed"),
+		};
+	}
+	const errors = await res.json();
+	throw new Error(`Something went wrong: ${errors.errors}`);
+}
+
+export async function getServerConfig(): Promise {
+	const res = await api.get(`/server-config`);
+	return res.data;
+}
+
+export async function switchModel(name: string): Promise {
+	const res = await api.post(`/model`, { name });
+	return res.data;
+}
+
+export async function switchPluginModel(
+	plugin_name: string,
+	model_name: string,
+) {
+	return api.post(`/switch_plugin_model`, { plugin_name, model_name });
+}
+
+export async function currentModel(): Promise {
+	const res = await api.get("/model");
+	return res.data;
+}
+
+export async function runPlugin(
+	genMask: boolean,
+	name: string,
+	imageFile: File,
+	upscale?: number,
+	clicks?: number[][],
+) {
+	const imageBase64 = await convertToBase64(imageFile);
+	const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image";
+	const res = await fetch(`${API_ENDPOINT}/${p}`, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/json",
+		},
+		body: JSON.stringify({
+			name,
+			image: imageBase64,
+			upscale,
+			clicks,
+		}),
+	});
+	if (res.ok) {
+		const blob = await res.blob();
+		return { blob: URL.createObjectURL(blob) };
+	}
+	const errMsg = await res.json();
+	throw new Error(errMsg);
+}
+
+export async function getMediaFile(tab: string, filename: string) {
+	const res = await fetch(
+		`${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
+			filename,
+		)}`,
+		{
+			method: "GET",
+		},
+	);
+	if (res.ok) {
+		const blob = await res.blob();
+		const file = new File([blob], filename, {
+			type: res.headers.get("Content-Type") ?? "image/png",
+		});
+		return file;
+	}
+	const errMsg = await res.json();
+	throw new Error(errMsg.errors);
+}
+
+export async function getMedias(tab: string): Promise {
+	const res = await api.get(`medias`, { params: { tab } });
+	return res.data;
+}
+
+export async function downloadToOutput(
+	image: HTMLImageElement,
+	filename: string,
+	mimeType: string,
+) {
+	const file = await srcToFile(image.src, filename, mimeType);
+	const fd = new FormData();
+	fd.append("file", file);
+
+	try {
+		const res = await fetch(`${API_ENDPOINT}/save_image`, {
+			method: "POST",
+			body: fd,
+		});
+		if (!res.ok) {
+			const errMsg = await res.text();
+			throw new Error(errMsg);
+		}
+	} catch (error) {
+		throw new Error(`Something went wrong: ${error}`);
+	}
+}
+
+export async function getSamplers(): Promise {
+	const res = await api.post("/samplers");
+	return res.data;
+}
+
+export async function postAdjustMask(
+	mask: File | Blob,
+	operate: "expand" | "shrink" | "reverse",
+	kernel_size: number,
+) {
+	const maskBase64 = await convertToBase64(mask);
+	const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/json",
+		},
+		body: JSON.stringify({
+			mask: maskBase64,
+			operate: operate,
+			kernel_size: kernel_size,
+		}),
+	});
+	if (res.ok) {
+		const blob = await res.blob();
+		return blob;
+	}
+	const errMsg = await res.json();
+	throw new Error(errMsg);
+}
+
+export async function submitMask(imageFile: File, mask: File | Blob) {
+	const imageBase64 = await convertToBase64(imageFile);
+	const maskBase64 = await convertToBase64(mask);
+
+	const res = await fetch(`${API_ENDPOINT}/submit-mask`, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/json",
+		},
+		body: JSON.stringify({
+			image: imageBase64,
+			mask: maskBase64,
+		}),
+	});
+	if (res.ok) {
+		const blob = await res.blob();
+		return {
+			blob: URL.createObjectURL(blob),
+			seed: res.headers.get("X-Seed"),
+		};
+	}
+	// const errors = await res.json();
+	throw new Error(`Submit successfull.`);
+}
diff --git a/custom-demo/front-end/src/features/upload-draw/lib/const.ts b/custom-demo/front-end/src/features/upload-draw/lib/const.ts
new file mode 100644
index 0000000..ff269bc
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/lib/const.ts
@@ -0,0 +1,23 @@
+export const ACCENT_COLOR = "#ffcc00bb"
+export const DEFAULT_BRUSH_SIZE = 40
+export const MIN_BRUSH_SIZE = 3
+export const MAX_BRUSH_SIZE = 200
+export const MODEL_TYPE_INPAINT = "inpaint"
+export const MODEL_TYPE_DIFFUSERS_SD = "diffusers_sd"
+export const MODEL_TYPE_DIFFUSERS_SDXL = "diffusers_sdxl"
+export const MODEL_TYPE_DIFFUSERS_SD_INPAINT = "diffusers_sd_inpaint"
+export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
+export const MODEL_TYPE_OTHER = "diffusers_other"
+export const BRUSH_COLOR = "#ffcc00bb"
+
+export const LDM = "ldm"
+export const CV2 = "cv2"
+
+export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
+export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
+export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"
+export const POWERPAINT = "Sanster/PowerPaint-V1-stable-diffusion-inpainting"
+export const ANYTEXT = "Sanster/AnyText"
+
+export const DEFAULT_NEGATIVE_PROMPT =
+  "out of frame, lowres, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, disfigured, gross proportions, malformed limbs, watermark, signature"
diff --git a/custom-demo/front-end/src/features/upload-draw/lib/states.ts b/custom-demo/front-end/src/features/upload-draw/lib/states.ts
new file mode 100644
index 0000000..e053f16
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/lib/states.ts
@@ -0,0 +1,1231 @@
+import { persist } from "zustand/middleware";
+import { shallow } from "zustand/shallow";
+import { immer } from "zustand/middleware/immer";
+import { castDraft } from "immer";
+import { createWithEqualityFn } from "zustand/traditional";
+import {
+	AdjustMaskOperate,
+	CV2Flag,
+	ExtenderDirection,
+	FreeuConfig,
+	LDMSampler,
+	Line,
+	LineGroup,
+	ModelInfo,
+	PluginParams,
+	Point,
+	PowerPaintTask,
+	ServerConfig,
+	Size,
+	SortBy,
+	SortOrder,
+} from "./types";
+import {
+	BRUSH_COLOR,
+	DEFAULT_BRUSH_SIZE,
+	DEFAULT_NEGATIVE_PROMPT,
+	MAX_BRUSH_SIZE,
+	MODEL_TYPE_INPAINT,
+	PAINT_BY_EXAMPLE,
+} from "./const";
+import {
+	blobToImage,
+	canvasToImage,
+	dataURItoBlob,
+	generateMask,
+	loadImage,
+	srcToFile,
+} from "./utils";
+import inpaint, { postAdjustMask, runPlugin, submitMask } from "./api";
+import { notifications } from "@mantine/notifications";
+
+type FileManagerState = {
+	sortBy: SortBy;
+	sortOrder: SortOrder;
+	layout: "rows" | "masonry";
+	searchText: string;
+	inputDirectory: string;
+	outputDirectory: string;
+};
+
+type CropperState = {
+	x: number;
+	y: number;
+	width: number;
+	height: number;
+};
+
+export type Settings = {
+	model: ModelInfo;
+	enableDownloadMask: boolean;
+	enableManualInpainting: boolean;
+	enableUploadMask: boolean;
+	enableAutoExtractPrompt: boolean;
+	showCropper: boolean;
+	showExtender: boolean;
+	extenderDirection: ExtenderDirection;
+
+	// For LDM
+	ldmSteps: number;
+	ldmSampler: LDMSampler;
+
+	// For ZITS
+	zitsWireframe: boolean;
+
+	// For OpenCV2
+	cv2Radius: number;
+	cv2Flag: CV2Flag;
+
+	// For Diffusion moel
+	prompt: string;
+	negativePrompt: string;
+	seed: number;
+	seedFixed: boolean;
+
+	// For SD
+	sdMaskBlur: number;
+	sdStrength: number;
+	sdSteps: number;
+	sdGuidanceScale: number;
+	sdSampler: string;
+	sdMatchHistograms: boolean;
+	sdScale: number;
+
+	// Pix2Pix
+	p2pImageGuidanceScale: number;
+
+	// ControlNet
+	enableControlnet: boolean;
+	controlnetConditioningScale: number;
+	controlnetMethod: string;
+
+	enableLCMLora: boolean;
+	enableFreeu: boolean;
+	freeuConfig: FreeuConfig;
+
+	// PowerPaint
+	powerpaintTask: PowerPaintTask;
+
+	// AdjustMask
+	adjustMaskKernelSize: number;
+};
+
+type InteractiveSegState = {
+	isInteractiveSeg: boolean;
+	tmpInteractiveSegMask: HTMLImageElement | null;
+	clicks: number[][];
+};
+
+type EditorState = {
+	baseBrushSize: number;
+	brushSizeScale: number;
+	renders: HTMLImageElement[];
+	lineGroups: LineGroup[];
+	lastLineGroup: LineGroup;
+	curLineGroup: LineGroup;
+
+	// mask from interactive-seg or other segmentation models
+	extraMasks: HTMLImageElement[];
+	prevExtraMasks: HTMLImageElement[];
+
+	temporaryMasks: HTMLImageElement[];
+	// redo 相关
+	redoRenders: HTMLImageElement[];
+	redoCurLines: Line[];
+	redoLineGroups: LineGroup[];
+};
+
+type AppState = {
+	file: File | null;
+	paintByExampleFile: File | null;
+	customMask: File | null;
+	imageHeight: number;
+	imageWidth: number;
+	isInpainting: boolean;
+	isPluginRunning: boolean;
+	isAdjustingMask: boolean;
+	windowSize: Size;
+	editorState: EditorState;
+	disableShortCuts: boolean;
+
+	interactiveSegState: InteractiveSegState;
+	fileManagerState: FileManagerState;
+
+	cropperState: CropperState;
+	extenderState: CropperState;
+	isCropperExtenderResizing: boolean;
+
+	serverConfig: ServerConfig;
+
+	settings: Settings;
+};
+
+type AppAction = {
+	updateAppState: (newState: Partial) => void;
+	setFile: (file: File) => Promise;
+	setCustomFile: (file: File) => void;
+	setIsInpainting: (newValue: boolean) => void;
+	getIsProcessing: () => boolean;
+	setBaseBrushSize: (newValue: number) => void;
+	decreaseBaseBrushSize: () => void;
+	increaseBaseBrushSize: () => void;
+	getBrushSize: () => number;
+	setImageSize: (width: number, height: number) => void;
+
+	isSD: () => boolean;
+
+	setCropperX: (newValue: number) => void;
+	setCropperY: (newValue: number) => void;
+	setCropperWidth: (newValue: number) => void;
+	setCropperHeight: (newValue: number) => void;
+
+	setExtenderX: (newValue: number) => void;
+	setExtenderY: (newValue: number) => void;
+	setExtenderWidth: (newValue: number) => void;
+	setExtenderHeight: (newValue: number) => void;
+
+	setIsCropperExtenderResizing: (newValue: boolean) => void;
+	updateExtenderDirection: (newValue: ExtenderDirection) => void;
+	resetExtender: (width: number, height: number) => void;
+	updateExtenderByBuiltIn: (
+		direction: ExtenderDirection,
+		scale: number,
+	) => void;
+
+	setServerConfig: (newValue: ServerConfig) => void;
+	setSeed: (newValue: number) => void;
+	updateSettings: (newSettings: Partial) => void;
+	setModel: (newModel: ModelInfo) => void;
+	updateFileManagerState: (newState: Partial) => void;
+	updateInteractiveSegState: (newState: Partial) => void;
+	resetInteractiveSegState: () => void;
+	handleInteractiveSegAccept: () => void;
+	showPromptInput: () => boolean;
+
+	submitMaskImage: () => Promise;
+	runInpainting: () => Promise;
+	showPrevMask: () => Promise;
+	hidePrevMask: () => void;
+	runRenderablePlugin: (
+		genMask: boolean,
+		pluginName: string,
+		params?: PluginParams,
+	) => Promise;
+
+	// EditorState
+	getCurrentTargetFile: () => Promise;
+	updateEditorState: (newState: Partial) => void;
+	runMannually: () => boolean;
+	handleCanvasMouseDown: (point: Point) => void;
+	handleCanvasMouseMove: (point: Point) => void;
+	cleanCurLineGroup: () => void;
+	resetRedoState: () => void;
+	undo: () => void;
+	redo: () => void;
+	undoDisabled: () => boolean;
+	redoDisabled: () => boolean;
+
+	adjustMask: (operate: AdjustMaskOperate) => Promise;
+	clearMask: () => void;
+};
+
+const defaultValues: AppState = {
+	file: null,
+	paintByExampleFile: null,
+	customMask: null,
+	imageHeight: 0,
+	imageWidth: 0,
+	isInpainting: false,
+	isPluginRunning: false,
+	isAdjustingMask: false,
+	disableShortCuts: false,
+
+	windowSize: {
+		height: 600,
+		width: 800,
+	},
+	editorState: {
+		baseBrushSize: DEFAULT_BRUSH_SIZE,
+		brushSizeScale: 1,
+		renders: [],
+		extraMasks: [],
+		prevExtraMasks: [],
+		temporaryMasks: [],
+		lineGroups: [],
+		lastLineGroup: [],
+		curLineGroup: [],
+		redoRenders: [],
+		redoCurLines: [],
+		redoLineGroups: [],
+	},
+
+	interactiveSegState: {
+		isInteractiveSeg: false,
+		tmpInteractiveSegMask: null,
+		clicks: [],
+	},
+
+	cropperState: {
+		x: 0,
+		y: 0,
+		width: 512,
+		height: 512,
+	},
+	extenderState: {
+		x: 0,
+		y: 0,
+		width: 512,
+		height: 512,
+	},
+	isCropperExtenderResizing: false,
+
+	fileManagerState: {
+		sortBy: SortBy.CTIME,
+		sortOrder: SortOrder.DESCENDING,
+		layout: "masonry",
+		searchText: "",
+		inputDirectory: "",
+		outputDirectory: "",
+	},
+	serverConfig: {
+		plugins: [],
+		modelInfos: [],
+		removeBGModel: "briaai/RMBG-1.4",
+		removeBGModels: [],
+		realesrganModel: "realesr-general-x4v3",
+		realesrganModels: [],
+		interactiveSegModel: "vit_b",
+		interactiveSegModels: [],
+		enableFileManager: false,
+		enableAutoSaving: false,
+		enableControlnet: false,
+		controlnetMethod: "lllyasviel/control_v11p_sd15_canny",
+		disableModelSwitch: false,
+		isDesktop: false,
+		samplers: ["DPM++ 2M SDE Karras"],
+	},
+	settings: {
+		model: {
+			name: "lama",
+			path: "lama",
+			model_type: "inpaint",
+			support_controlnet: false,
+			support_strength: false,
+			support_outpainting: false,
+			controlnets: [],
+			support_freeu: false,
+			support_lcm_lora: false,
+			is_single_file_diffusers: false,
+			need_prompt: false,
+		},
+		enableControlnet: false,
+		showCropper: false,
+		showExtender: false,
+		extenderDirection: ExtenderDirection.xy,
+		enableDownloadMask: false,
+		enableManualInpainting: false,
+		enableUploadMask: false,
+		enableAutoExtractPrompt: true,
+		ldmSteps: 30,
+		ldmSampler: LDMSampler.ddim,
+		zitsWireframe: true,
+		cv2Radius: 5,
+		cv2Flag: CV2Flag.INPAINT_NS,
+		prompt: "",
+		negativePrompt: DEFAULT_NEGATIVE_PROMPT,
+		seed: 42,
+		seedFixed: false,
+		sdMaskBlur: 12,
+		sdStrength: 1.0,
+		sdSteps: 50,
+		sdGuidanceScale: 7.5,
+		sdSampler: "DPM++ 2M",
+		sdMatchHistograms: false,
+		sdScale: 1.0,
+		p2pImageGuidanceScale: 1.5,
+		controlnetConditioningScale: 0.4,
+		controlnetMethod: "lllyasviel/control_v11p_sd15_canny",
+		enableLCMLora: false,
+		enableFreeu: false,
+		freeuConfig: { s1: 0.9, s2: 0.2, b1: 1.2, b2: 1.4 },
+		powerpaintTask: PowerPaintTask.text_guided,
+		adjustMaskKernelSize: 12,
+	},
+};
+
+export const useStore = createWithEqualityFn()(
+	persist(
+		immer((set, get) => ({
+			...defaultValues,
+
+			showPrevMask: async () => {
+				if (get().settings.showExtender) {
+					return;
+				}
+				const {
+					lastLineGroup,
+					curLineGroup,
+					prevExtraMasks,
+					extraMasks,
+				} = get().editorState;
+				if (curLineGroup.length !== 0 || extraMasks.length !== 0) {
+					return;
+				}
+				const { imageWidth, imageHeight } = get();
+
+				const maskCanvas = generateMask(
+					imageWidth,
+					imageHeight,
+					[lastLineGroup],
+					prevExtraMasks,
+					BRUSH_COLOR,
+				);
+				try {
+					const maskImage = await canvasToImage(maskCanvas);
+					set((state) => {
+						state.editorState.temporaryMasks.push(
+							castDraft(maskImage),
+						);
+					});
+				} catch (e) {
+					console.error(e);
+					return;
+				}
+			},
+			hidePrevMask: () => {
+				set((state) => {
+					state.editorState.temporaryMasks = [];
+				});
+			},
+
+			getCurrentTargetFile: async (): Promise => {
+				const file = get().file!; // 一定是在 file 加载了以后才可能调用这个函数
+				const renders = get().editorState.renders;
+
+				let targetFile = file;
+				if (renders.length > 0) {
+					const lastRender = renders[renders.length - 1];
+					targetFile = await srcToFile(
+						lastRender.currentSrc,
+						file.name,
+						file.type,
+					);
+				}
+				return targetFile;
+			},
+
+			submitMaskImage: async () => {
+				const { file, imageWidth, imageHeight, settings } = get();
+				if (file === null) {
+					return;
+				}
+
+				const {
+					lastLineGroup,
+					curLineGroup,
+					lineGroups,
+					renders,
+					prevExtraMasks,
+					extraMasks,
+				} = get().editorState;
+
+				const useLastLineGroup =
+					curLineGroup.length === 0 &&
+					extraMasks.length === 0 &&
+					!settings.showExtender;
+
+				let maskImages: HTMLImageElement[] = [];
+				let maskLineGroup: LineGroup = [];
+				if (useLastLineGroup === true) {
+					maskLineGroup = lastLineGroup;
+					maskImages = prevExtraMasks;
+				} else {
+					maskLineGroup = curLineGroup;
+					maskImages = extraMasks;
+				}
+
+				if (
+					maskLineGroup.length === 0 &&
+					maskImages === null &&
+					!settings.showExtender
+				) {
+					notifications.show({
+						color: "red",
+						title: "Error",
+						message: "Please draw mask on picture",
+					});
+					return;
+				}
+
+				const newLineGroups = [...lineGroups, maskLineGroup];
+
+				set((state) => {
+					state.isInpainting = true;
+				});
+
+				let targetFile = file;
+				if (useLastLineGroup === true) {
+					if (renders.length > 1) {
+						const lastRender = renders[renders.length - 2];
+						targetFile = await srcToFile(
+							lastRender.currentSrc,
+							file.name,
+							file.type,
+						);
+					}
+				} else if (renders.length > 0) {
+					const lastRender = renders[renders.length - 1];
+					targetFile = await srcToFile(
+						lastRender.currentSrc,
+						file.name,
+						file.type,
+					);
+				}
+
+				const maskCanvas = generateMask(
+					imageWidth,
+					imageHeight,
+					[maskLineGroup],
+					maskImages,
+					BRUSH_COLOR,
+				);
+				if (useLastLineGroup) {
+					const temporaryMask = await canvasToImage(maskCanvas);
+					set((state) => {
+						state.editorState.temporaryMasks = castDraft([
+							temporaryMask,
+						]);
+					});
+				}
+
+				try {
+					const res = await submitMask(
+						targetFile,
+						dataURItoBlob(maskCanvas.toDataURL()),
+					);
+
+					const { blob, seed } = res;
+					if (seed) {
+						get().setSeed(parseInt(seed, 10));
+					}
+					const newRender = new Image();
+					await loadImage(newRender, blob);
+					const newRenders = [...renders, newRender];
+					get().setImageSize(newRender.width, newRender.height);
+					get().updateEditorState({
+						renders: newRenders,
+						lineGroups: newLineGroups,
+						lastLineGroup: maskLineGroup,
+						curLineGroup: [],
+						extraMasks: [],
+						prevExtraMasks: maskImages,
+					});
+				} catch (e: any) {
+					notifications.show({
+						color: "red",
+						title: "Error",
+						message: e.message ? e.message : e.toString(),
+					});
+				}
+
+				get().resetRedoState();
+				set((state) => {
+					state.isInpainting = false;
+					state.editorState.temporaryMasks = [];
+				});
+			},
+
+			runInpainting: async () => {
+				const {
+					isInpainting,
+					file,
+					paintByExampleFile,
+					imageWidth,
+					imageHeight,
+					settings,
+					cropperState,
+					extenderState,
+				} = get();
+				if (isInpainting || file === null) {
+					return;
+				}
+				if (
+					get().settings.model.support_outpainting &&
+					settings.showExtender &&
+					extenderState.height === imageHeight &&
+					extenderState.width === imageWidth
+				) {
+					return;
+				}
+
+				const {
+					lastLineGroup,
+					curLineGroup,
+					lineGroups,
+					renders,
+					prevExtraMasks,
+					extraMasks,
+				} = get().editorState;
+
+				const useLastLineGroup =
+					curLineGroup.length === 0 &&
+					extraMasks.length === 0 &&
+					!settings.showExtender;
+
+				// useLastLineGroup 的影响
+				// 1. 使用上一次的 mask
+				// 2. 结果替换当前 render
+				let maskImages: HTMLImageElement[] = [];
+				let maskLineGroup: LineGroup = [];
+				if (useLastLineGroup === true) {
+					maskLineGroup = lastLineGroup;
+					maskImages = prevExtraMasks;
+				} else {
+					maskLineGroup = curLineGroup;
+					maskImages = extraMasks;
+				}
+
+				if (
+					maskLineGroup.length === 0 &&
+					maskImages === null &&
+					!settings.showExtender
+				) {
+					notifications.show({
+						color: "red",
+						title: "Error",
+						message: "Please draw mask on picture",
+					});
+					return;
+				}
+
+				const newLineGroups = [...lineGroups, maskLineGroup];
+
+				set((state) => {
+					state.isInpainting = true;
+				});
+
+				let targetFile = file;
+				if (useLastLineGroup === true) {
+					// renders.length == 1 还是用原来的
+					if (renders.length > 1) {
+						const lastRender = renders[renders.length - 2];
+						targetFile = await srcToFile(
+							lastRender.currentSrc,
+							file.name,
+							file.type,
+						);
+					}
+				} else if (renders.length > 0) {
+					const lastRender = renders[renders.length - 1];
+					targetFile = await srcToFile(
+						lastRender.currentSrc,
+						file.name,
+						file.type,
+					);
+				}
+
+				const maskCanvas = generateMask(
+					imageWidth,
+					imageHeight,
+					[maskLineGroup],
+					maskImages,
+					BRUSH_COLOR,
+				);
+				if (useLastLineGroup) {
+					const temporaryMask = await canvasToImage(maskCanvas);
+					set((state) => {
+						state.editorState.temporaryMasks = castDraft([
+							temporaryMask,
+						]);
+					});
+				}
+
+				try {
+					const res = await inpaint(
+						targetFile,
+						settings,
+						cropperState,
+						extenderState,
+						dataURItoBlob(maskCanvas.toDataURL()),
+						paintByExampleFile,
+					);
+
+					const { blob, seed } = res;
+					if (seed) {
+						get().setSeed(parseInt(seed, 10));
+					}
+					const newRender = new Image();
+					await loadImage(newRender, blob);
+					const newRenders = [...renders, newRender];
+					get().setImageSize(newRender.width, newRender.height);
+					get().updateEditorState({
+						renders: newRenders,
+						lineGroups: newLineGroups,
+						lastLineGroup: maskLineGroup,
+						curLineGroup: [],
+						extraMasks: [],
+						prevExtraMasks: maskImages,
+					});
+				} catch (e: any) {
+					notifications.show({
+						color: "red",
+						title: "Error",
+						message: e.message ? e.message : e.toString(),
+					});
+				}
+
+				get().resetRedoState();
+				set((state) => {
+					state.isInpainting = false;
+					state.editorState.temporaryMasks = [];
+				});
+			},
+
+			runRenderablePlugin: async (
+				genMask: boolean,
+				pluginName: string,
+				params: PluginParams = { upscale: 1 },
+			) => {
+				const { renders, lineGroups } = get().editorState;
+				set((state) => {
+					state.isPluginRunning = true;
+				});
+
+				try {
+					const start = new Date();
+					const targetFile = await get().getCurrentTargetFile();
+					const res = await runPlugin(
+						genMask,
+						pluginName,
+						targetFile,
+						params.upscale,
+					);
+					const { blob } = res;
+
+					if (!genMask) {
+						const newRender = new Image();
+						await loadImage(newRender, blob);
+						get().setImageSize(newRender.width, newRender.height);
+						const newRenders = [...renders, newRender];
+						const newLineGroups = [...lineGroups, []];
+						get().updateEditorState({
+							renders: newRenders,
+							lineGroups: newLineGroups,
+						});
+					} else {
+						const newMask = new Image();
+						await loadImage(newMask, blob);
+						set((state) => {
+							state.editorState.extraMasks.push(
+								castDraft(newMask),
+							);
+						});
+					}
+					const end = new Date();
+					const time = end.getTime() - start.getTime();
+
+					notifications.show({
+						title: "Success",
+						message: `Run ${pluginName} successfully in ${
+							time / 1000
+						}s`,
+					});
+				} catch (e: any) {
+					notifications.show({
+						color: "red",
+						title: "Error",
+						message: e.message ? e.message : e.toString(),
+					});
+				}
+				set((state) => {
+					state.isPluginRunning = false;
+				});
+			},
+
+			// Edirot State //
+			updateEditorState: (newState: Partial) => {
+				set((state) => {
+					state.editorState = castDraft({
+						...state.editorState,
+						...newState,
+					});
+				});
+			},
+
+			cleanCurLineGroup: () => {
+				get().updateEditorState({ curLineGroup: [] });
+			},
+
+			handleCanvasMouseDown: (point: Point) => {
+				let lineGroup: LineGroup = [];
+				const state = get();
+				if (state.runMannually()) {
+					lineGroup = [...state.editorState.curLineGroup];
+				}
+				lineGroup.push({ size: state.getBrushSize(), pts: [point] });
+				set((state) => {
+					state.editorState.curLineGroup = lineGroup;
+				});
+			},
+
+			handleCanvasMouseMove: (point: Point) => {
+				set((state) => {
+					const curLineGroup = state.editorState.curLineGroup;
+					if (curLineGroup.length) {
+						curLineGroup[curLineGroup.length - 1].pts.push(point);
+					}
+				});
+			},
+
+			runMannually: (): boolean => {
+				const state = get();
+				return (
+					state.settings.enableManualInpainting ||
+					state.settings.model.model_type !== MODEL_TYPE_INPAINT
+				);
+			},
+
+			getIsProcessing: (): boolean => {
+				return (
+					get().isInpainting ||
+					get().isPluginRunning ||
+					get().isAdjustingMask
+				);
+			},
+
+			isSD: (): boolean => {
+				return get().settings.model.model_type !== MODEL_TYPE_INPAINT;
+			},
+
+			// undo/redo
+
+			undoDisabled: (): boolean => {
+				const editorState = get().editorState;
+				if (editorState.renders.length > 0) {
+					return false;
+				}
+				if (get().runMannually()) {
+					if (editorState.curLineGroup.length === 0) {
+						return true;
+					}
+				} else if (editorState.renders.length === 0) {
+					return true;
+				}
+				return false;
+			},
+
+			undo: () => {
+				if (
+					get().runMannually() &&
+					get().editorState.curLineGroup.length !== 0
+				) {
+					// undoStroke
+					set((state) => {
+						const editorState = state.editorState;
+						if (editorState.curLineGroup.length === 0) {
+							return;
+						}
+						editorState.lastLineGroup = [];
+						const lastLine = editorState.curLineGroup.pop()!;
+						editorState.redoCurLines.push(lastLine);
+					});
+				} else {
+					set((state) => {
+						const editorState = state.editorState;
+						if (
+							editorState.renders.length === 0 ||
+							editorState.lineGroups.length === 0
+						) {
+							return;
+						}
+						const lastLineGroup = editorState.lineGroups.pop()!;
+						editorState.redoLineGroups.push(lastLineGroup);
+						editorState.redoCurLines = [];
+						editorState.curLineGroup = [];
+
+						const lastRender = editorState.renders.pop()!;
+						editorState.redoRenders.push(lastRender);
+					});
+				}
+			},
+
+			redoDisabled: (): boolean => {
+				const editorState = get().editorState;
+				if (editorState.redoRenders.length > 0) {
+					return false;
+				}
+				if (get().runMannually()) {
+					if (editorState.redoCurLines.length === 0) {
+						return true;
+					}
+				} else if (editorState.redoRenders.length === 0) {
+					return true;
+				}
+				return false;
+			},
+
+			redo: () => {
+				if (
+					get().runMannually() &&
+					get().editorState.redoCurLines.length !== 0
+				) {
+					set((state) => {
+						const editorState = state.editorState;
+						if (editorState.redoCurLines.length === 0) {
+							return;
+						}
+						const line = editorState.redoCurLines.pop()!;
+						editorState.curLineGroup.push(line);
+					});
+				} else {
+					set((state) => {
+						const editorState = state.editorState;
+						if (
+							editorState.redoRenders.length === 0 ||
+							editorState.redoLineGroups.length === 0
+						) {
+							return;
+						}
+						const lastLineGroup = editorState.redoLineGroups.pop()!;
+						editorState.lineGroups.push(lastLineGroup);
+						editorState.curLineGroup = [];
+
+						const lastRender = editorState.redoRenders.pop()!;
+						editorState.renders.push(lastRender);
+					});
+				}
+			},
+
+			resetRedoState: () => {
+				set((state) => {
+					state.editorState.redoCurLines = [];
+					state.editorState.redoLineGroups = [];
+					state.editorState.redoRenders = [];
+				});
+			},
+
+			//****//
+
+			updateAppState: (newState: Partial) => {
+				set(() => newState);
+			},
+
+			getBrushSize: (): number => {
+				return (
+					get().editorState.baseBrushSize *
+					get().editorState.brushSizeScale
+				);
+			},
+
+			showPromptInput: (): boolean => {
+				const model = get().settings.model;
+				return (
+					model.model_type !== MODEL_TYPE_INPAINT &&
+					model.name !== PAINT_BY_EXAMPLE
+				);
+			},
+
+			setServerConfig: (newValue: ServerConfig) => {
+				set((state) => {
+					state.serverConfig = newValue;
+					state.settings.enableControlnet = newValue.enableControlnet;
+					state.settings.controlnetMethod = newValue.controlnetMethod;
+				});
+			},
+
+			updateSettings: (newSettings: Partial) => {
+				set((state) => {
+					state.settings = {
+						...state.settings,
+						...newSettings,
+					};
+				});
+			},
+
+			setModel: (newModel: ModelInfo) => {
+				set((state) => {
+					state.settings.model = newModel;
+
+					if (
+						newModel.support_controlnet &&
+						!newModel.controlnets.includes(
+							state.settings.controlnetMethod,
+						)
+					) {
+						state.settings.controlnetMethod =
+							newModel.controlnets[0];
+					}
+				});
+			},
+
+			updateFileManagerState: (newState: Partial) => {
+				set((state) => {
+					state.fileManagerState = {
+						...state.fileManagerState,
+						...newState,
+					};
+				});
+			},
+
+			updateInteractiveSegState: (
+				newState: Partial,
+			) => {
+				set((state) => {
+					return {
+						...state,
+						interactiveSegState: {
+							...state.interactiveSegState,
+							...newState,
+						},
+					};
+				});
+			},
+
+			resetInteractiveSegState: () => {
+				get().updateInteractiveSegState(
+					defaultValues.interactiveSegState,
+				);
+			},
+
+			handleInteractiveSegAccept: () => {
+				set((state) => {
+					if (state.interactiveSegState.tmpInteractiveSegMask) {
+						state.editorState.extraMasks.push(
+							castDraft(
+								state.interactiveSegState.tmpInteractiveSegMask,
+							),
+						);
+					}
+					state.interactiveSegState = castDraft({
+						...defaultValues.interactiveSegState,
+					});
+				});
+			},
+
+			setIsInpainting: (newValue: boolean) =>
+				set((state) => {
+					state.isInpainting = newValue;
+				}),
+
+			setFile: async (file: File) => {
+				set((state) => {
+					state.file = file;
+					state.interactiveSegState = castDraft(
+						defaultValues.interactiveSegState,
+					);
+					state.editorState = castDraft(defaultValues.editorState);
+					state.cropperState = defaultValues.cropperState;
+				});
+			},
+
+			setCustomFile: (file: File) =>
+				set((state) => {
+					state.customMask = file;
+				}),
+
+			setBaseBrushSize: (newValue: number) =>
+				set((state) => {
+					state.editorState.baseBrushSize = newValue;
+				}),
+
+			decreaseBaseBrushSize: () => {
+				const baseBrushSize = get().editorState.baseBrushSize;
+				let newBrushSize = baseBrushSize;
+				if (baseBrushSize > 10) {
+					newBrushSize = baseBrushSize - 10;
+				}
+				if (baseBrushSize <= 10 && baseBrushSize > 0) {
+					newBrushSize = baseBrushSize - 3;
+				}
+				get().setBaseBrushSize(newBrushSize);
+			},
+
+			increaseBaseBrushSize: () => {
+				const baseBrushSize = get().editorState.baseBrushSize;
+				const newBrushSize = Math.min(
+					baseBrushSize + 10,
+					MAX_BRUSH_SIZE,
+				);
+				get().setBaseBrushSize(newBrushSize);
+			},
+
+			setImageSize: (width: number, height: number) => {
+				// 根据图片尺寸调整 brushSize 的 scale
+				set((state) => {
+					state.imageWidth = width;
+					state.imageHeight = height;
+					state.editorState.brushSizeScale =
+						Math.max(Math.min(width, height), 512) / 512;
+				});
+				get().resetExtender(width, height);
+			},
+
+			setCropperX: (newValue: number) =>
+				set((state) => {
+					state.cropperState.x = newValue;
+				}),
+
+			setCropperY: (newValue: number) =>
+				set((state) => {
+					state.cropperState.y = newValue;
+				}),
+
+			setCropperWidth: (newValue: number) =>
+				set((state) => {
+					state.cropperState.width = newValue;
+				}),
+
+			setCropperHeight: (newValue: number) =>
+				set((state) => {
+					state.cropperState.height = newValue;
+				}),
+
+			setExtenderX: (newValue: number) =>
+				set((state) => {
+					state.extenderState.x = newValue;
+				}),
+
+			setExtenderY: (newValue: number) =>
+				set((state) => {
+					state.extenderState.y = newValue;
+				}),
+
+			setExtenderWidth: (newValue: number) =>
+				set((state) => {
+					state.extenderState.width = newValue;
+				}),
+
+			setExtenderHeight: (newValue: number) =>
+				set((state) => {
+					state.extenderState.height = newValue;
+				}),
+
+			setIsCropperExtenderResizing: (newValue: boolean) =>
+				set((state) => {
+					state.isCropperExtenderResizing = newValue;
+				}),
+
+			updateExtenderDirection: (newValue: ExtenderDirection) => {
+				console.log(
+					`updateExtenderDirection: ${JSON.stringify(
+						get().extenderState,
+					)}`,
+				);
+				set((state) => {
+					state.settings.extenderDirection = newValue;
+					state.extenderState.x = 0;
+					state.extenderState.y = 0;
+					state.extenderState.width = state.imageWidth;
+					state.extenderState.height = state.imageHeight;
+				});
+				get().updateExtenderByBuiltIn(newValue, 1.5);
+			},
+
+			updateExtenderByBuiltIn: (
+				direction: ExtenderDirection,
+				scale: number,
+			) => {
+				const newExtenderState = { ...defaultValues.extenderState };
+				let { x, y, width, height } = newExtenderState;
+				const { imageWidth, imageHeight } = get();
+				width = imageWidth;
+				height = imageHeight;
+
+				switch (direction) {
+					case ExtenderDirection.x:
+						x = -Math.ceil((imageWidth * (scale - 1)) / 2);
+						width = Math.ceil(imageWidth * scale);
+						break;
+					case ExtenderDirection.y:
+						y = -Math.ceil((imageHeight * (scale - 1)) / 2);
+						height = Math.ceil(imageHeight * scale);
+						break;
+					case ExtenderDirection.xy:
+						x = -Math.ceil((imageWidth * (scale - 1)) / 2);
+						y = -Math.ceil((imageHeight * (scale - 1)) / 2);
+						width = Math.ceil(imageWidth * scale);
+						height = Math.ceil(imageHeight * scale);
+						break;
+					default:
+						break;
+				}
+
+				set((state) => {
+					state.extenderState.x = x;
+					state.extenderState.y = y;
+					state.extenderState.width = width;
+					state.extenderState.height = height;
+				});
+			},
+
+			resetExtender: (width: number, height: number) => {
+				set((state) => {
+					state.extenderState.x = 0;
+					state.extenderState.y = 0;
+					state.extenderState.width = width;
+					state.extenderState.height = height;
+				});
+			},
+
+			setSeed: (newValue: number) =>
+				set((state) => {
+					state.settings.seed = newValue;
+				}),
+
+			adjustMask: async (operate: AdjustMaskOperate) => {
+				const { imageWidth, imageHeight } = get();
+				const { curLineGroup, extraMasks } = get().editorState;
+				const { adjustMaskKernelSize } = get().settings;
+				if (curLineGroup.length === 0 && extraMasks.length === 0) {
+					return;
+				}
+
+				set((state) => {
+					state.isAdjustingMask = true;
+				});
+
+				const maskCanvas = generateMask(
+					imageWidth,
+					imageHeight,
+					[curLineGroup],
+					extraMasks,
+					BRUSH_COLOR,
+				);
+				const maskBlob = dataURItoBlob(maskCanvas.toDataURL());
+				const newMaskBlob = await postAdjustMask(
+					maskBlob,
+					operate,
+					adjustMaskKernelSize,
+				);
+				const newMask = await blobToImage(newMaskBlob);
+
+				// TODO: currently ignore stroke undo/redo
+				set((state) => {
+					state.editorState.extraMasks = [castDraft(newMask)];
+					state.editorState.curLineGroup = [];
+				});
+
+				set((state) => {
+					state.isAdjustingMask = false;
+				});
+			},
+			clearMask: () => {
+				set((state) => {
+					state.editorState.extraMasks = [];
+					state.editorState.curLineGroup = [];
+				});
+			},
+		})),
+		{
+			name: "ZUSTAND_STATE", // name of the item in the storage (must be unique)
+			version: 1,
+			partialize: (state) =>
+				Object.fromEntries(
+					Object.entries(state).filter(([key]) =>
+						["fileManagerState", "settings"].includes(key),
+					),
+				),
+		},
+	),
+	shallow,
+);
diff --git a/custom-demo/front-end/src/features/upload-draw/lib/types.ts b/custom-demo/front-end/src/features/upload-draw/lib/types.ts
new file mode 100644
index 0000000..38cacc0
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/lib/types.ts
@@ -0,0 +1,136 @@
+export interface Filename {
+  name: string
+  height: number
+  width: number
+  ctime: number
+  mtime: number
+}
+
+export interface PluginInfo {
+  name: string
+  support_gen_image: boolean
+  support_gen_mask: boolean
+}
+
+export interface ServerConfig {
+  plugins: PluginInfo[]
+  modelInfos: ModelInfo[]
+  removeBGModel: string
+  removeBGModels: string[]
+  realesrganModel: string
+  realesrganModels: string[]
+  interactiveSegModel: string
+  interactiveSegModels: string[]
+  enableFileManager: boolean
+  enableAutoSaving: boolean
+  enableControlnet: boolean
+  controlnetMethod: string
+  disableModelSwitch: boolean
+  isDesktop: boolean
+  samplers: string[]
+}
+
+export interface GenInfo {
+  prompt: string
+  negative_prompt: string
+}
+
+export interface ModelInfo {
+  name: string
+  path: string
+  model_type:
+    | "inpaint"
+    | "diffusers_sd"
+    | "diffusers_sdxl"
+    | "diffusers_sd_inpaint"
+    | "diffusers_sdxl_inpaint"
+    | "diffusers_other"
+  support_strength: boolean
+  support_outpainting: boolean
+  support_controlnet: boolean
+  controlnets: string[]
+  support_freeu: boolean
+  support_lcm_lora: boolean
+  need_prompt: boolean
+  is_single_file_diffusers: boolean
+}
+
+export enum PluginName {
+  RemoveBG = "RemoveBG",
+  AnimeSeg = "AnimeSeg",
+  RealESRGAN = "RealESRGAN",
+  GFPGAN = "GFPGAN",
+  RestoreFormer = "RestoreFormer",
+  InteractiveSeg = "InteractiveSeg",
+}
+
+export interface PluginParams {
+  upscale: number
+}
+
+export enum SortBy {
+  NAME = "name",
+  CTIME = "ctime",
+  MTIME = "mtime",
+}
+
+export enum SortOrder {
+  DESCENDING = "desc",
+  ASCENDING = "asc",
+}
+
+export enum LDMSampler {
+  ddim = "ddim",
+  plms = "plms",
+}
+
+export enum CV2Flag {
+  INPAINT_NS = "INPAINT_NS",
+  INPAINT_TELEA = "INPAINT_TELEA",
+}
+
+export interface Rect {
+  x: number
+  y: number
+  width: number
+  height: number
+}
+
+export interface FreeuConfig {
+  s1: number
+  s2: number
+  b1: number
+  b2: number
+}
+
+export interface Point {
+  x: number
+  y: number
+}
+
+export interface Line {
+  size?: number
+  pts: Point[]
+}
+
+export type LineGroup = Array
+
+export interface Size {
+  width: number
+  height: number
+}
+
+export enum ExtenderDirection {
+  x = "x",
+  y = "y",
+  xy = "xy",
+}
+
+export enum PowerPaintTask {
+  text_guided = "text-guided",
+  shape_guided = "shape-guided",
+  object_remove = "object-remove",
+  outpainting = "outpainting",
+}
+
+export type AdjustMaskOperate = "expand" | "shrink" | "reverse"
diff --git a/custom-demo/front-end/src/features/upload-draw/lib/utils.ts b/custom-demo/front-end/src/features/upload-draw/lib/utils.ts
new file mode 100644
index 0000000..becbdf8
--- /dev/null
+++ b/custom-demo/front-end/src/features/upload-draw/lib/utils.ts
@@ -0,0 +1,246 @@
+import { type ClassValue, clsx } from "clsx";
+import { SyntheticEvent } from "react";
+import { twMerge } from "tailwind-merge";
+import { LineGroup } from "./types";
+import { BRUSH_COLOR } from "./const";
+
+export function cn(...inputs: ClassValue[]) {
+	return twMerge(clsx(inputs));
+}
+
+export function keepGUIAlive() {
+	async function getRequest(url = "") {
+		const response = await fetch(url, {
+			method: "GET",
+			cache: "no-cache",
+		});
+		return response.json();
+	}
+
+	const keepAliveServer = () => {
+		const url = document.location;
+		const route = "/flaskwebgui-keep-server-alive";
+		getRequest(url + route).then((data) => {
+			return data;
+		});
+	};
+
+	const intervalRequest = 3 * 1000;
+	keepAliveServer();
+	setInterval(keepAliveServer, intervalRequest);
+}
+
+export function dataURItoBlob(dataURI: string) {
+	const mime = dataURI.split(",")[0].split(":")[1].split(";")[0];
+	const binary = atob(dataURI.split(",")[1]);
+	const array = [];
+	for (let i = 0; i < binary.length; i += 1) {
+		array.push(binary.charCodeAt(i));
+	}
+	return new Blob([new Uint8Array(array)], { type: mime });
+}
+
+export function loadImage(image: HTMLImageElement, src: string) {
+	return new Promise((resolve, reject) => {
+		const initSRC = image.src;
+		const img = image;
+		img.onload = resolve;
+		img.onerror = (err) => {
+			img.src = initSRC;
+			reject(err);
+		};
+		img.src = src;
+	});
+}
+
+export async function blobToImage(blob: Blob) {
+	const dataURL = URL.createObjectURL(blob);
+	const newImage = new Image();
+	await loadImage(newImage, dataURL);
+	return newImage;
+}
+
+export function canvasToImage(
+	canvas: HTMLCanvasElement,
+): Promise {
+	return new Promise((resolve, reject) => {
+		const image = new Image();
+
+		image.addEventListener("load", () => {
+			resolve(image);
+		});
+
+		image.addEventListener("error", (error) => {
+			reject(error);
+		});
+
+		image.src = canvas.toDataURL();
+	});
+}
+
+export function fileToImage(file: File): Promise {
+	return new Promise((resolve, reject) => {
+		const reader = new FileReader();
+		reader.onload = () => {
+			const image = new Image();
+			image.onload = () => {
+				resolve(image);
+			};
+			image.onerror = () => {
+				reject("无法加载图像。");
+			};
+			image.src = reader.result as string;
+		};
+		reader.onerror = () => {
+			reject("无法读取文件。");
+		};
+		reader.readAsDataURL(file);
+	});
+}
+
+export function srcToFile(src: string, fileName: string, mimeType: string) {
+	return fetch(src)
+		.then(function (res) {
+			return res.arrayBuffer();
+		})
+		.then(function (buf) {
+			return new File([buf], fileName, { type: mimeType });
+		});
+}
+
+export async function askWritePermission() {
+	try {
+		// The clipboard-write permission is granted automatically to pages
+		// when they are the active tab. So it's not required, but it's more safe.
+		const { state } = await navigator.permissions.query({
+			name: "clipboard-write" as PermissionName,
+		});
+		return state === "granted";
+	} catch (error) {
+		// Browser compatibility / Security error (ONLY HTTPS) ...
+		return false;
+	}
+}
+
+function canvasToBlob(canvas: HTMLCanvasElement, mime: string): Promise {
+	return new Promise((resolve, reject) =>
+		canvas.toBlob(async (d) => {
+			if (d) {
+				resolve(d);
+			} else {
+				reject(new Error("Expected toBlob() to be defined"));
+			}
+		}, mime),
+	);
+}
+
+const setToClipboard = async (blob: any) => {
+	const data = [new ClipboardItem({ [blob.type]: blob })];
+	await navigator.clipboard.write(data);
+};
+
+export function isRightClick(ev: SyntheticEvent) {
+	const mouseEvent = ev.nativeEvent as MouseEvent;
+	return mouseEvent.button === 2;
+}
+
+export function isMidClick(ev: SyntheticEvent) {
+	const mouseEvent = ev.nativeEvent as MouseEvent;
+	return mouseEvent.button === 1;
+}
+
+export async function copyCanvasImage(canvas: HTMLCanvasElement) {
+	const blob = await canvasToBlob(canvas, "image/png");
+	try {
+		await setToClipboard(blob);
+	} catch {
+		console.log("Copy image failed!");
+	}
+}
+
+export function downloadImage(uri: string, name: string) {
+	const link = document.createElement("a");
+	link.href = uri;
+	link.download = name;
+
+	// this is necessary as link.click() does not work on the latest firefox
+	link.dispatchEvent(
+		new MouseEvent("click", {
+			bubbles: true,
+			cancelable: true,
+			view: window,
+		}),
+	);
+
+	setTimeout(() => {
+		// For Firefox it is necessary to delay revoking the ObjectURL
+		// window.URL.revokeObjectURL(base64)
+		link.remove();
+	}, 100);
+}
+
+export function mouseXY(ev: SyntheticEvent) {
+	const mouseEvent = ev.nativeEvent as MouseEvent;
+	return { x: mouseEvent.offsetX, y: mouseEvent.offsetY };
+}
+
+export function drawLines(
+	ctx: CanvasRenderingContext2D,
+	lines: LineGroup,
+	color = BRUSH_COLOR,
+) {
+	ctx.strokeStyle = color;
+	ctx.lineCap = "round";
+	ctx.lineJoin = "round";
+
+	lines.forEach((line) => {
+		if (!line?.pts.length || !line.size) {
+			return;
+		}
+		ctx.lineWidth = line.size;
+		ctx.beginPath();
+		ctx.moveTo(line.pts[0].x, line.pts[0].y);
+		line.pts.forEach((pt) => ctx.lineTo(pt.x, pt.y));
+		ctx.stroke();
+	});
+}
+
+export const generateMask = (
+	imageWidth: number,
+	imageHeight: number,
+	lineGroups: LineGroup[],
+	maskImages: HTMLImageElement[] = [],
+	lineGroupsColor: string = "white",
+): HTMLCanvasElement => {
+	const maskCanvas = document.createElement("canvas");
+	maskCanvas.width = imageWidth;
+	maskCanvas.height = imageHeight;
+	const ctx = maskCanvas.getContext("2d");
+	if (!ctx) {
+		throw new Error("could not retrieve mask canvas");
+	}
+
+	maskImages.forEach((maskImage) => {
+		ctx.drawImage(maskImage, 0, 0, imageWidth, imageHeight);
+	});
+
+	lineGroups.forEach((lineGroup) => {
+		drawLines(ctx, lineGroup, lineGroupsColor);
+	});
+
+	return maskCanvas;
+};
+
+export const convertToBase64 = (fileOrBlob: File | Blob): Promise => {
+	return new Promise((resolve, reject) => {
+		const reader = new FileReader();
+		reader.onload = (event) => {
+			const base64String = event.target?.result as string;
+			resolve(base64String);
+		};
+		reader.onerror = (error) => {
+			reject(error);
+		};
+		reader.readAsDataURL(fileOrBlob);
+	});
+};
diff --git a/custom-demo/front-end/src/main.tsx b/custom-demo/front-end/src/main.tsx
index fce2cfe..3d97b86 100644
--- a/custom-demo/front-end/src/main.tsx
+++ b/custom-demo/front-end/src/main.tsx
@@ -1,22 +1,18 @@
-import React from "react"
-import ReactDOM from "react-dom/client"
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
-import "inter-ui/inter.css"
-import App from "./App.tsx"
-import "./globals.css"
-import { ThemeProvider } from "next-themes"
-import { TooltipProvider } from "./components/ui/tooltip.tsx"
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App.tsx";
+import "./globals.css";
 
-const queryClient = new QueryClient()
+import { MantineProvider } from "@mantine/core";
+import { Notifications } from "@mantine/notifications";
+import "@mantine/core/styles.css";
+import "@mantine/notifications/styles.css";
 
 ReactDOM.createRoot(document.getElementById("root")!).render(
-  
-    
-      
-        
-          
-        
-      
-    
-  
-)
+	
+		
+			
+			
+		
+	,
+);