You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by ku...@apache.org on 2024/05/23 07:57:24 UTC

(incubator-answer) 03/03: feat(plugin/embed): merge from dev and add embed plugin

This is an automated email from the ASF dual-hosted git repository.

kumfo pushed a commit to branch feat/1.3.5/embed
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit f972a23dd88dd2bd6adf2e8ef1c015c46a1186bd
Author: kumfo <ku...@sifou.com>
AuthorDate: Thu May 23 15:57:10 2024 +0800

    feat(plugin/embed): merge from dev and add embed plugin
---
 Dockerfile                                         |    2 +-
 Makefile                                           |    4 +-
 README.md                                          |    2 +-
 charts/values.yaml                                 |    2 +-
 cmd/answer/main.go                                 |    2 +-
 cmd/wire_gen.go                                    |    4 +-
 docs/docs.go                                       |  134 +-
 docs/swagger.json                                  |  134 +-
 docs/swagger.yaml                                  |   90 +-
 i18n/af_ZA.yaml                                    |    6 +-
 i18n/ar_SA.yaml                                    |    6 +-
 i18n/az_AZ.yaml                                    |    6 +-
 i18n/bal_BA.yaml                                   |    6 +-
 i18n/ban_ID.yaml                                   |    6 +-
 i18n/bn_BD.yaml                                    |    6 +-
 i18n/bs_BA.yaml                                    |    6 +-
 i18n/ca_ES.yaml                                    |    6 +-
 i18n/cs_CZ.yaml                                    |    8 +-
 i18n/cy_GB.yaml                                    |    8 +-
 i18n/da_DK.yaml                                    |    8 +-
 i18n/de_DE.yaml                                    |   48 +-
 i18n/el_GR.yaml                                    |    6 +-
 i18n/en_US.yaml                                    |    8 +-
 i18n/es_ES.yaml                                    |    6 +
 i18n/fa_IR.yaml                                    |   12 +-
 i18n/fi_FI.yaml                                    |    6 +-
 i18n/fr_FR.yaml                                    |   12 +-
 i18n/he_IL.yaml                                    |    6 +-
 i18n/hi_IN.yaml                                    |    8 +-
 i18n/hu_HU.yaml                                    |    6 +-
 i18n/hy_AM.yaml                                    |    6 +-
 i18n/id_ID.yaml                                    |    8 +-
 i18n/it_IT.yaml                                    |  116 +-
 i18n/ja_JP.yaml                                    |   14 +-
 i18n/ko_KR.yaml                                    |    8 +-
 i18n/nl_NL.yaml                                    |    6 +-
 i18n/no_NO.yaml                                    |    6 +-
 i18n/pl_PL.yaml                                    |   86 +-
 i18n/pt_PT.yaml                                    |   12 +-
 i18n/ro_RO.yaml                                    |   30 +-
 i18n/ru_RU.yaml                                    |   66 +-
 i18n/sk_SK.yaml                                    |    6 +
 i18n/sq_AL.yaml                                    |    6 +-
 i18n/sr_SP.yaml                                    |    6 +-
 i18n/sv_SE.yaml                                    |    8 +-
 i18n/te_IN.yaml                                    |    8 +-
 i18n/tr_TR.yaml                                    |    8 +-
 i18n/uk_UA.yaml                                    |   10 +-
 i18n/vi_VN.yaml                                    |    8 +-
 i18n/zh_CN.yaml                                    |    8 +-
 i18n/zh_TW.yaml                                    |   22 +-
 internal/controller/answer_controller.go           |    3 +
 internal/controller/controller.go                  |    2 +
 internal/controller/embed_controller.go            |   47 +
 ...{controller.go => plugin_captcha_controller.go} |   58 +-
 internal/controller/question_controller.go         |    5 +
 internal/controller/template_controller.go         |    5 +
 internal/controller/template_render/answer.go      |    4 +
 internal/controller/user_controller.go             |   33 +-
 internal/controller_admin/siteinfo_controller.go   |    2 +
 internal/migrations/init.go                        |  138 ++
 internal/repo/collection/collection_group_repo.go  |    1 -
 internal/repo/question/question_repo.go            |    1 -
 internal/repo/repo_test/tag_rel_repo_test.go       |   14 +-
 internal/repo/tag/tag_repo.go                      |    2 +-
 internal/repo/user/user_repo.go                    |    8 +-
 internal/router/answer_api_router.go               |    2 +-
 internal/router/plugin_api_router.go               |   10 +
 internal/schema/answer_schema.go                   |    2 +
 internal/schema/comment_schema.go                  |    9 +-
 internal/schema/plugin_option_schema.go            |    6 +
 internal/schema/question_schema.go                 |    4 +
 internal/schema/search_schema.go                   |   14 +-
 internal/schema/user_schema.go                     |   24 +-
 internal/schema/vote_schema.go                     |    8 +-
 internal/service/action/captcha_service.go         |  106 +-
 internal/service/action/captcha_strategy.go        |    8 +-
 internal/service/activity/activity.go              |    2 +-
 internal/service/content/answer_service.go         |   26 +-
 internal/service/content/question_service.go       |    9 +-
 internal/service/content/user_service.go           |   20 +-
 internal/service/rank/rank_service.go              |    2 +-
 internal/service/review/review_service.go          |   46 +-
 .../service/siteinfo_common/siteinfo_service.go    |    2 +
 internal/service/tag_common/tag_common.go          |   15 +-
 internal/service/user_common/user.go               |    5 +-
 pkg/converter/markdown.go                          |    2 +
 plugin/captcha.go                                  |   82 ++
 plugin/plugin.go                                   |   15 +
 plugin/reviewer.go                                 |   14 +
 script/plugin_list                                 |    3 +-
 ui/config-overrides.js                             |   67 +-
 ui/package.json                                    |   10 +-
 ui/pnpm-lock.yaml                                  | 1533 +++++++-------------
 ui/public/index.html                               |    4 +
 ui/src/components/Actions/index.tsx                |   80 +-
 .../components/Comment/components/Form/index.tsx   |    1 +
 ui/src/components/Comment/index.tsx                |  283 ++--
 ui/src/components/Editor/ToolBars/blockquote.tsx   |    2 +-
 ui/src/components/Editor/ToolBars/bold.tsx         |    4 +-
 ui/src/components/Editor/ToolBars/code.tsx         |    4 +-
 ui/src/components/Editor/ToolBars/formula.tsx      |  106 --
 ui/src/components/Editor/ToolBars/heading.tsx      |    4 +-
 ui/src/components/Editor/ToolBars/hr.tsx           |    4 +-
 ui/src/components/Editor/ToolBars/image.tsx        |   26 +-
 ui/src/components/Editor/ToolBars/index.ts         |    2 -
 ui/src/components/Editor/ToolBars/italic.tsx       |    4 +-
 ui/src/components/Editor/ToolBars/link.tsx         |    4 +-
 ui/src/components/Editor/ToolBars/ol.tsx           |    4 +-
 ui/src/components/Editor/ToolBars/ul.tsx           |    4 +-
 ui/src/components/Editor/types.ts                  |   48 +-
 ui/src/components/Editor/utils/extension.ts        |  255 ++++
 ui/src/components/Editor/utils/index.ts            |  186 +--
 .../Admin => components/Mentions}/index.scss       |    6 +-
 ui/src/components/Mentions/index.tsx               |   17 +-
 ui/src/components/Operate/index.tsx                |  107 +-
 ui/src/components/PageTags/index.tsx               |   25 +
 ui/src/components/SchemaForm/index.tsx             |    3 +
 ui/src/components/SideNav/index.scss               |    4 +-
 ui/src/components/Unactivate/index.tsx             |   16 +-
 ui/src/hooks/usePrompt/index.tsx                   |    1 +
 ui/src/hooks/useReportModal/index.tsx              |   63 +-
 ui/src/i18n/init.ts                                |   11 +-
 ui/src/pages/Admin/Answers/index.tsx               |    9 +-
 ui/src/pages/Admin/Branding/index.tsx              |    4 +-
 ui/src/pages/Admin/CssAndHtml/index.tsx            |    4 +-
 ui/src/pages/Admin/General/index.tsx               |    4 +-
 ui/src/pages/Admin/Interface/index.tsx             |    4 +-
 ui/src/pages/Admin/Legal/index.tsx                 |    4 +-
 ui/src/pages/Admin/Login/index.tsx                 |    4 +-
 ui/src/pages/Admin/Plugins/Installed/index.tsx     |    4 +-
 ui/src/pages/Admin/Privileges/index.tsx            |    8 +-
 ui/src/pages/Admin/Questions/index.tsx             |    7 +-
 ui/src/pages/Admin/Seo/index.tsx                   |    4 +-
 ui/src/pages/Admin/SettingsUsers/index.tsx         |    4 +-
 ui/src/pages/Admin/Smtp/index.tsx                  |    4 +-
 ui/src/pages/Admin/Themes/index.tsx                |    4 +-
 ui/src/pages/Admin/Users/index.tsx                 |    9 +-
 ui/src/pages/Admin/Write/index.tsx                 |    4 +-
 ui/src/pages/Admin/index.scss                      |   18 +
 ui/src/pages/Install/index.tsx                     |    9 +-
 ui/src/pages/Layout/index.tsx                      |    2 +-
 ui/src/pages/Questions/Ask/index.tsx               |  186 ++-
 .../Detail/components/InviteToAnswer/index.tsx     |   47 +-
 .../Detail/components/WriteAnswer/index.tsx        |  100 +-
 ui/src/pages/Questions/EditAnswer/index.tsx        |   76 +-
 ui/src/pages/Questions/index.tsx                   |    2 +-
 .../Review/components/ApproveDropdown/index.tsx    |   89 +-
 .../Review/components/EditPostModal/index.tsx      |   80 +-
 .../pages/Review/components/FlagContent/index.tsx  |    6 +-
 ui/src/pages/Search/index.tsx                      |   15 +-
 ui/src/pages/SideNavLayout/index.tsx               |    4 +-
 ui/src/pages/Tags/Create/index.tsx                 |   15 +-
 ui/src/pages/Timeline/components/Item/index.tsx    |    3 +-
 ui/src/pages/{Admin => Timeline}/index.scss        |   19 +-
 ui/src/pages/Timeline/index.tsx                    |    6 +-
 .../Users/AccountForgot/components/sendEmail.tsx   |   26 +-
 .../Users/ChangeEmail/components/sendEmail.tsx     |   21 +-
 ui/src/pages/Users/Login/index.tsx                 |   49 +-
 ui/src/pages/Users/OauthBindEmail/index.tsx        |    9 +-
 ui/src/pages/Users/PasswordReset/index.tsx         |   15 +-
 .../Users/Register/components/SignUpForm/index.tsx |   25 +-
 .../Account/components/ModifyEmail/index.tsx       |   37 +-
 .../Account/components/ModifyPass/index.tsx        |   35 +-
 ui/src/pages/Users/Settings/Profile/index.tsx      |   17 +-
 ui/src/plugins/builtin/HostingConnector/index.tsx  |    7 +-
 ui/src/plugins/builtin/SearchInfo/index.tsx        |    7 +-
 .../plugins/builtin/ThirdPartyConnector/index.tsx  |    7 +-
 ui/src/router/index.tsx                            |    4 +-
 ui/src/router/pathFactory.ts                       |    2 +-
 ui/src/services/client/user.ts                     |    7 +
 ui/src/utils/localize.ts                           |    2 +-
 ui/src/utils/pluginKit/index.ts                    |   85 +-
 .../index.scss => utils/pluginKit/interface.ts}    |   10 +-
 ui/src/utils/pluginKit/utils.ts                    |   22 +-
 175 files changed, 3357 insertions(+), 2447 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 6278e1a5..bd5f8768 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -34,7 +34,7 @@ ARG CGO_EXTRA_CFLAGS
 
 COPY . ${BUILD_DIR}
 WORKDIR ${BUILD_DIR}
-RUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm@latest \
+RUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm@8.9.2 \
     && make clean build
 
 RUN chmod 755 answer
diff --git a/Makefile b/Makefile
index 9da39bec..9205c033 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 .PHONY: build clean ui
 
-VERSION=1.3.0
+VERSION=1.3.1
 BIN=answer
 DIR_SRC=./cmd/answer
 DOCKER_CMD=docker
@@ -38,7 +38,7 @@ clean:
 
 install-ui-packages:
 	@corepack enable
-	@corepack prepare pnpm@latest --activate
+	@corepack prepare pnpm@8.9.2 --activate
 
 ui:
 	@cd ui && pnpm pre-install && pnpm build && cd -
diff --git a/README.md b/README.md
index c3b30b71..b2d89834 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache
 ### Running with docker
 
 ```bash
-docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.3.0
+docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.3.1
 ```
 
 For more information, see [Installation](https://answer.apache.org/docs/installation).
diff --git a/charts/values.yaml b/charts/values.yaml
index c7a9c20d..8bb36c46 100644
--- a/charts/values.yaml
+++ b/charts/values.yaml
@@ -35,7 +35,7 @@ env:
     # [DEBUG INFO WARN ERROR]
     value: "INFO"
   # uncomment the below values to use AUTO_INSTALL and not have to go through the setup process.
-  # Once used to do the initial setup, these variables wont be used moving forward.
+  # Once used to do the initial setup, these variables won't be used moving forward.
   # You must at a minimum comment AUTO_INSTALL after initial setup to prevent an error about the database already being initiated.
   # - name: AUTO_INSTALL
   #   value: "true"
diff --git a/cmd/answer/main.go b/cmd/answer/main.go
index f8e88bae..1ac3b237 100644
--- a/cmd/answer/main.go
+++ b/cmd/answer/main.go
@@ -20,7 +20,7 @@
 package main
 
 import (
-	_ "github.com/apache/incubator-answer-plugins/reviewer-basic"
+	_ "github.com/apache/incubator-answer-plugins/embed-basic"
 	answercmd "github.com/apache/incubator-answer/cmd"
 )
 
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 7ed904d0..1bad939a 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -262,7 +262,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
 	connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
 	userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
 	userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)
-	pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController)
+	captchaController := controller.NewCaptchaController()
+	embedController := controller.NewEmbedController()
+	pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController)
 	ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter)
 	scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
 	application := newApplication(serverConf, ginEngine, scheduledTaskManager)
diff --git a/docs/docs.go b/docs/docs.go
index 1c463c12..7d96f4b0 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -2653,6 +2653,44 @@ const docTemplate = `{
                 }
             }
         },
+        "/answer/api/v1/embed/config": {
+            "get": {
+                "description": "GetEmbedConfig",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PluginEmbed"
+                ],
+                "summary": "GetEmbedConfig",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/handler.RespBody"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/schema.GetEmbedOptionResp"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/answer/api/v1/file": {
             "post": {
                 "security": [
@@ -6188,9 +6226,9 @@ const docTemplate = `{
                 }
             }
         },
-        "/answer/api/v1/user/register/captcha": {
-            "get": {
-                "description": "UserRegisterCaptcha",
+        "/answer/api/v1/user/register/email": {
+            "post": {
+                "description": "UserRegisterByEmail",
                 "consumes": [
                     "application/json"
                 ],
@@ -6200,7 +6238,18 @@ const docTemplate = `{
                 "tags": [
                     "User"
                 ],
-                "summary": "UserRegisterCaptcha",
+                "summary": "UserRegisterByEmail",
+                "parameters": [
+                    {
+                        "description": "UserRegisterReq",
+                        "name": "data",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/schema.UserRegisterReq"
+                        }
+                    }
+                ],
                 "responses": {
                     "200": {
                         "description": "OK",
@@ -6223,9 +6272,14 @@ const docTemplate = `{
                 }
             }
         },
-        "/answer/api/v1/user/register/email": {
-            "post": {
-                "description": "UserRegisterByEmail",
+        "/answer/api/v1/user/staff": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "get user staff",
                 "consumes": [
                     "application/json"
                 ],
@@ -6235,15 +6289,15 @@ const docTemplate = `{
                 "tags": [
                     "User"
                 ],
-                "summary": "UserRegisterByEmail",
+                "summary": "get user staff",
                 "parameters": [
                     {
-                        "description": "UserRegisterReq",
+                        "description": "GetUserStaffReq",
                         "name": "data",
                         "in": "body",
                         "required": true,
                         "schema": {
-                            "$ref": "#/definitions/schema.UserRegisterReq"
+                            "$ref": "#/definitions/schema.GetUserStaffReq"
                         }
                     }
                 ],
@@ -6259,7 +6313,7 @@ const docTemplate = `{
                                     "type": "object",
                                     "properties": {
                                         "data": {
-                                            "$ref": "#/definitions/schema.UserLoginResp"
+                                            "$ref": "#/definitions/schema.GetUserStaffResp"
                                         }
                                     }
                                 }
@@ -6907,7 +6961,6 @@ const docTemplate = `{
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "mention_username_list": {
@@ -7590,6 +7643,17 @@ const docTemplate = `{
                 }
             }
         },
+        "schema.GetEmbedOptionResp": {
+            "type": "object",
+            "properties": {
+                "enable": {
+                    "type": "boolean"
+                },
+                "platform": {
+                    "type": "string"
+                }
+            }
+        },
         "schema.GetFollowingTagsResp": {
             "type": "object",
             "properties": {
@@ -8313,6 +8377,36 @@ const docTemplate = `{
                 }
             }
         },
+        "schema.GetUserStaffReq": {
+            "type": "object",
+            "properties": {
+                "page_size": {
+                    "type": "integer",
+                    "minimum": 1
+                },
+                "username": {
+                    "type": "string",
+                    "maxLength": 500
+                }
+            }
+        },
+        "schema.GetUserStaffResp": {
+            "type": "object",
+            "properties": {
+                "avatar": {
+                    "description": "avatar",
+                    "type": "string"
+                },
+                "display_name": {
+                    "description": "display name",
+                    "type": "string"
+                },
+                "username": {
+                    "description": "username",
+                    "type": "string"
+                }
+            }
+        },
         "schema.GetVoteWithPageResp": {
             "type": "object",
             "properties": {
@@ -8849,7 +8943,6 @@ const docTemplate = `{
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "comment_id": {
@@ -10097,12 +10190,10 @@ const docTemplate = `{
             ],
             "properties": {
                 "captcha_code": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "captcha_id": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "e_mail": {
                     "type": "string",
@@ -10267,12 +10358,10 @@ const docTemplate = `{
             ],
             "properties": {
                 "captcha_code": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "captcha_id": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "old_pass": {
                     "type": "string",
@@ -10420,15 +10509,12 @@ const docTemplate = `{
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "is_cancel": {
-                    "description": "is cancel",
                     "type": "boolean"
                 },
                 "object_id": {
-                    "description": "id",
                     "type": "string"
                 }
             }
diff --git a/docs/swagger.json b/docs/swagger.json
index 00b06d9a..a8fc8159 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -2642,6 +2642,44 @@
                 }
             }
         },
+        "/answer/api/v1/embed/config": {
+            "get": {
+                "description": "GetEmbedConfig",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PluginEmbed"
+                ],
+                "summary": "GetEmbedConfig",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/handler.RespBody"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/schema.GetEmbedOptionResp"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/answer/api/v1/file": {
             "post": {
                 "security": [
@@ -6177,9 +6215,9 @@
                 }
             }
         },
-        "/answer/api/v1/user/register/captcha": {
-            "get": {
-                "description": "UserRegisterCaptcha",
+        "/answer/api/v1/user/register/email": {
+            "post": {
+                "description": "UserRegisterByEmail",
                 "consumes": [
                     "application/json"
                 ],
@@ -6189,7 +6227,18 @@
                 "tags": [
                     "User"
                 ],
-                "summary": "UserRegisterCaptcha",
+                "summary": "UserRegisterByEmail",
+                "parameters": [
+                    {
+                        "description": "UserRegisterReq",
+                        "name": "data",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/schema.UserRegisterReq"
+                        }
+                    }
+                ],
                 "responses": {
                     "200": {
                         "description": "OK",
@@ -6212,9 +6261,14 @@
                 }
             }
         },
-        "/answer/api/v1/user/register/email": {
-            "post": {
-                "description": "UserRegisterByEmail",
+        "/answer/api/v1/user/staff": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "get user staff",
                 "consumes": [
                     "application/json"
                 ],
@@ -6224,15 +6278,15 @@
                 "tags": [
                     "User"
                 ],
-                "summary": "UserRegisterByEmail",
+                "summary": "get user staff",
                 "parameters": [
                     {
-                        "description": "UserRegisterReq",
+                        "description": "GetUserStaffReq",
                         "name": "data",
                         "in": "body",
                         "required": true,
                         "schema": {
-                            "$ref": "#/definitions/schema.UserRegisterReq"
+                            "$ref": "#/definitions/schema.GetUserStaffReq"
                         }
                     }
                 ],
@@ -6248,7 +6302,7 @@
                                     "type": "object",
                                     "properties": {
                                         "data": {
-                                            "$ref": "#/definitions/schema.UserLoginResp"
+                                            "$ref": "#/definitions/schema.GetUserStaffResp"
                                         }
                                     }
                                 }
@@ -6896,7 +6950,6 @@
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "mention_username_list": {
@@ -7579,6 +7632,17 @@
                 }
             }
         },
+        "schema.GetEmbedOptionResp": {
+            "type": "object",
+            "properties": {
+                "enable": {
+                    "type": "boolean"
+                },
+                "platform": {
+                    "type": "string"
+                }
+            }
+        },
         "schema.GetFollowingTagsResp": {
             "type": "object",
             "properties": {
@@ -8302,6 +8366,36 @@
                 }
             }
         },
+        "schema.GetUserStaffReq": {
+            "type": "object",
+            "properties": {
+                "page_size": {
+                    "type": "integer",
+                    "minimum": 1
+                },
+                "username": {
+                    "type": "string",
+                    "maxLength": 500
+                }
+            }
+        },
+        "schema.GetUserStaffResp": {
+            "type": "object",
+            "properties": {
+                "avatar": {
+                    "description": "avatar",
+                    "type": "string"
+                },
+                "display_name": {
+                    "description": "display name",
+                    "type": "string"
+                },
+                "username": {
+                    "description": "username",
+                    "type": "string"
+                }
+            }
+        },
         "schema.GetVoteWithPageResp": {
             "type": "object",
             "properties": {
@@ -8838,7 +8932,6 @@
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "comment_id": {
@@ -10086,12 +10179,10 @@
             ],
             "properties": {
                 "captcha_code": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "captcha_id": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "e_mail": {
                     "type": "string",
@@ -10256,12 +10347,10 @@
             ],
             "properties": {
                 "captcha_code": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "captcha_id": {
-                    "type": "string",
-                    "maxLength": 500
+                    "type": "string"
                 },
                 "old_pass": {
                     "type": "string",
@@ -10409,15 +10498,12 @@
                     "type": "string"
                 },
                 "captcha_id": {
-                    "description": "captcha_id",
                     "type": "string"
                 },
                 "is_cancel": {
-                    "description": "is cancel",
                     "type": "boolean"
                 },
                 "object_id": {
-                    "description": "id",
                     "type": "string"
                 }
             }
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index e9bb895e..a3c8f887 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -164,7 +164,6 @@ definitions:
       captcha_code:
         type: string
       captcha_id:
-        description: captcha_id
         type: string
       mention_username_list:
         description: '@ user id list'
@@ -652,6 +651,13 @@ definitions:
         description: website
         type: string
     type: object
+  schema.GetEmbedOptionResp:
+    properties:
+      enable:
+        type: boolean
+      platform:
+        type: string
+    type: object
   schema.GetFollowingTagsResp:
     properties:
       display_name:
@@ -1152,6 +1158,27 @@ definitions:
       slug_name:
         type: string
     type: object
+  schema.GetUserStaffReq:
+    properties:
+      page_size:
+        minimum: 1
+        type: integer
+      username:
+        maxLength: 500
+        type: string
+    type: object
+  schema.GetUserStaffResp:
+    properties:
+      avatar:
+        description: avatar
+        type: string
+      display_name:
+        description: display name
+        type: string
+      username:
+        description: username
+        type: string
+    type: object
   schema.GetVoteWithPageResp:
     properties:
       answer_id:
@@ -1524,7 +1551,6 @@ definitions:
       captcha_code:
         type: string
       captcha_id:
-        description: captcha_id
         type: string
       comment_id:
         description: comment id
@@ -2380,10 +2406,8 @@ definitions:
   schema.UserChangeEmailSendCodeReq:
     properties:
       captcha_code:
-        maxLength: 500
         type: string
       captcha_id:
-        maxLength: 500
         type: string
       e_mail:
         maxLength: 500
@@ -2504,10 +2528,8 @@ definitions:
   schema.UserModifyPasswordReq:
     properties:
       captcha_code:
-        maxLength: 500
         type: string
       captcha_id:
-        maxLength: 500
         type: string
       old_pass:
         maxLength: 32
@@ -2611,13 +2633,10 @@ definitions:
       captcha_code:
         type: string
       captcha_id:
-        description: captcha_id
         type: string
       is_cancel:
-        description: is cancel
         type: boolean
       object_id:
-        description: id
         type: string
     required:
     - object_id
@@ -4222,6 +4241,28 @@ paths:
       summary: unbind external user login
       tags:
       - PluginConnector
+  /answer/api/v1/embed/config:
+    get:
+      consumes:
+      - application/json
+      description: GetEmbedConfig
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/handler.RespBody'
+            - properties:
+                data:
+                  items:
+                    $ref: '#/definitions/schema.GetEmbedOptionResp'
+                  type: array
+              type: object
+      summary: GetEmbedConfig
+      tags:
+      - PluginEmbed
   /answer/api/v1/file:
     post:
       consumes:
@@ -6373,11 +6414,18 @@ paths:
       summary: get user ranking
       tags:
       - User
-  /answer/api/v1/user/register/captcha:
-    get:
+  /answer/api/v1/user/register/email:
+    post:
       consumes:
       - application/json
-      description: UserRegisterCaptcha
+      description: UserRegisterByEmail
+      parameters:
+      - description: UserRegisterReq
+        in: body
+        name: data
+        required: true
+        schema:
+          $ref: '#/definitions/schema.UserRegisterReq'
       produces:
       - application/json
       responses:
@@ -6390,21 +6438,21 @@ paths:
                 data:
                   $ref: '#/definitions/schema.UserLoginResp'
               type: object
-      summary: UserRegisterCaptcha
+      summary: UserRegisterByEmail
       tags:
       - User
-  /answer/api/v1/user/register/email:
-    post:
+  /answer/api/v1/user/staff:
+    get:
       consumes:
       - application/json
-      description: UserRegisterByEmail
+      description: get user staff
       parameters:
-      - description: UserRegisterReq
+      - description: GetUserStaffReq
         in: body
         name: data
         required: true
         schema:
-          $ref: '#/definitions/schema.UserRegisterReq'
+          $ref: '#/definitions/schema.GetUserStaffReq'
       produces:
       - application/json
       responses:
@@ -6415,9 +6463,11 @@ paths:
             - $ref: '#/definitions/handler.RespBody'
             - properties:
                 data:
-                  $ref: '#/definitions/schema.UserLoginResp'
+                  $ref: '#/definitions/schema.GetUserStaffResp'
               type: object
-      summary: UserRegisterByEmail
+      security:
+      - ApiKeyAuth: []
+      summary: get user staff
       tags:
       - User
   /answer/api/v1/vote/down:
diff --git a/i18n/af_ZA.yaml b/i18n/af_ZA.yaml
index 89c34f73..8c6d0d7d 100644
--- a/i18n/af_ZA.yaml
+++ b/i18n/af_ZA.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ar_SA.yaml b/i18n/ar_SA.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/ar_SA.yaml
+++ b/i18n/ar_SA.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/az_AZ.yaml b/i18n/az_AZ.yaml
index 483744f7..07e50541 100644
--- a/i18n/az_AZ.yaml
+++ b/i18n/az_AZ.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bal_BA.yaml b/i18n/bal_BA.yaml
index 483744f7..07e50541 100644
--- a/i18n/bal_BA.yaml
+++ b/i18n/bal_BA.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ban_ID.yaml b/i18n/ban_ID.yaml
index 483744f7..07e50541 100644
--- a/i18n/ban_ID.yaml
+++ b/i18n/ban_ID.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bn_BD.yaml b/i18n/bn_BD.yaml
index 483744f7..07e50541 100644
--- a/i18n/bn_BD.yaml
+++ b/i18n/bn_BD.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bs_BA.yaml b/i18n/bs_BA.yaml
index 483744f7..07e50541 100644
--- a/i18n/bs_BA.yaml
+++ b/i18n/bs_BA.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ca_ES.yaml b/i18n/ca_ES.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/ca_ES.yaml
+++ b/i18n/ca_ES.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/cs_CZ.yaml b/i18n/cs_CZ.yaml
index e71eeda4..eac0631a 100644
--- a/i18n/cs_CZ.yaml
+++ b/i18n/cs_CZ.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/cy_GB.yaml b/i18n/cy_GB.yaml
index a1c14f5f..2af2fde0 100644
--- a/i18n/cy_GB.yaml
+++ b/i18n/cy_GB.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/da_DK.yaml b/i18n/da_DK.yaml
index 0d48719d..2a7cf3ee 100644
--- a/i18n/da_DK.yaml
+++ b/i18n/da_DK.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/de_DE.yaml b/i18n/de_DE.yaml
index 7ac93bfd..690f68e0 100644
--- a/i18n/de_DE.yaml
+++ b/i18n/de_DE.yaml
@@ -78,10 +78,10 @@ backend:
         other: Level 1 (weniger Reputation für privates Team, Gruppen)
     level_2:
       description:
-        other: Level 2 (niedrige Reputation erforderlich für Start-Community)
+        other: Level 2 (niedrige Reputation für Startup-Community)
     level_3:
       description:
-        other: Level 3 (hohe Reputation, für ausgereifte Community)
+        other: Level 3 (hohe Reputation für eine reife Community)
     level_custom:
       description:
         other: Benutzerdefinierter Level
@@ -142,7 +142,7 @@ backend:
   error:
     common:
       invalid_url:
-        other: Invalid URL.
+        other: Ungültige URL.
     password:
       space_invalid:
         other: Passwort darf keine Leerzeichen enthalten.
@@ -204,7 +204,7 @@ backend:
       already_deleted:
         other: Dieser Beitrag wurde gelöscht.
       under_review:
-        other: Your post is awaiting review. It will be visible after it has been approved.
+        other: Ihr Beitrag wartet auf Überprüfung. Er wird sichtbar sein, nachdem er genehmigt wurde.
       not_found:
         other: Frage nicht gefunden.
       cannot_deleted:
@@ -491,14 +491,14 @@ backend:
     accepted:
       other: akzeptiert
     edit:
-      other: edit
+      other: bearbeiten
   review:
     queued_post:
       other: Queued post
     flagged_post:
-      other: Flagged post
+      other: Beiträge gemeldet
     suggested_post_edit:
-      other: Suggested edits
+      other: Änderungsvorschläge
 # The following fields are used for interface presentation(Front-end)
 ui:
   how_to_format:
@@ -680,8 +680,8 @@ ui:
       empty: Kann nicht leer sein.
     msg:
       empty: Bitte wähle einen Grund aus.
-      not_a_url: URL format is incorrect.
-      url_not_match: URL origin does not match the current website.
+      not_a_url: URL hat ein falsches Format.
+      url_not_match: URL-Ursprung stimmt nicht mit der aktuellen Website überein.
   tag_modal:
     title: Neuen Tag erstellen
     form:
@@ -793,10 +793,10 @@ ui:
     no_desc: Der Tag hat keine Beschreibung.
     more: Mehr
   ask:
-    title: Frage hinzufügen
+    title: Frage stellen
     edit_title: Frage bearbeiten
     default_reason: Frage bearbeiten
-    default_first_reason: Frage hinzufügen
+    default_first_reason: Frage stellen
     similar_questions: Ähnliche Fragen
     form:
       fields:
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         Wir haben eine E-Mail an diese Adresse geschickt. Bitte befolge die Anweisungen zur Bestätigung.
       email:
+        label: E-Mail
+      new_email:
         label: Neue E-Mail
         msg: Neue E-Mail darf nicht leer sein.
       pass:
@@ -1038,8 +1040,8 @@ ui:
     title: Verwandte Fragen
     answers: antworten
   invite_to_answer:
-    title: Menschen gefragt
-    desc: Wähle Leute aus, von denen du glaubst, dass sie die Antwort wissen könnten.
+    title: Frage jemanden
+    desc: Lade Leute ein, von denen du glaubst, dass sie die Antwort wissen könnten.
     invite: Zur Antwort einladen
     add: Personen hinzufügen
     search: Personen suchen
@@ -1049,7 +1051,7 @@ ui:
     asked: gefragt
     update: Geändert
     edit: bearbeitet
-    commented: commented
+    commented: kommentiert
     Views: Gesehen
     Follow: Folgen
     Following: Folgend
@@ -1067,7 +1069,7 @@ ui:
       title: Antworten
       score: Punkte
       newest: Neueste
-      oldest: Oldest
+      oldest: Älteste
       btn_accept: Akzeptieren
       btn_accepted: Akzeptiert
     write_answer:
@@ -1092,8 +1094,8 @@ ui:
       title: Diesen Beitrag erneut öffnen
       content: Bist du sicher, dass du wieder öffnen willst?
     list:
-      confirm_btn: List
-      title: List this post
+      confirm_btn: Liste
+      title: Diesen Beitrag auflisten
       content: Are you sure you want to list?
     unlist:
       confirm_btn: Unlist
@@ -1127,7 +1129,7 @@ ui:
     signup: Registrieren
     logout: Ausloggen
     verify: Überprüfen
-    add_question: Frage hinzufügen
+    add_question: Frage stellen
     approve: Genehmigen
     reject: Ablehnen
     skip: Überspringen
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Suchergebnisse
     keywords: Schlüsselwörter
@@ -1257,7 +1260,7 @@ ui:
     downvoted: negativ bewertet
     mod_short: MOD
     mod_long: Moderatoren
-    x_reputation: Ansehen
+    x_reputation: ansehen
     x_votes: Stimmen erhalten
     x_answers: Antworten
     x_questions: Fragen
@@ -1489,7 +1492,7 @@ ui:
       btn_submit: Senden
     users:
       title: Benutzer
-      name: Namen
+      name: Name
       email: E-Mail
       reputation: Ansehen
       created_at: Erstellt
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Benötigtes Reputations-Level
         text: Wähle die für die Privilegien erforderliche Reputation aus
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: kann nicht leer sein
@@ -1841,7 +1847,7 @@ ui:
     no_data: "Wir konnten nichts finden."
   users:
     title: Benutzer
-    users_with_the_most_reputation: Benutzer mit den höchsten Ansehenspunkten dieser Woche
+    users_with_the_most_reputation: Benutzer mit den höchsten Reputationspunkten dieser Woche
     users_with_the_most_vote: Benutzer, die diese Woche am meisten gestimmt haben
     staffs: Unsere Community Teammitglieder
     reputation: Ansehen
diff --git a/i18n/el_GR.yaml b/i18n/el_GR.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/el_GR.yaml
+++ b/i18n/el_GR.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 943608dd..58bc6c77 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1029,6 +1029,8 @@ ui:
         We've sent an email to that address. Please follow the confirmation
         instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1197,6 +1199,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1213,7 +1216,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1817,6 +1820,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1 : number should be equal or larger than 1
 
   form:
     optional: (optional)
diff --git a/i18n/es_ES.yaml b/i18n/es_ES.yaml
index 2ffbe74c..a2811cfd 100644
--- a/i18n/es_ES.yaml
+++ b/i18n/es_ES.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         Te hemos enviado un email a esa dirección. Por favor sigue las instrucciones de confirmación.
       email:
+        label: Correo
+      new_email:
         label: Nuevo correo
         msg: El nuevo correo no puede estar vacío.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Resultados de la búsqueda
     keywords: Palabras claves
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Nivel de reputación requerido
         text: Elegir reputación requerida para los privilegios
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (opcional)
     empty: no puede estar en blanco
diff --git a/i18n/fa_IR.yaml b/i18n/fa_IR.yaml
index 0f52761b..589779ea 100644
--- a/i18n/fa_IR.yaml
+++ b/i18n/fa_IR.yaml
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         ما یک ایمیل به آن آدرس ارسال کردیم. لطفا مراحل تایید را طی کنید.
       email:
-        label: ایمیل جدید
-        msg: ایمیل جدید نباید خالی باشد.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: رمز عبور فعلی
         msg: رمز عبور نمی تواند خالی باشد.
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: نتایج جستجو
     keywords: کلیدواژه ها
@@ -1176,7 +1179,7 @@ ui:
       more: بیشتر
     tips:
       title: گزینه های پیشرفته جستجو
-      tag: "<1>[tag]</1> جستجو با یک تگ"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> جستجو براساس نویسنده"
       answer: "<1>answers:0</1> سوال بی جواب"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/fi_FI.yaml b/i18n/fi_FI.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/fi_FI.yaml
+++ b/i18n/fi_FI.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/fr_FR.yaml b/i18n/fr_FR.yaml
index a389a08e..ba633f73 100644
--- a/i18n/fr_FR.yaml
+++ b/i18n/fr_FR.yaml
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Nous vous avons envoyé un mail à cette adresse. Merci de suivre les instructions.
       email:
-        label: Nouvel e-mail
-        msg: L'email ne peut pas être vide.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Mot de passe actuel
         msg: Le mot de passe ne peut pas être vide.
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Résultats de la recherche
     keywords: Mots-clés
@@ -1176,7 +1179,7 @@ ui:
       more: Plus
     tips:
       title: Astuces de recherche avancée
-      tag: "<1>[tag]</1> recherche à l'aide d'un tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>utilisateur:username</1> recherche par auteur"
       answer: "<1>réponses:0</1> questions sans réponses"
       score: "<1>score:3</1> messages avec plus de 3 points"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Niveau de réputation requis
         text: Choisissez la réputation requise pour les privilèges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optionnel)
     empty: ne peut pas être vide
diff --git a/i18n/he_IL.yaml b/i18n/he_IL.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/he_IL.yaml
+++ b/i18n/he_IL.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/hi_IN.yaml b/i18n/hi_IN.yaml
index 0d48719d..2a7cf3ee 100644
--- a/i18n/hi_IN.yaml
+++ b/i18n/hi_IN.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/hu_HU.yaml b/i18n/hu_HU.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/hu_HU.yaml
+++ b/i18n/hu_HU.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/hy_AM.yaml b/i18n/hy_AM.yaml
index 483744f7..07e50541 100644
--- a/i18n/hy_AM.yaml
+++ b/i18n/hy_AM.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml
index de2f95c4..4b3ae49e 100644
--- a/i18n/id_ID.yaml
+++ b/i18n/id_ID.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml
index 52b3d016..3b04a3b6 100644
--- a/i18n/it_IT.yaml
+++ b/i18n/it_IT.yaml
@@ -142,7 +142,7 @@ backend:
   error:
     common:
       invalid_url:
-        other: Invalid URL.
+        other: URL non valido.
     password:
       space_invalid:
         other: La password non può contenere spazi.
@@ -204,7 +204,7 @@ backend:
       already_deleted:
         other: Questo post è stato eliminato.
       under_review:
-        other: Your post is awaiting review. It will be visible after it has been approved.
+        other: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato.
       not_found:
         other: domanda non trovata
       cannot_deleted:
@@ -491,14 +491,14 @@ backend:
     accepted:
       other: Accettato
     edit:
-      other: edit
+      other: modifica
   review:
     queued_post:
-      other: Queued post
+      other: Post in coda
     flagged_post:
-      other: Flagged post
+      other: Post contrassegnato
     suggested_post_edit:
-      other: Suggested edits
+      other: Modifiche suggerite
 # The following fields are used for interface presentation(Front-end)
 ui:
   how_to_format:
@@ -680,8 +680,8 @@ ui:
       empty: Non può essere vuoto.
     msg:
       empty: Per favore seleziona un motivo
-      not_a_url: URL format is incorrect.
-      url_not_match: URL origin does not match the current website.
+      not_a_url: Formato URL errato.
+      url_not_match: L'origine dell'URL non corrisponde al sito web corrente.
   tag_modal:
     title: Crea un nuovo tag
     form:
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Abbiamo inviato una mail a quell'indirizzo. Si prega di seguire le istruzioni di conferma.
       email:
-        label: Nuova email
-        msg: La nuova email non può essere vuota.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Password attuale
         msg: La password non può essere vuota
@@ -1049,7 +1051,7 @@ ui:
     asked: chiesto
     update: Modificato
     edit: modificato
-    commented: commented
+    commented: commentato
     Views: Visualizzati
     Follow: Segui
     Following: Segui già
@@ -1067,7 +1069,7 @@ ui:
       title: Risposte
       score: Punteggio
       newest: Più recenti
-      oldest: Oldest
+      oldest: Meno recente
       btn_accept: Accetta
       btn_accepted: Accettato
     write_answer:
@@ -1092,13 +1094,13 @@ ui:
       title: Riapri questo post
       content: Sei sicuro di voler riaprire?
     list:
-      confirm_btn: List
-      title: List this post
-      content: Are you sure you want to list?
+      confirm_btn: Listare
+      title: Lista questo post
+      content: Sei sicuro di volerlo listare?
     unlist:
-      confirm_btn: Unlist
-      title: Unlist this post
-      content: Are you sure you want to unlist?
+      confirm_btn: Rimuovi dall'elenco
+      title: Rimuovi questo post
+      content: Sei sicuro di voler rimuovere dall'elenco?
     pin:
       title: Fissa questo post in cima al profilo
       content: Sei sicuro di voler fissare in blocco? Questo post apparirà in cima a tutte le liste.
@@ -1120,9 +1122,9 @@ ui:
     save: Salva
     delete: Elimina
     undelete: Ripristina
-    list: List
-    unlist: Unlist
-    unlisted: Unlisted
+    list: Aggiungi all'elenco
+    unlist: Rimuovi dall'elenco
+    unlisted: Rimosso dall'elenco
     login: Accedi
     signup: Registrati
     logout: Disconnetti
@@ -1153,13 +1155,14 @@ ui:
     reset: Resetta
     tag: Tag
     post_lowercase: post
-    filter: Filter
-    ignore: Ignore
-    submit: Submit
-    normal: Normal
-    closed: Closed
-    deleted: Deleted
-    pending: Pending
+    filter: 'Filtra'
+    ignore: Ignora
+    submit: Invia
+    normal: Normale
+    closed: Chiuso
+    deleted: Eliminato
+    pending: In attesa
+    more: More
   search:
     title: Risultati della ricerca
     keywords: Parole chiave
@@ -1176,7 +1179,7 @@ ui:
       more: Altro
     tips:
       title: Suggerimenti per ricerca avanzata
-      tag: "<1>[tag]</1> cerca dentro un tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> ricerca per autore"
       answer: "<1>risposte:0</1> domande senza risposta"
       score: "<1>punteggio:3</1> messaggi con un punteggio di 3+"
@@ -1406,8 +1409,8 @@ ui:
       votes: "Voti:"
       users: "Utenti:"
       flags: "Flags"
-      reviews: "Reviews:"
-      site_health: Site health
+      reviews: "Revisioni"
+      site_health: Stato del sito
       version: "Versione:"
       https: "HTTPS:"
       upload_folder: "Carica Cartella"
@@ -1422,7 +1425,7 @@ ui:
       database_size: "Dimensioni del database"
       storage_used: "Spazio di archiviazione utilizzato:"
       uptime: "Tempo di attività:"
-      links: Links
+      links: Collegamenti
       plugins: Plugin
       github: GitHub
       blog: Blog
@@ -1530,7 +1533,7 @@ ui:
         content: Un utente sospeso non può accedere.
     questions:
       page_title: Domande
-      unlisted: Unlisted
+      unlisted: Rimosso dall'elenco
       post: Posta
       votes: Voti
       answers: Risposte
@@ -1538,7 +1541,7 @@ ui:
       status: Stato
       action: Azione
       change: Modifica
-      pending: Pending
+      pending: In attesa
       filter:
         placeholder: "Filtra per titolo, domanda:id"
     answers:
@@ -1576,8 +1579,8 @@ ui:
         validate: Email di contatto non valida.
         text: Indirizzo e-mail del contatto chiave responsabile di questo sito.
       check_update:
-        label: Software updates
-        text: Automatically check for updates
+        label: Aggiornamenti Software
+        text: Controlla automaticamente gli aggiornamenti
     interface:
       page_title: Interfaccia
       language:
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Livello di reputazione richiesto 
         text: Scegli la reputazione richiesta per i privilegi
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (opzionale)
     empty: non può essere vuoto
@@ -1792,22 +1798,22 @@ ui:
     edit_answer: Edita la risposta
     edit_tag: Edita  tag
     empty: Nessuna attività di revisione rimasta.
-    approve_revision_tip: Do you approve this revision?
-    approve_flag_tip: Do you approve this flag?
-    approve_post_tip: Do you approve this post?
-    approve_user_tip: Do you approve this user?
-    suggest_edits: Suggested edits
-    flag_post: Flag post
-    flag_user: Flag user
-    queued_post: Queued post
-    queued_user: Queued user
-    filter_label: Type
-    reputation: reputation
-    flag_post_type: Flagged this post as {{ type }}.
-    flag_user_type: Flagged this user as {{ type }}.
-    edit_post: Edit post
-    list_post: List post
-    unlist_post: Unlist post
+    approve_revision_tip: Approvi questa revisione?
+    approve_flag_tip: Approvi questo contrassegno?
+    approve_post_tip: Approvi questo post?
+    approve_user_tip: Approvate questo utente?
+    suggest_edits: Modifiche suggerite
+    flag_post: Post contrassegnato
+    flag_user: Utente contrassegno
+    queued_post: "Posta in coda\n"
+    queued_user: Utente in coda
+    filter_label: Digita
+    reputation: reputazione
+    flag_post_type: Post contrassegnato come {{ type }}.
+    flag_user_type: Utente contrassegnato come {{ type }}.
+    edit_post: Modifica il post
+    list_post: Lista il post
+    unlist_post: Rimuovi post
   timeline:
     undeleted: Non cancellato
     deleted: eliminato
@@ -1858,6 +1864,6 @@ ui:
     post_hide_list: Questo post è stato nascosto dall'elenco.
     post_show_list: Questo post è stato mostrato in elenco.
     post_reopen: Questo post è stato riaperto.
-    post_list: This post has been listed.
-    post_unlist: This post has been unlisted.
-    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.
+    post_list: Questo post è stato inserito.
+    post_unlist: Questo post è stato rimosso.
+    post_pending: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato.
diff --git a/i18n/ja_JP.yaml b/i18n/ja_JP.yaml
index d1452052..10859efd 100644
--- a/i18n/ja_JP.yaml
+++ b/i18n/ja_JP.yaml
@@ -518,7 +518,7 @@ ui:
     edit_tag: タグを編集
     ask_a_question: 質問を追加
     edit_question: 質問を編集
-    edit_answer: 回答を編集 
+    edit_answer: 回答を編集
     search: 検索
     posts_containing: Posts containing
     settings: 設定
@@ -763,8 +763,8 @@ ui:
       Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.
     tip_vote: It adds something useful to the post
   edit_answer:
-    title: 回答を編集 
-    default_reason: 回答を編集 
+    title: 回答を編集
+    default_reason: 回答を編集
     default_first_reason: 回答を追加
     form:
       fields:
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/ko_KR.yaml b/i18n/ko_KR.yaml
index 1e584537..ab5db7de 100644
--- a/i18n/ko_KR.yaml
+++ b/i18n/ko_KR.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/nl_NL.yaml b/i18n/nl_NL.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/nl_NL.yaml
+++ b/i18n/nl_NL.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/no_NO.yaml
+++ b/i18n/no_NO.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/pl_PL.yaml b/i18n/pl_PL.yaml
index b33392da..aeb867dc 100644
--- a/i18n/pl_PL.yaml
+++ b/i18n/pl_PL.yaml
@@ -142,7 +142,7 @@ backend:
   error:
     common:
       invalid_url:
-        other: Invalid URL.
+        other: Nieprawidłowy URL.
     password:
       space_invalid:
         other: Hasło nie może zawierać spacji.
@@ -204,7 +204,7 @@ backend:
       already_deleted:
         other: Ten post został usunięty.
       under_review:
-        other: Your post is awaiting review. It will be visible after it has been approved.
+        other: Twój post oczekuje na recenzje. Będzie widoczny po jej akceptacji.
       not_found:
         other: Pytanie nie zostało odnalezione.
       cannot_deleted:
@@ -491,14 +491,14 @@ backend:
     accepted:
       other: zaakceptowane
     edit:
-      other: edit
+      other: edytuj
   review:
     queued_post:
-      other: Queued post
+      other: Post w kolejce
     flagged_post:
-      other: Flagged post
+      other: Post oznaczony
     suggested_post_edit:
-      other: Suggested edits
+      other: Sugerowane zmiany
 # The following fields are used for interface presentation(Front-end)
 ui:
   how_to_format:
@@ -680,8 +680,8 @@ ui:
       empty: Nie może być puste.
     msg:
       empty: Proszę wybrać powód.
-      not_a_url: URL format is incorrect.
-      url_not_match: URL origin does not match the current website.
+      not_a_url: Format adresu URL jest nieprawidłowy.
+      url_not_match: Wskazany URL nie pasuje do bieżącej witryny.
   tag_modal:
     title: Utwórz nowy tag
     form:
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Wysłaliśmy e-mail na ten adres. Prosimy postępować zgodnie z instrukcjami potwierdzającymi.
       email:
-        label: Nowy email
-        msg: Nowy adres email nie może być pusty.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Aktualne hasło
         msg: Hasło nie może być puste.
@@ -1049,7 +1051,7 @@ ui:
     asked: zadał(a)
     update: Zmodyfikowane
     edit: edytowany
-    commented: commented
+    commented: skomentowano
     Views: Wyświetlone
     Follow: Obserwuj
     Following: Obserwuje
@@ -1067,7 +1069,7 @@ ui:
       title: Odpowiedzi
       score: Ocena
       newest: Najnowsze
-      oldest: Oldest
+      oldest: Najstarsze
       btn_accept: Akceptuj
       btn_accepted: Zaakceptowane
     write_answer:
@@ -1092,13 +1094,13 @@ ui:
       title: Otwórz ponownie ten post
       content: Czy na pewno chcesz go ponownie otworzyć?
     list:
-      confirm_btn: List
-      title: List this post
+      confirm_btn: Lista
+      title: Pokaż ten post
       content: Are you sure you want to list?
     unlist:
-      confirm_btn: Unlist
-      title: Unlist this post
-      content: Are you sure you want to unlist?
+      confirm_btn: Usuń z listy
+      title: Usuń ten post z listy
+      content: Czy na pewno chcesz usunąć z listy?
     pin:
       title: Przypnij ten post
       content: Czy na pewno chcesz przypiąć go globalnie? Ten post będzie wyświetlany na górze wszystkich list postów.
@@ -1120,7 +1122,7 @@ ui:
     save: Zapisz
     delete: Usuń
     undelete: Przywróć
-    list: List
+    list: Lista
     unlist: Unlist
     unlisted: Unlisted
     login: Zaloguj się
@@ -1155,11 +1157,12 @@ ui:
     post_lowercase: post
     filter: Filter
     ignore: Ignore
-    submit: Submit
-    normal: Normal
-    closed: Closed
-    deleted: Deleted
-    pending: Pending
+    submit: Prześlij
+    normal: Normalny
+    closed: Zamknięty
+    deleted: Usunięty
+    pending: Oczekujący
+    more: More
   search:
     title: Wyniki wyszukiwania
     keywords: Słowa kluczowe
@@ -1176,7 +1179,7 @@ ui:
       more: Więcej
     tips:
       title: Porady dotyczące wyszukiwania zaawansowanego
-      tag: "<1>[tag]</1> wyszukiwanie w obrębie tagu"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> wyszukiwanie według autora"
       answer: "<1> answers:0 </1> pytania bez odpowiedzi"
       score: "<1>score:3</1> posty z oceną 3+"
@@ -1576,8 +1579,8 @@ ui:
         validate: Email kontaktowy nie jest poprawny.
         text: Adres email głównego kontaktu odpowiedzialnego za tę stronę.
       check_update:
-        label: Software updates
-        text: Automatically check for updates
+        label: Aktualizacjia oprogramowania
+        text: Automatycznie sprawdzaj dostępność aktualizacji
     interface:
       page_title: Interfejs
       language:
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Wymagany poziom reputacji
         text: Wybierz reputację wymaganą dla uprawnień
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (opcjonalne)
     empty: nie może być puste
@@ -1792,20 +1798,20 @@ ui:
     edit_answer: Edytuj odpowiedź
     edit_tag: Edytuj tag
     empty: Nie ma więcej zadań do przeglądu.
-    approve_revision_tip: Do you approve this revision?
-    approve_flag_tip: Do you approve this flag?
-    approve_post_tip: Do you approve this post?
-    approve_user_tip: Do you approve this user?
+    approve_revision_tip: Czy akceptujesz tę wersję?
+    approve_flag_tip: Czy akceptujesz tę flagę?
+    approve_post_tip: Czy zatwierdzasz ten post?
+    approve_user_tip: Czy zatwierdzasz tego użytkownika?
     suggest_edits: Suggested edits
-    flag_post: Flag post
-    flag_user: Flag user
+    flag_post: Oznacz wpis
+    flag_user: Oznacz użytkownika
     queued_post: Queued post
     queued_user: Queued user
-    filter_label: Type
-    reputation: reputation
-    flag_post_type: Flagged this post as {{ type }}.
-    flag_user_type: Flagged this user as {{ type }}.
-    edit_post: Edit post
+    filter_label: Typ
+    reputation: reputacja
+    flag_post_type: Oznaczono ten wpis jako {{ type }}.
+    flag_user_type: Oznaczono tego użytkownika jako {{ type }}.
+    edit_post: Edytuj wpis
     list_post: List post
     unlist_post: Unlist post
   timeline:
@@ -1858,6 +1864,6 @@ ui:
     post_hide_list: Ten post został ukryty na liście.
     post_show_list: Ten post został wyświetlony na liście.
     post_reopen: Ten post został ponownie otwarty.
-    post_list: This post has been listed.
-    post_unlist: This post has been unlisted.
-    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.
+    post_list: Ten wpis został umieszczony na liście.
+    post_unlist: Ten wpis został usunięty z listy.
+    post_pending: Twój wpis oczekuje na recenzje. Będzie widoczny po jej akceptacji.
diff --git a/i18n/pt_PT.yaml b/i18n/pt_PT.yaml
index 8142d538..b88b9a10 100644
--- a/i18n/pt_PT.yaml
+++ b/i18n/pt_PT.yaml
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Enviamos um e-mail para esse endereço. Siga as instruções de confirmação.
       email:
-        label: E-mail
-        msg: E-mail não pode ser vazio.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Senha atual
         msg: Senha não pode ser vazio.
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Procurar Resultados
     keywords: Palavras-chave
@@ -1176,7 +1179,7 @@ ui:
       more: Mais
     tips:
       title: Dicas de Pesquisa Avançada
-      tag: "<1>[tag]</1> pesquisar dentro de um marcador"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> buscar por autor"
       answer: "<1>answers:0</1> perguntas não respondidas"
       score: "<1>score:3</1> postagens com mais de 3+ placares"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Nível de reputação necessário
         text: Escolha a reputação necessária para os privilégios
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (opcional)
     empty: não pode ser vazio
diff --git a/i18n/ro_RO.yaml b/i18n/ro_RO.yaml
index 9f3ec8b6..2745023b 100644
--- a/i18n/ro_RO.yaml
+++ b/i18n/ro_RO.yaml
@@ -491,12 +491,12 @@ backend:
     accepted:
       other: acceptat
     edit:
-      other: edit
+      other: editează
   review:
     queued_post:
-      other: Queued post
+      other: Posturi în așteptare
     flagged_post:
-      other: Flagged post
+      other: Postare marcată
     suggested_post_edit:
       other: Suggested edits
 # The following fields are used for interface presentation(Front-end)
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Am trimis un e-mail la acea adresă. Vă rugăm să urmați instrucțiunile de confirmare.
       email:
-        label: E-mail nou
-        msg: Câmpul e-mail nu poate fi gol.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Parolă actuală
         msg: Parola nu poate fi goală.
@@ -1067,7 +1069,7 @@ ui:
       title: Răspunsuri
       score: Scor
       newest: Cele mai noi
-      oldest: Oldest
+      oldest: Cel mai vechi
       btn_accept: Acceptă
       btn_accepted: Acceptă
     write_answer:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Rezultatele căutării
     keywords: Cuvinte cheie
@@ -1176,7 +1179,7 @@ ui:
       more: Mai mult
     tips:
       title: Sfaturi de căutare avansate
-      tag: "<1>[tag]</1> caută fără o etichetă"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>utilizator:username</1> căutare de către autor"
       answer: "<1>răspunsuri:0</1> întrebări fără răspuns"
       score: "<1>scor:3</1> postări cu un scor de 3+"
@@ -1538,7 +1541,7 @@ ui:
       status: Stare
       action: Actiune
       change: Schimbare
-      pending: Pending
+      pending: În așteptare
       filter:
         placeholder: "Filtrează după titlu, întrebare: id"
     answers:
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Nivel necesar de reputație
         text: Alegeți reputația necesară pentru privilegii
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (opțional)
     empty: nu poate fi lăsat necompletat
@@ -1801,12 +1807,12 @@ ui:
     flag_user: Flag user
     queued_post: Queued post
     queued_user: Queued user
-    filter_label: Type
+    filter_label: Tip
     reputation: reputation
-    flag_post_type: Flagged this post as {{ type }}.
+    flag_post_type: Acest post a fost marcat de {{ type }}.
     flag_user_type: Flagged this user as {{ type }}.
-    edit_post: Edit post
-    list_post: List post
+    edit_post: Editează postarea
+    list_post: Listează postarea
     unlist_post: Unlist post
   timeline:
     undeleted: restabilește
diff --git a/i18n/ru_RU.yaml b/i18n/ru_RU.yaml
index e56120b6..3b0dc47a 100644
--- a/i18n/ru_RU.yaml
+++ b/i18n/ru_RU.yaml
@@ -142,7 +142,7 @@ backend:
   error:
     common:
       invalid_url:
-        other: Invalid URL.
+        other: Неверная URL.
     password:
       space_invalid:
         other: Пароль не должен содержать пробелы.
@@ -204,7 +204,7 @@ backend:
       already_deleted:
         other: Этот пост был удалён.
       under_review:
-        other: Your post is awaiting review. It will be visible after it has been approved.
+        other: Ваш пост ожидает проверки. Он станет видимым после одобрения.
       not_found:
         other: Вопрос не найден.
       cannot_deleted:
@@ -491,18 +491,18 @@ backend:
     accepted:
       other: принято
     edit:
-      other: edit
+      other: редактировать
   review:
     queued_post:
       other: Queued post
     flagged_post:
       other: Flagged post
     suggested_post_edit:
-      other: Suggested edits
+      other: Предложенные исправления
 # The following fields are used for interface presentation(Front-end)
 ui:
   how_to_format:
-    title: Как отформатировать
+    title: 'Форматирование:'
     desc: >-
       <ul class="mb-0"><li><p class="mb-2">создание ссылок: </p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">пробел между абзацами</p></li><li><p class="mb-2"><em>_italic_</em> или **<strong>bold</strong>**</p></li><li><p class="mb-2">отступ для кода - 4 пробела</p></li><li><p class="mb-2">тег  <code>&gt;</code> для цитаты в начале строки</p></li><li><p class="mb-2">обратные кавычки для строки кода <code>`like _thi [...]
   pagination:
@@ -680,8 +680,8 @@ ui:
       empty: Не может быть пустым.
     msg:
       empty: Пожалуйста, выбери причину.
-      not_a_url: URL format is incorrect.
-      url_not_match: URL origin does not match the current website.
+      not_a_url: Недопустимый формат URL.
+      url_not_match: URL адрес не соответствует текущему веб-сайту.
   tag_modal:
     title: Новый тег
     form:
@@ -785,7 +785,7 @@ ui:
     sort_buttons:
       popular: Популярное
       name: Имя
-      newest: Самые новые
+      newest: Последние
     button_follow: Подписаться
     button_following: Подписки
     tag_label: вопросы
@@ -809,9 +809,9 @@ ui:
             empty: Заголовок не может быть пустым.
             range: Заголовок должен быть меньше 150 символов
         body:
-          label: Тело вопроса
+          label: 'Вопрос:'
           msg:
-            empty: Тело вопроса не может быть пустым.
+            empty: Вопрос не может быть пустым.
         tags:
           label: Теги
           msg:
@@ -824,7 +824,7 @@ ui:
           label: Изменить краткое описание
           placeholder: >-
             Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование)
-    btn_post_question: Задайте свой вопрос
+    btn_post_question: Задать вопрос
     btn_save_edits: Сохранить изменения
     answer_question: Ответить на свой собственный вопрос
     post_question&answer: Опубликуйте свой вопрос и ответ
@@ -832,7 +832,7 @@ ui:
     add_btn: Тег
     create_btn: новый тег
     search_tag: Поиск тега
-    hint: "Опишите, о чем ваш вопрос, требуется хотя бы один тег."
+    hint: "Добавьте хотя бы один тег."
     no_result: Нет соответствующих тэгов
     tag_required_text: Обязательный тег (хотя бы один)
   header:
@@ -999,8 +999,10 @@ ui:
       change_email_info: >-
         Мы отправили электронное письмо на этот адрес. Пожалуйста, следуйте инструкциям из письма.
       email:
-        label: Новый email-адрес
-        msg: Новый email не может быть пустым.
+        label: Email
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Текущий пароль
         msg: Пароль не может быть пустым.
@@ -1066,7 +1068,7 @@ ui:
     answers:
       title: Ответы
       score: Оценка
-      newest: Самые новые
+      newest: Последние
       oldest: Oldest
       btn_accept: Принять
       btn_accepted: Принято
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Результаты поиска
     keywords: Ключевые слова
@@ -1170,13 +1173,13 @@ ui:
     more: Ещё
     sort_btns:
       relevance: По релевантности
-      newest: Самые новые
+      newest: Последние
       active: Активные
       score: Оценки
       more: Больше
     tips:
       title: Советы по расширенному поиску
-      tag: "<1>[тег]</1> поиск по тегу"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> поиск по автору"
       answer: "<1>ответов:0</1> вопросы без ответов"
       score: "<1>score:3</1> записи с рейтингом 3+"
@@ -1217,8 +1220,8 @@ ui:
     x_answers: "{{ count }} ответов"
     questions: Вопросы
     answers: Ответы
-    newest: Самые новые
-    active: Активный
+    newest: Последние
+    active: Активные
     frequent: Популярные
     score: Оценка
     unanswered: Без ответа
@@ -1238,7 +1241,7 @@ ui:
     reputation: Репутация
     comments: Комментарии
     votes: Голоса
-    newest: Самые новые
+    newest: Последние
     score: Оценки
     edit_profile: Редактировать профиль
     visited_x_days: "Посещено {{ count }} дней"
@@ -1405,9 +1408,9 @@ ui:
       comments: "Комментарии:"
       votes: "Голоса:"
       users: "Пользователи:"
-      flags: "Флаги:"
+      flags: "Жалобы:"
       reviews: "Reviews:"
-      site_health: Site health
+      site_health: Здоровье сайта
       version: "Версия:"
       https: "HTTPS:"
       upload_folder: "Каталог загрузки:"
@@ -1422,12 +1425,12 @@ ui:
       database_size: "Размер базы данных:"
       storage_used: "Использовано хранилища: "
       uptime: "Время работы:"
-      links: Links
+      links: Ссылки
       plugins: Плагины
       github: GitHub
       blog: Блог
       contact: Контакты
-      forum: Forum
+      forum: Форум
       documents: Документы
       feedback: Обратная связь
       support: Поддержка
@@ -1502,7 +1505,7 @@ ui:
       all: Все
       staff: Сотрудники
       more: Ещё
-      inactive: Неактивный
+      inactive: Неактивные
       suspended: Заблокированные
       deleted: Удаленные
       normal: Обычный
@@ -1538,7 +1541,7 @@ ui:
       status: Статус
       action: Действие
       change: Изменить
-      pending: Pending
+      pending: Ожидают
       filter:
         placeholder: "Фильтровать по заголовку, question:id"
     answers:
@@ -1576,8 +1579,8 @@ ui:
         validate: Контактный адрес электронной почты не может быть пустым.
         text: Адрес электронной почты контактного лица, ответственного за данный сайт.
       check_update:
-        label: Software updates
-        text: Automatically check for updates
+        label: Обновления программного обеспечения
+        text: Автоматически проверять наличие обновлений
     interface:
       page_title: Интерфейс
       language:
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Необходимый уровень репутации
         text: Выберите количество репутации, необходимое для получения привилегий
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (опционально)
     empty: не может быть пустым
@@ -1796,13 +1802,13 @@ ui:
     approve_flag_tip: Do you approve this flag?
     approve_post_tip: Do you approve this post?
     approve_user_tip: Do you approve this user?
-    suggest_edits: Suggested edits
+    suggest_edits: Предложенные исправления
     flag_post: Flag post
     flag_user: Flag user
     queued_post: Queued post
     queued_user: Queued user
     filter_label: Type
-    reputation: reputation
+    reputation: репутация
     flag_post_type: Flagged this post as {{ type }}.
     flag_user_type: Flagged this user as {{ type }}.
     edit_post: Edit post
diff --git a/i18n/sk_SK.yaml b/i18n/sk_SK.yaml
index 7daf62b0..7bbe5cf1 100644
--- a/i18n/sk_SK.yaml
+++ b/i18n/sk_SK.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Výsledky vyhľadávania
     keywords: Kľúčové slová
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (voliteľné)
     empty: nemôže byť prázdne
diff --git a/i18n/sq_AL.yaml b/i18n/sq_AL.yaml
index 483744f7..07e50541 100644
--- a/i18n/sq_AL.yaml
+++ b/i18n/sq_AL.yaml
@@ -708,7 +708,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -803,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/sr_SP.yaml b/i18n/sr_SP.yaml
index 28941e9e..39b63e8c 100644
--- a/i18n/sr_SP.yaml
+++ b/i18n/sr_SP.yaml
@@ -715,7 +715,9 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Email
-        msg: Email cannot be empty.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       password_title: Password
       current_pass:
         label: Current Password
@@ -811,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/sv_SE.yaml b/i18n/sv_SE.yaml
index 7cef6932..66722d03 100644
--- a/i18n/sv_SE.yaml
+++ b/i18n/sv_SE.yaml
@@ -1000,6 +1000,8 @@ ui:
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
         label: Ny e-postadress
+      new_email:
+        label: New email
         msg: New email cannot be empty.
       pass:
         label: Nuvarande lösenord
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Sökresultat
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/te_IN.yaml b/i18n/te_IN.yaml
index dc22340c..ef79b398 100644
--- a/i18n/te_IN.yaml
+++ b/i18n/te_IN.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/tr_TR.yaml b/i18n/tr_TR.yaml
index 0460fda5..9d2c8aca 100644
--- a/i18n/tr_TR.yaml
+++ b/i18n/tr_TR.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/uk_UA.yaml b/i18n/uk_UA.yaml
index 21421f66..49a8abfd 100644
--- a/i18n/uk_UA.yaml
+++ b/i18n/uk_UA.yaml
@@ -1000,7 +1000,9 @@ ui:
         Ми надіслали електронний лист на цю адресу. Будь ласка, дотримуйтесь інструкцій для підтвердження.
       email:
         label: Нова електронна пошта
-        msg: Комірка нової електронної пошти не може бути порожньою.
+      new_email:
+        label: New email
+        msg: New email cannot be empty.
       pass:
         label: Поточний пароль
         msg: Комірка паролю не може бути порожньою.
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Результати пошуку
     keywords: Ключові слова
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/vi_VN.yaml b/i18n/vi_VN.yaml
index d28041ce..a66565d1 100644
--- a/i18n/vi_VN.yaml
+++ b/i18n/vi_VN.yaml
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         We've sent an email to that address. Please follow the confirmation instructions.
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: Search Results
     keywords: Keywords
@@ -1176,7 +1179,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (optional)
     empty: cannot be empty
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index f4d142ca..a2459da5 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -142,7 +142,7 @@ backend:
   error:
     common:
       invalid_url:
-        other: 不合法的 URL
+        other: 无效的 URL。
     password:
       space_invalid:
         other: 密码不得含有空格。
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         邮件已发送。请根据指引完成验证。
       email:
+        label: 电子邮件地址
+      new_email:
         label: 新的电子邮件地址
         msg: 新邮箱不能为空。
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: 已关闭
     deleted: 已删除
     pending: 等待处理
+    more: 更多
   search:
     title: 搜索结果
     keywords: 关键词
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: 级别所需声望
         text: 选择特权所需的声望值
+      msg:
+        should_be_number: 输入必须是数字
+        number_larger_1: 数字应该大于等于 1
   form:
     optional: (选填)
     empty: 不能为空
diff --git a/i18n/zh_TW.yaml b/i18n/zh_TW.yaml
index c0377af7..3e3f26b0 100644
--- a/i18n/zh_TW.yaml
+++ b/i18n/zh_TW.yaml
@@ -29,9 +29,9 @@ backend:
     database_error:
       other: 資料庫錯誤。
     forbidden_error:
-      other: Forbidden.
+      other: 訪問被拒絕。
     duplicate_request_error:
-      other: Duplicate submission.
+      other: 提交重複。
   action:
     report:
       other: 舉報
@@ -42,11 +42,11 @@ backend:
     close:
       other: 關閉
     reopen:
-      other: Reopen
+      other: 再次開啟。
     forbidden_error:
-      other: Forbidden.
+      other: 訪問被拒絕。
     pin:
-      other: Pin
+      other: 置頂
     hide:
       other: Unlist
     unpin:
@@ -738,7 +738,7 @@ ui:
   dates:
     long_date: MM月DD日
     long_date_with_year: "YYYY年MM月DD日"
-    long_date_with_time: "YYYY年MM月DD日 HH:mm"
+    long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm"
     now: 剛剛
     x_seconds_ago: "{{count}} 秒前"
     x_minutes_ago: "{{count}} 分鐘前"
@@ -756,7 +756,7 @@ ui:
     btn_flag: 舉報
     btn_save_edits: 保存
     btn_cancel: 取消
-    show_more: "{{count}}條剩餘評論"
+    show_more: "{{count}} 條剩餘評論"
     tip_question: >-
       通过評論询问更多问题或提出改進建議。避免在評論中回答問題。
     tip_answer: >-
@@ -999,6 +999,8 @@ ui:
       change_email_info: >-
         我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。
       email:
+        label: Email
+      new_email:
         label: New email
         msg: New email cannot be empty.
       pass:
@@ -1160,6 +1162,7 @@ ui:
     closed: Closed
     deleted: Deleted
     pending: Pending
+    more: More
   search:
     title: 搜尋結果
     keywords: 關鍵詞
@@ -1657,7 +1660,7 @@ ui:
       restrict_answer:
         title: Restrict answer
         label: Each user can only write one answer for each question
-        text: "They can use the edit link to refine and improve their existing answer, instead."
+        text: "關閉此功能以允許使用者對同一問題撰寫多個答案,可能導致答案不集中。"
       recommend_tags:
         label: Recommend tags
         text: "請在上方輸入標籤固定連結,每行一個標籤。"
@@ -1774,6 +1777,9 @@ ui:
       level:
         label: Reputation required level
         text: Choose the reputation required for the privileges
+      msg:
+        should_be_number: the input should be number
+        number_larger_1: number should be equal or larger than 1
   form:
     optional: (選填)
     empty: 不能為空
diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go
index 390aa422..0142c58c 100644
--- a/internal/controller/answer_controller.go
+++ b/internal/controller/answer_controller.go
@@ -260,6 +260,9 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
 		}
 	}
 
+	req.UserAgent = ctx.GetHeader("User-Agent")
+	req.IP = ctx.ClientIP()
+
 	answerID, err := ac.answerService.Insert(ctx, req)
 	if err != nil {
 		handler.HandleResponse(ctx, err, nil)
diff --git a/internal/controller/controller.go b/internal/controller/controller.go
index 42c2c1d3..cfaf8f33 100644
--- a/internal/controller/controller.go
+++ b/internal/controller/controller.go
@@ -48,4 +48,6 @@ var ProviderSetController = wire.NewSet(
 	NewPermissionController,
 	NewUserPluginController,
 	NewReviewController,
+	NewCaptchaController,
+	NewEmbedController,
 )
diff --git a/internal/controller/embed_controller.go b/internal/controller/embed_controller.go
new file mode 100644
index 00000000..109d8fb7
--- /dev/null
+++ b/internal/controller/embed_controller.go
@@ -0,0 +1,47 @@
+package controller
+
+import (
+	"github.com/apache/incubator-answer/internal/base/handler"
+	"github.com/apache/incubator-answer/internal/schema"
+	"github.com/apache/incubator-answer/plugin"
+	"github.com/gin-gonic/gin"
+)
+
+type EmbedController struct {
+}
+
+func NewEmbedController() *EmbedController {
+	return &EmbedController{}
+}
+
+// GetEmbedConfig godoc
+// @Summary GetEmbedConfig
+// @Description GetEmbedConfig
+// @Tags PluginEmbed
+// @Accept json
+// @Produce json
+// @Router /answer/api/v1/embed/config [get]
+// @Success 200 {object} handler.RespBody{data=[]schema.GetEmbedOptionResp}
+func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) {
+	resp := make([]*schema.GetEmbedOptionResp, 0)
+	var slugName string
+
+	_ = plugin.CallEmbed(func(base plugin.Embed) error {
+		slugName = base.Info().SlugName
+		return nil
+	})
+
+	_ = plugin.CallConfig(func(fn plugin.Config) error {
+		if fn.Info().SlugName == slugName {
+			for _, field := range fn.ConfigFields() {
+				resp = append(resp, &schema.GetEmbedOptionResp{
+					Platform: field.Name,
+					Enable:   field.Value.(bool),
+				})
+			}
+			return nil
+		}
+		return nil
+	})
+	handler.HandleResponse(ctx, nil, resp)
+}
diff --git a/internal/controller/controller.go b/internal/controller/plugin_captcha_controller.go
similarity index 50%
copy from internal/controller/controller.go
copy to internal/controller/plugin_captcha_controller.go
index 42c2c1d3..7b47e071 100644
--- a/internal/controller/controller.go
+++ b/internal/controller/plugin_captcha_controller.go
@@ -19,33 +19,35 @@
 
 package controller
 
-import "github.com/google/wire"
+import (
+	"encoding/json"
 
-// ProviderSetController is controller providers.
-var ProviderSetController = wire.NewSet(
-	NewLangController,
-	NewCommentController,
-	NewReportController,
-	NewVoteController,
-	NewTagController,
-	NewFollowController,
-	NewCollectionController,
-	NewUserController,
-	NewQuestionController,
-	NewAnswerController,
-	NewSearchController,
-	NewRevisionController,
-	NewRankController,
-	NewReasonController,
-	NewNotificationController,
-	NewSiteInfoController,
-	NewDashboardController,
-	NewUploadController,
-	NewActivityController,
-	NewTemplateController,
-	NewConnectorController,
-	NewUserCenterController,
-	NewPermissionController,
-	NewUserPluginController,
-	NewReviewController,
+	"github.com/apache/incubator-answer/internal/base/handler"
+	"github.com/apache/incubator-answer/plugin"
+	"github.com/gin-gonic/gin"
 )
+
+// CaptchaController comment controller
+type CaptchaController struct {
+}
+
+// NewCaptchaController new controller
+func NewCaptchaController() *CaptchaController {
+	return &CaptchaController{}
+}
+
+type GetCaptchaConfigResp struct {
+	SlugName string         `json:"slug_name"`
+	Config   map[string]any `json:"config"`
+}
+
+// GetCaptchaConfig get captcha config
+func (uc *CaptchaController) GetCaptchaConfig(ctx *gin.Context) {
+	resp := &GetCaptchaConfigResp{}
+	_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {
+		resp.SlugName = fn.Info().SlugName
+		_ = json.Unmarshal([]byte(fn.GetConfig()), &resp.Config)
+		return nil
+	})
+	handler.HandleResponse(ctx, nil, resp)
+}
diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go
index 072ffd74..1b0dee92 100644
--- a/internal/controller/question_controller.go
+++ b/internal/controller/question_controller.go
@@ -434,6 +434,9 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
 		return
 	}
 
+	req.UserAgent = ctx.GetHeader("User-Agent")
+	req.IP = ctx.ClientIP()
+
 	resp, err := qc.questionService.AddQuestion(ctx, req)
 	if err != nil {
 		errlist, ok := resp.([]*validator.FormErrorField)
@@ -526,6 +529,8 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {
 		return
 	}
 
+	req.UserAgent = ctx.GetHeader("User-Agent")
+	req.IP = ctx.ClientIP()
 	resp, err := qc.questionService.AddQuestion(ctx, questionReq)
 	if err != nil {
 		errlist, ok := resp.([]*validator.FormErrorField)
diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
index b128b91a..fa95e460 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -218,6 +218,11 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
 		}
 	}
 
+	if _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil {
+		answerID = ""
+		titleIsAnswerID = false
+	}
+
 	url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, questionID)
 	if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {
 		if len(ctx.Request.URL.Query()) > 0 {
diff --git a/internal/controller/template_render/answer.go b/internal/controller/template_render/answer.go
index cc94f0dd..72a0925a 100644
--- a/internal/controller/template_render/answer.go
+++ b/internal/controller/template_render/answer.go
@@ -28,3 +28,7 @@ import (
 func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
 	return t.answerService.SearchList(ctx, req)
 }
+
+func (t *TemplateRenderController) AnswerDetail(ctx context.Context, id string) (*schema.AnswerInfo, error) {
+	return t.answerService.GetDetail(ctx, id)
+}
diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go
index f0e70cfe..20fc68ec 100644
--- a/internal/controller/user_controller.go
+++ b/internal/controller/user_controller.go
@@ -509,19 +509,6 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) {
 
 }
 
-// UserRegisterCaptcha godoc
-// @Summary UserRegisterCaptcha
-// @Description UserRegisterCaptcha
-// @Tags User
-// @Accept json
-// @Produce json
-// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}
-// @Router /answer/api/v1/user/register/captcha [get]
-func (uc *UserController) UserRegisterCaptcha(ctx *gin.Context) {
-	resp, err := uc.actionService.UserRegisterCaptcha(ctx)
-	handler.HandleResponse(ctx, err, resp)
-}
-
 // GetUserNotificationConfig get user's notification config
 // @Summary get user's notification config
 // @Description get user's notification config
@@ -657,6 +644,26 @@ func (uc *UserController) UserRanking(ctx *gin.Context) {
 	handler.HandleResponse(ctx, err, resp)
 }
 
+// UserStaff get user staff
+// @Summary get user staff
+// @Description get user staff
+// @Tags User
+// @Accept json
+// @Produce json
+// @Security ApiKeyAuth
+// @Param data body schema.GetUserStaffReq true "GetUserStaffReq"
+// @Success 200 {object} handler.RespBody{data=schema.GetUserStaffResp}
+// @Router /answer/api/v1/user/staff [get]
+func (uc *UserController) UserStaff(ctx *gin.Context) {
+	req := &schema.GetUserStaffReq{}
+	if handler.BindAndCheck(ctx, req) {
+		return
+	}
+
+	resp, err := uc.userService.GetUserStaff(ctx, req)
+	handler.HandleResponse(ctx, err, resp)
+}
+
 // UserUnsubscribeNotification unsubscribe notification
 // @Summary unsubscribe notification
 // @Description unsubscribe notification
diff --git a/internal/controller_admin/siteinfo_controller.go b/internal/controller_admin/siteinfo_controller.go
index 6c1966b3..1526fd20 100644
--- a/internal/controller_admin/siteinfo_controller.go
+++ b/internal/controller_admin/siteinfo_controller.go
@@ -21,6 +21,7 @@ package controller_admin
 
 import (
 	"net/http"
+	"html"
 
 	"github.com/apache/incubator-answer/internal/base/handler"
 	"github.com/apache/incubator-answer/internal/base/middleware"
@@ -237,6 +238,7 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
 		return
 	}
 	err := sc.siteInfoService.SaveSiteGeneral(ctx, req)
+	req.Name = html.UnescapeString(req.Name)
 	handler.HandleResponse(ctx, err, req)
 }
 
diff --git a/internal/migrations/init.go b/internal/migrations/init.go
index 8448bfc4..0c50857a 100644
--- a/internal/migrations/init.go
+++ b/internal/migrations/init.go
@@ -23,6 +23,10 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"time"
+
+	"github.com/apache/incubator-answer/internal/base/data"
+	"github.com/apache/incubator-answer/internal/repo/unique"
 	"github.com/apache/incubator-answer/internal/schema"
 	"github.com/segmentfault/pacman/log"
 
@@ -73,6 +77,7 @@ func (m *Mentor) InitDB() error {
 	m.do("init site info user config", m.initSiteInfoUsersConfig)
 	m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank)
 	m.do("init site info write", m.initSiteInfoWrite)
+	m.do("init default content", m.initDefaultContent)
 	return m.err
 }
 
@@ -255,3 +260,136 @@ func (m *Mentor) initSiteInfoWrite() {
 		Status:  1,
 	})
 }
+
+func (m *Mentor) initDefaultContent() {
+	uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine})
+	now := time.Now()
+
+	tagId, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Tag{}.TableName())
+	if err != nil {
+		m.err = err
+		return
+	}
+
+	q1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())
+	if err != nil {
+		m.err = err
+		return
+	}
+
+	a1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())
+	if err != nil {
+		m.err = err
+		return
+	}
+
+	q2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())
+	if err != nil {
+		m.err = err
+		return
+	}
+
+	a2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())
+	if err != nil {
+		m.err = err
+		return
+	}
+
+	tag := entity.Tag{
+		ID:            tagId,
+		SlugName:      "support",
+		DisplayName:   "support",
+		OriginalText:  "For general support questions.",
+		ParsedText:    "<p>For general support questions.</p>",
+		UserID:        "1",
+		QuestionCount: 1,
+		Status:        entity.TagStatusAvailable,
+	}
+
+	q1 := &entity.Question{
+		ID:           q1Id,
+		CreatedAt:    now,
+		UserID:       "1",
+		Title:        "What is a tag?",
+		OriginalText: "When asking a question, we need to choose tags. What are tags and why should I use them?",
+		ParsedText:   "<p>When asking a question, we need to choose tags. What are tags and why should I use them?</p>",
+		Show:         entity.QuestionShow,
+		Status:       entity.QuestionStatusAvailable,
+		AnswerCount:  1,
+	}
+
+	a1 := &entity.Answer{
+		ID:           a1Id,
+		CreatedAt:    now,
+		QuestionID:   q1Id,
+		UserID:       "1",
+		OriginalText: "Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.",
+		ParsedText:   "<p>Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.</p>",
+		Status:       entity.AnswerStatusAvailable,
+	}
+
+	q2 := &entity.Question{
+		ID:           q2Id,
+		CreatedAt:    now,
+		UserID:       "1",
+		Title:        "What is reputation and how do I earn them?",
+		OriginalText: "I see that each user has reputation points, What is it and how do I earn them?",
+		ParsedText:   "<p>I see that each user has reputation points, What is it and how do I earn them?</p>",
+		Show:         entity.QuestionShow,
+		Status:       entity.QuestionStatusAvailable,
+		AnswerCount:  1,
+	}
+
+	a2 := &entity.Answer{
+		ID:           a2Id,
+		CreatedAt:    now,
+		QuestionID:   q2Id,
+		UserID:       "1",
+		OriginalText: "Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.",
+		ParsedText:   "<p>Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.</p>",
+		Status:       entity.AnswerStatusAvailable,
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(tag)
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(q1)
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(a1)
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{
+		ObjectID: q1.ID,
+		TagID:    tag.ID,
+		Status:   entity.TagRelStatusAvailable,
+	})
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(q2)
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(a2)
+	if m.err != nil {
+		return
+	}
+
+	_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{
+		ObjectID: q2.ID,
+		TagID:    tag.ID,
+		Status:   entity.TagRelStatusAvailable,
+	})
+	if m.err != nil {
+		return
+	}
+}
diff --git a/internal/repo/collection/collection_group_repo.go b/internal/repo/collection/collection_group_repo.go
index 90c21963..05711f5a 100644
--- a/internal/repo/collection/collection_group_repo.go
+++ b/internal/repo/collection/collection_group_repo.go
@@ -21,7 +21,6 @@ package collection
 
 import (
 	"context"
-	"xorm.io/xorm"
 
 	"github.com/apache/incubator-answer/internal/service/collection"
 	"xorm.io/xorm"
diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go
index 8dca0c65..29ab9c5d 100644
--- a/internal/repo/question/question_repo.go
+++ b/internal/repo/question/question_repo.go
@@ -26,7 +26,6 @@ import (
 	"strings"
 	"time"
 	"unicode"
-	"xorm.io/xorm"
 
 	"github.com/apache/incubator-answer/internal/base/constant"
 	"github.com/apache/incubator-answer/internal/base/data"
diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go
index edcd45e9..f3696590 100644
--- a/internal/repo/repo_test/tag_rel_repo_test.go
+++ b/internal/repo/repo_test/tag_rel_repo_test.go
@@ -36,13 +36,13 @@ var (
 	tagRelOnce     sync.Once
 	testTagRelList = []*entity.TagRel{
 		{
-			ObjectID: "10010000000000001",
-			TagID:    "10030000000000001",
+			ObjectID: "10010000000000101",
+			TagID:    "10030000000000101",
 			Status:   entity.TagRelStatusAvailable,
 		},
 		{
-			ObjectID: "10010000000000002",
-			TagID:    "10030000000000002",
+			ObjectID: "10010000000000202",
+			TagID:    "10030000000000202",
 			Status:   entity.TagRelStatusAvailable,
 		},
 	}
@@ -68,7 +68,7 @@ func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) {
 func Test_tagListRepo_CountTagRelByTagID(t *testing.T) {
 	tagRelOnce.Do(addTagRelList)
 	tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
-	count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001")
+	count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
 	assert.NoError(t, err)
 	assert.Equal(t, int64(1), count)
 }
@@ -96,7 +96,7 @@ func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) {
 	err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids)
 	assert.NoError(t, err)
 
-	count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001")
+	count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
 	assert.NoError(t, err)
 	assert.Equal(t, int64(0), count)
 
@@ -107,7 +107,7 @@ func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) {
 	err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids)
 	assert.NoError(t, err)
 
-	count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001")
+	count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
 	assert.NoError(t, err)
 	assert.Equal(t, int64(1), count)
 }
diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go
index eea54995..c9c2c1fa 100644
--- a/internal/repo/tag/tag_repo.go
+++ b/internal/repo/tag/tag_repo.go
@@ -77,7 +77,7 @@ func (tr *tagRepo) RecoverTag(ctx context.Context, tagID string) (err error) {
 	return
 }
 
-// MustGetTagByID get tag by id
+// MustGetTagByNameOrID get tag by name or id
 func (tr *tagRepo) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (
 	tag *entity.Tag, exist bool, err error) {
 	if len(tagID) == 0 && len(slugName) == 0 {
diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go
index 628394cf..568ebf90 100644
--- a/internal/repo/user/user_repo.go
+++ b/internal/repo/user/user_repo.go
@@ -246,12 +246,16 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
 	return count, nil
 }
 
-func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int) (userList []*entity.User, err error) {
+func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int,
+	onlyStaff bool) (userList []*entity.User, err error) {
 	userList = make([]*entity.User, 0)
 	session := ur.data.DB.Context(ctx)
+	if onlyStaff {
+		session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1")
+	}
 	session.Where("status = ?", entity.UserStatusAvailable)
 	session.Where("username LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%")
-	session.OrderBy("username ASC, id DESC")
+	session.OrderBy("username ASC, `user`.id DESC")
 	session.Limit(limit)
 	err = session.Find(&userList)
 	if err != nil {
diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go
index 1ea2be89..40a5bba3 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -131,7 +131,6 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *
 	routerGroup := r.Group("", middleware.BanAPIForUserCenter)
 	routerGroup.POST("/user/login/email", a.userController.UserEmailLogin)
 	routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail)
-	routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
 	routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail)
 	routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify)
 	routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
@@ -146,6 +145,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
 	// user
 	r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
 	r.GET("/user/ranking", a.userController.UserRanking)
+	r.GET("/user/staff", a.userController.UserStaff)
 
 	// answer
 	r.GET("/answer/info", a.answerController.Get)
diff --git a/internal/router/plugin_api_router.go b/internal/router/plugin_api_router.go
index 8554b4c7..e4a85a15 100644
--- a/internal/router/plugin_api_router.go
+++ b/internal/router/plugin_api_router.go
@@ -27,15 +27,21 @@ import (
 type PluginAPIRouter struct {
 	connectorController  *controller.ConnectorController
 	userCenterController *controller.UserCenterController
+	captchaController    *controller.CaptchaController
+	embedController      *controller.EmbedController
 }
 
 func NewPluginAPIRouter(
 	connectorController *controller.ConnectorController,
 	userCenterController *controller.UserCenterController,
+	captchaController *controller.CaptchaController,
+	embedController *controller.EmbedController,
 ) *PluginAPIRouter {
 	return &PluginAPIRouter{
 		connectorController:  connectorController,
 		userCenterController: userCenterController,
+		captchaController:    captchaController,
+		embedController:      embedController,
 	}
 }
 
@@ -54,6 +60,10 @@ func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
 	r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect)
 	r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback)
 	r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback)
+
+	// captcha plugin
+	r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig)
+	r.GET("/embed/config", pr.embedController.GetEmbedConfig)
 }
 
 func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {
diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go
index d0c208e6..01bc64a2 100644
--- a/internal/schema/answer_schema.go
+++ b/internal/schema/answer_schema.go
@@ -54,6 +54,8 @@ type AnswerAddReq struct {
 	CanRecover  bool   `json:"-"`
 	CaptchaID   string `json:"captcha_id"`
 	CaptchaCode string `json:"captcha_code"`
+	IP          string `json:"-"`
+	UserAgent   string `json:"-"`
 }
 
 func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go
index 4f318d87..275e1b9a 100644
--- a/internal/schema/comment_schema.go
+++ b/internal/schema/comment_schema.go
@@ -38,6 +38,9 @@ type AddCommentReq struct {
 	ParsedText string `json:"-"`
 	// @ user id list
 	MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
+	CaptchaID           string   `json:"captcha_id"`
+	CaptchaCode         string   `json:"captcha_code"`
+
 	// user id
 	UserID string `json:"-"`
 	// whether user can add it
@@ -45,9 +48,7 @@ type AddCommentReq struct {
 	// whether user can edit it
 	CanEdit bool `json:"-"`
 	// whether user can delete it
-	CanDelete   bool   `json:"-"`
-	CaptchaID   string `json:"captcha_id"` // captcha_id
-	CaptchaCode string `json:"captcha_code"`
+	CanDelete bool `json:"-"`
 }
 
 func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
@@ -61,7 +62,7 @@ type RemoveCommentReq struct {
 	CommentID string `validate:"required" json:"comment_id"`
 	// user id
 	UserID      string `json:"-"`
-	CaptchaID   string `json:"captcha_id"` // captcha_id
+	CaptchaID   string `json:"captcha_id"`
 	CaptchaCode string `json:"captcha_code"`
 }
 
diff --git a/internal/schema/plugin_option_schema.go b/internal/schema/plugin_option_schema.go
new file mode 100644
index 00000000..e0224609
--- /dev/null
+++ b/internal/schema/plugin_option_schema.go
@@ -0,0 +1,6 @@
+package schema
+
+type GetEmbedOptionResp struct {
+	Platform string `json:"platform"`
+	Enable   bool   `json:"enable"`
+}
diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go
index 48fd97c5..f915d5ea 100644
--- a/internal/schema/question_schema.go
+++ b/internal/schema/question_schema.go
@@ -86,6 +86,8 @@ type QuestionAdd struct {
 	QuestionPermission
 	CaptchaID   string `json:"captcha_id"` // captcha_id
 	CaptchaCode string `json:"captcha_code"`
+	IP          string `json:"-"`
+	UserAgent   string `json:"-"`
 }
 
 func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) {
@@ -115,6 +117,8 @@ type QuestionAddByAnswer struct {
 	QuestionPermission
 	CaptchaID   string `json:"captcha_id"` // captcha_id
 	CaptchaCode string `json:"captcha_code"`
+	IP          string `json:"-"`
+	UserAgent   string `json:"-"`
 }
 
 func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) {
diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go
index 59f32c5e..50aeaa6f 100644
--- a/internal/schema/search_schema.go
+++ b/internal/schema/search_schema.go
@@ -29,13 +29,13 @@ import (
 )
 
 type SearchDTO struct {
-	UserID      string // UserID current login user ID
-	Query       string `validate:"required,gte=1,lte=60" json:"q" form:"q"`                   // Query the query string
-	Page        int    `validate:"omitempty,min=1" form:"page,default=1" json:"page"`         //Query number of pages
-	Size        int    `validate:"omitempty,min=1,max=50" form:"size,default=30" json:"size"` //Search page size
-	Order       string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" json:"order" enums:"newest,active,score,relevance"`
-	CaptchaID   string `json:"captcha_id"` // captcha_id
-	CaptchaCode string `json:"captcha_code"`
+	Query       string `validate:"required,gte=1,lte=60" form:"q"`
+	Page        int    `validate:"omitempty,min=1" form:"page,default=1"`
+	Size        int    `validate:"omitempty,min=1,max=50" form:"size,default=30"`
+	Order       string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" enums:"newest,active,score,relevance"`
+	CaptchaID   string `form:"captcha_id"`
+	CaptchaCode string `form:"captcha_code"`
+	UserID      string `json:"-"`
 }
 
 func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) {
diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go
index a95acfc7..13971708 100644
--- a/internal/schema/user_schema.go
+++ b/internal/schema/user_schema.go
@@ -220,8 +220,8 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
 type UserModifyPasswordReq struct {
 	OldPass     string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
 	Pass        string `validate:"required,gte=8,lte=32" json:"pass"`
-	CaptchaID   string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
-	CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
+	CaptchaID   string `json:"captcha_id"`
+	CaptchaCode string `json:"captcha_code"`
 	UserID      string `json:"-"`
 	AccessToken string `json:"-"`
 }
@@ -382,8 +382,8 @@ type UserChangeEmailVerifyReq struct {
 }
 
 type UserVerifyEmailSendReq struct {
-	CaptchaID   string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
-	CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
+	CaptchaID   string `json:"captcha_id"`
+	CaptchaCode string `json:"captcha_code"`
 }
 
 // UserRankingResp user ranking response
@@ -412,3 +412,19 @@ type UserUnsubscribeNotificationReq struct {
 	Code    string `validate:"required,gt=0,lte=500" json:"code"`
 	Content string `json:"-"`
 }
+
+// GetUserStaffReq get user staff request
+type GetUserStaffReq struct {
+	Username string `validate:"omitempty,gt=0,lte=500" form:"username"`
+	PageSize int    `validate:"omitempty,min=1" form:"page_size"`
+}
+
+// GetUserStaffResp get user staff response
+type GetUserStaffResp struct {
+	// username
+	Username string `json:"username"`
+	// display name
+	DisplayName string `json:"display_name"`
+	// avatar
+	Avatar string `json:"avatar"`
+}
diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go
index f941e1e4..e82adcc2 100644
--- a/internal/schema/vote_schema.go
+++ b/internal/schema/vote_schema.go
@@ -20,11 +20,11 @@
 package schema
 
 type VoteReq struct {
-	ObjectID    string `validate:"required" form:"object_id" json:"object_id"`  //	 id
-	IsCancel    bool   `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel
-	UserID      string `json:"-"`
-	CaptchaID   string `json:"captcha_id"` // captcha_id
+	ObjectID    string `validate:"required" json:"object_id"`
+	IsCancel    bool   `validate:"omitempty" json:"is_cancel"`
+	CaptchaID   string `json:"captcha_id"`
 	CaptchaCode string `json:"captcha_code"`
+	UserID      string `json:"-"`
 }
 
 type VoteResp struct {
diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go
index 7619b983..326ea637 100644
--- a/internal/service/action/captcha_service.go
+++ b/internal/service/action/captcha_service.go
@@ -21,14 +21,11 @@ package action
 
 import (
 	"context"
-	"image/color"
-	"strings"
 
-	"github.com/apache/incubator-answer/internal/base/reason"
 	"github.com/apache/incubator-answer/internal/entity"
 	"github.com/apache/incubator-answer/internal/schema"
-	"github.com/mojocn/base64Captcha"
-	"github.com/segmentfault/pacman/errors"
+	"github.com/apache/incubator-answer/pkg/token"
+	"github.com/apache/incubator-answer/plugin"
 	"github.com/segmentfault/pacman/log"
 )
 
@@ -84,49 +81,29 @@ func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRe
 	}
 	verificationResult := cs.ValidationStrategy(ctx, unit, req.Action)
 	if !verificationResult {
-		resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx)
 		resp.Verify = true
+		resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx)
+		if err != nil {
+			log.Errorf("GenerateCaptcha error: %v", err)
+		}
 	}
 	return
 }
 
-func (cs *CaptchaService) UserRegisterCaptcha(ctx context.Context) (resp *schema.ActionRecordResp, err error) {
-	resp = &schema.ActionRecordResp{}
-	resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx)
-	resp.Verify = true
-	return
-}
-
-func (cs *CaptchaService) UserRegisterVerifyCaptcha(
-	ctx context.Context, id string, VerifyValue string,
-) bool {
-	if id == "" || VerifyValue == "" {
-		return false
-	}
-	pass, err := cs.VerifyCaptcha(ctx, id, VerifyValue)
-	if err != nil {
-		return false
-	}
-	return pass
-}
-
 // ActionRecordVerifyCaptcha
 // Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct
 func (cs *CaptchaService) ActionRecordVerifyCaptcha(
-	ctx context.Context, actionType string, unit string, id string, VerifyValue string,
+	ctx context.Context, actionType string, unit string, captchaID string, captchaCode string,
 ) bool {
 	verificationResult := cs.ValidationStrategy(ctx, unit, actionType)
-	if !verificationResult {
-		if id == "" || VerifyValue == "" {
-			return false
-		}
-		pass, err := cs.VerifyCaptcha(ctx, id, VerifyValue)
-		if err != nil {
-			return false
-		}
-		return pass
+	if verificationResult {
+		return true
+	}
+	pass, err := cs.VerifyCaptcha(ctx, captchaID, captchaCode)
+	if err != nil {
+		return false
 	}
-	return true
+	return pass
 }
 
 func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, unit string) (int, error) {
@@ -155,43 +132,32 @@ func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string
 
 // GenerateCaptcha generate captcha
 func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase64 string, err error) {
-	driverString := base64Captcha.DriverString{
-		Height:          60,
-		Width:           200,
-		NoiseCount:      0,
-		ShowLineOptions: 2 | 4,
-		Length:          4,
-		Source:          "1234567890qwertyuioplkjhgfdsazxcvbnm",
-		BgColor:         &color.RGBA{R: 211, G: 211, B: 211, A: 0},
-		Fonts:           []string{"wqy-microhei.ttc"},
-	}
-	driver := driverString.ConvertFonts()
-
-	id, content, answer := driver.GenerateIdQuestionAnswer()
-	item, err := driver.DrawCaptcha(content)
-	if err != nil {
-		return "", "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
-	}
-	err = cs.captchaRepo.SetCaptcha(ctx, id, answer)
-	if err != nil {
-		return "", "", err
+	realCaptcha := ""
+	key = token.GenerateToken()
+	_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {
+		if captcha, code := fn.Create(); len(code) > 0 {
+			captchaBase64 = captcha
+			realCaptcha = code
+		}
+		return nil
+	})
+	if len(realCaptcha) == 0 {
+		return key, captchaBase64, nil
 	}
 
-	captchaBase64 = item.EncodeB64string()
-	return id, captchaBase64, nil
+	err = cs.captchaRepo.SetCaptcha(ctx, key, realCaptcha)
+	return key, captchaBase64, err
 }
 
 // VerifyCaptcha generate captcha
 func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) {
-	realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key)
-	if err != nil {
-		log.Error("VerifyCaptcha GetCaptcha Error", err.Error())
-		return false, nil
-	}
-	err = cs.captchaRepo.DelCaptcha(ctx, key)
-	if err != nil {
-		log.Error("VerifyCaptcha DelCaptcha Error", err.Error())
-		return false, nil
-	}
-	return strings.TrimSpace(captcha) == realCaptcha, nil
+	realCaptcha, _ := cs.captchaRepo.GetCaptcha(ctx, key)
+
+	_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {
+		isCorrect = fn.Verify(realCaptcha, captcha)
+		return nil
+	})
+
+	_ = cs.captchaRepo.DelCaptcha(ctx, key)
+	return isCorrect, nil
 }
diff --git a/internal/service/action/captcha_strategy.go b/internal/service/action/captcha_strategy.go
index 72dfefed..1d2dcbc3 100644
--- a/internal/service/action/captcha_strategy.go
+++ b/internal/service/action/captcha_strategy.go
@@ -21,9 +21,11 @@ package action
 
 import (
 	"context"
-	"github.com/segmentfault/pacman/log"
 	"time"
 
+	"github.com/apache/incubator-answer/plugin"
+	"github.com/segmentfault/pacman/log"
+
 	"github.com/apache/incubator-answer/internal/entity"
 )
 
@@ -31,6 +33,10 @@ import (
 // true pass
 // false need captcha
 func (cs *CaptchaService) ValidationStrategy(ctx context.Context, unit, actionType string) bool {
+	// If the captcha is not enabled, the verification is passed directly
+	if !plugin.CaptchaEnabled() {
+		return true
+	}
 	info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType)
 	if err != nil {
 		log.Error(err)
diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go
index cb634d2a..0f3f8393 100644
--- a/internal/service/activity/activity.go
+++ b/internal/service/activity/activity.go
@@ -259,7 +259,7 @@ func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *sch
 	return resp, nil
 }
 
-// GetObjectTimelineDetail get object detail
+// getOneObjectDetail get object detail
 func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) (
 	resp *schema.ObjectTimelineDetail, err error) {
 	resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)}
diff --git a/internal/service/content/answer_service.go b/internal/service/content/answer_service.go
index d96d75c5..f8feda8a 100644
--- a/internal/service/content/answer_service.go
+++ b/internal/service/content/answer_service.go
@@ -241,12 +241,9 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
 	if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil {
 		return "", err
 	}
-	if as.reviewService.AddAnswerReview(ctx, insertData) {
-		if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, entity.AnswerStatusAvailable); err != nil {
-			return "", err
-		} else {
-			insertData.Status = entity.AnswerStatusAvailable
-		}
+	insertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent)
+	if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil {
+		return "", err
 	}
 	err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID)
 	if err != nil {
@@ -510,6 +507,18 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
 	return info, questionInfo, has, nil
 }
 
+func (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) {
+	answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
+	if err != nil {
+		return nil, err
+	}
+	if !has {
+		return nil, errors.BadRequest(reason.AnswerNotFound)
+	}
+	info := as.ShowFormat(ctx, answerInfo)
+	return info, nil
+}
+
 func (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) {
 	return as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId)
 }
@@ -625,6 +634,11 @@ func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *s
 }
 
 func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) {
+	// If the answer is updated by me, there is no notification for myself.
+	// equivalent behaviour as AnswerService.notificationAnswerTheQuestion
+	if questionUserID == answerUserID {
+		return
+	}
 	msg := &schema.NotificationMsg{
 		TriggerUserID:  answerUserID,
 		ReceiverUserID: questionUserID,
diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go
index e4f2b499..171f67bd 100644
--- a/internal/service/content/question_service.go
+++ b/internal/service/content/question_service.go
@@ -327,12 +327,9 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
 	if err != nil {
 		return
 	}
-	if qs.reviewService.AddQuestionReview(ctx, question, req.Tags) {
-		if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, entity.QuestionStatusAvailable); err != nil {
-			return nil, err
-		} else {
-			question.Status = entity.AnswerStatusAvailable
-		}
+	question.Status = qs.reviewService.AddQuestionReview(ctx, question, req.Tags, req.IP, req.UserAgent)
+	if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, question.Status); err != nil {
+		return nil, err
 	}
 	objectTagData := schema.TagChange{}
 	objectTagData.ObjectID = question.ID
diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go
index 4c9843a3..a2b33005 100644
--- a/internal/service/content/user_service.go
+++ b/internal/service/content/user_service.go
@@ -741,6 +741,24 @@ func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankin
 	return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil
 }
 
+// GetUserStaff get user staff
+func (us *UserService) GetUserStaff(ctx context.Context, req *schema.GetUserStaffReq) (
+	resp []*schema.GetUserStaffResp, err error) {
+	userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, req.PageSize, true)
+	if err != nil {
+		return nil, err
+	}
+	avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList)
+	for _, u := range userList {
+		resp = append(resp, &schema.GetUserStaffResp{
+			Username:    u.Username,
+			DisplayName: u.DisplayName,
+			Avatar:      avatarMapping[u.ID].GetURL(),
+		})
+	}
+	return resp, nil
+}
+
 // UserUnsubscribeNotification user unsubscribe email notification
 func (us *UserService) UserUnsubscribeNotification(
 	ctx context.Context, req *schema.UserUnsubscribeNotificationReq) (err error) {
@@ -858,7 +876,7 @@ func (us *UserService) SearchUserListByName(ctx context.Context, req *schema.Get
 	if len(req.Username) == 0 {
 		return resp, nil
 	}
-	userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5)
+	userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5, false)
 	if err != nil {
 		return resp, err
 	}
diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go
index ab164d4d..2b4462a6 100644
--- a/internal/service/rank/rank_service.go
+++ b/internal/service/rank/rank_service.go
@@ -242,7 +242,7 @@ func (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (
 	return powerMapping
 }
 
-// CheckRankPermission verify that the user meets the prestige criteria
+// checkUserRank verify that the user meets the prestige criteria
 func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) (
 	can bool, rank int) {
 	// get the amount of rank required for the current operation
diff --git a/internal/service/review/review_service.go b/internal/service/review/review_service.go
index 80c318b1..ab4079ee 100644
--- a/internal/service/review/review_service.go
+++ b/internal/service/review/review_service.go
@@ -99,28 +99,54 @@ func NewReviewService(
 
 // AddQuestionReview add review for question if needed
 func (cs *ReviewService) AddQuestionReview(ctx context.Context,
-	question *entity.Question, tags []*schema.TagItem) (needReview bool) {
+	question *entity.Question, tags []*schema.TagItem, ip, ua string) (questionStatus int) {
 	reviewContent := &plugin.ReviewContent{
 		ObjectType: constant.QuestionObjectType,
 		Title:      question.Title,
 		Content:    question.ParsedText,
+		IP:         ip,
+		UserAgent:  ua,
 	}
 	for _, tag := range tags {
 		reviewContent.Tags = append(reviewContent.Tags, tag.SlugName)
 	}
 	reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID)
-	return cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent)
+	reviewStatus := cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent)
+	switch reviewStatus {
+	case plugin.ReviewStatusApproved:
+		questionStatus = entity.QuestionStatusAvailable
+	case plugin.ReviewStatusNeedReview:
+		questionStatus = entity.QuestionStatusPending
+	case plugin.ReviewStatusDeleteDirectly:
+		questionStatus = entity.QuestionStatusDeleted
+	default:
+		questionStatus = entity.QuestionStatusAvailable
+	}
+	return questionStatus
 }
 
 // AddAnswerReview add review for answer if needed
 func (cs *ReviewService) AddAnswerReview(ctx context.Context,
-	answer *entity.Answer) (needReview bool) {
+	answer *entity.Answer, ip, ua string) (answerStatus int) {
 	reviewContent := &plugin.ReviewContent{
 		ObjectType: constant.AnswerObjectType,
 		Content:    answer.ParsedText,
+		IP:         ip,
+		UserAgent:  ua,
 	}
 	reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID)
-	return cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent)
+	reviewStatus := cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent)
+	switch reviewStatus {
+	case plugin.ReviewStatusApproved:
+		answerStatus = entity.AnswerStatusAvailable
+	case plugin.ReviewStatusNeedReview:
+		answerStatus = entity.AnswerStatusPending
+	case plugin.ReviewStatusDeleteDirectly:
+		answerStatus = entity.AnswerStatusDeleted
+	default:
+		answerStatus = entity.AnswerStatusAvailable
+	}
+	return answerStatus
 }
 
 // get review content author info
@@ -143,9 +169,9 @@ func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID
 
 // call plugin to review
 func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string,
-	reviewContent *plugin.ReviewContent) (approved bool) {
+	reviewContent *plugin.ReviewContent) (reviewStatus plugin.ReviewStatus) {
 	// As default, no need review
-	approved = true
+	reviewStatus = plugin.ReviewStatusApproved
 	objectID = uid.DeShortID(objectID)
 
 	r := &entity.Review{
@@ -161,23 +187,23 @@ func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectI
 
 	_ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error {
 		// If one of the reviewer plugin return false, then the review is not approved
-		if !approved {
+		if reviewStatus != plugin.ReviewStatusApproved {
 			return nil
 		}
 		if result := reviewer.Review(reviewContent); !result.Approved {
-			approved = false
+			reviewStatus = result.ReviewStatus
 			r.Reason = result.Reason
 			r.Submitter = reviewer.Info().SlugName
 		}
 		return nil
 	})
 
-	if !approved {
+	if reviewStatus == plugin.ReviewStatusNeedReview {
 		if err := cs.reviewRepo.AddReview(ctx, r); err != nil {
 			log.Errorf("add review failed, err: %v", err)
 		}
 	}
-	return approved
+	return reviewStatus
 }
 
 // UpdateReview update review
diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go
index 2036ec6e..01585876 100644
--- a/internal/service/siteinfo_common/siteinfo_service.go
+++ b/internal/service/siteinfo_common/siteinfo_service.go
@@ -22,6 +22,7 @@ package siteinfo_common
 import (
 	"context"
 	"encoding/json"
+	"html"
 
 	"github.com/apache/incubator-answer/internal/base/constant"
 	"github.com/apache/incubator-answer/internal/entity"
@@ -70,6 +71,7 @@ func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
 	if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
 		return nil, err
 	}
+	resp.Name = html.UnescapeString(resp.Name)
 	return resp, nil
 }
 
diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go
index 953b96f0..1dfe4f7f 100644
--- a/internal/service/tag_common/tag_common.go
+++ b/internal/service/tag_common/tag_common.go
@@ -321,10 +321,10 @@ func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (
 	if exist {
 		return nil, errors.BadRequest(reason.TagAlreadyExist)
 	}
-	SlugName := strings.ReplaceAll(req.SlugName, " ", "-")
-	SlugName = strings.ToLower(SlugName)
+	slugName := strings.ReplaceAll(req.SlugName, " ", "-")
+	slugName = strings.ToLower(slugName)
 	tagInfo := &entity.Tag{
-		SlugName:     SlugName,
+		SlugName:     slugName,
 		DisplayName:  req.DisplayName,
 		OriginalText: req.OriginalText,
 		ParsedText:   req.ParsedText,
@@ -837,14 +837,19 @@ func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTag
 	if !exist {
 		return errors.BadRequest(reason.TagNotFound)
 	}
+
+	//Adding equivalent slug formatting for tag update
+	slugName := strings.ReplaceAll(req.SlugName, " ", "-")
+	slugName = strings.ToLower(slugName)
+
 	//If the content is the same, ignore it
 	if tagInfo.OriginalText == req.OriginalText &&
 		tagInfo.DisplayName == req.DisplayName &&
-		tagInfo.SlugName == req.SlugName {
+		tagInfo.SlugName == slugName {
 		return nil
 	}
 
-	tagInfo.SlugName = req.SlugName
+	tagInfo.SlugName = slugName
 	tagInfo.DisplayName = req.DisplayName
 	tagInfo.OriginalText = req.OriginalText
 	tagInfo.ParsedText = req.ParsedText
diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go
index 334bcc4e..5e1a4830 100644
--- a/internal/service/user_common/user.go
+++ b/internal/service/user_common/user.go
@@ -21,9 +21,10 @@ package usercommon
 
 import (
 	"context"
+	"strings"
+
 	"github.com/apache/incubator-answer/internal/base/constant"
 	"github.com/apache/incubator-answer/pkg/converter"
-	"strings"
 
 	"github.com/Chain-Zhang/pinyin"
 	"github.com/apache/incubator-answer/internal/base/reason"
@@ -57,7 +58,7 @@ type UserRepo interface {
 	GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error)
 	GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
 	GetUserCount(ctx context.Context) (count int64, err error)
-	SearchUserListByName(ctx context.Context, name string, limit int) (userList []*entity.User, err error)
+	SearchUserListByName(ctx context.Context, name string, limit int, onlyStaff bool) (userList []*entity.User, err error)
 }
 
 // UserCommon user service
diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go
index ad762126..e537f554 100644
--- a/pkg/converter/markdown.go
+++ b/pkg/converter/markdown.go
@@ -21,6 +21,7 @@ package converter
 
 import (
 	"bytes"
+	"regexp"
 
 	"github.com/asaskevich/govalidator"
 	"github.com/microcosm-cc/bluemonday"
@@ -58,6 +59,7 @@ func Markdown2HTML(source string) string {
 	filter.RequireParseableURLs(false)
 	filter.RequireNoFollowOnFullyQualifiedLinks(false)
 	filter.AllowElements("kbd")
+	filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally()
 	html = filter.Sanitize(html)
 	return html
 }
diff --git a/plugin/captcha.go b/plugin/captcha.go
new file mode 100644
index 00000000..96047e67
--- /dev/null
+++ b/plugin/captcha.go
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package plugin
+
+type Captcha interface {
+	Base
+	// GetConfig required. Get the captcha plugin configuration.
+	// The configuration is used to generate the captcha for frontend. Such as the token for third-party service.
+	GetConfig() (configJsonStr string)
+	// Create optional. If this plugin need to create captcha via backend, implement this method.
+	// On other hand, if this plugin create captcha via third-party service, ignore this method.
+	// Return captcha: The captcha image base64 string, code: The real captcha code.
+	Create() (captcha, code string)
+	// Verify required. Verify the user input captcha is correct or not
+	// captcha: The captchaCode generated by Create method, if not implemented, it's empty.
+	Verify(captchaCode, userInput string) (pass bool)
+}
+
+var (
+	// CallCaptcha is a function that calls all registered parsers
+	callCaptcha,
+	registerCaptcha = MakePlugin[Captcha](false)
+)
+
+func CallCaptcha(fn func(fn Captcha) error) error {
+	slugName := ""
+	_ = callCaptcha(func(captcha Captcha) error {
+		slugName = captcha.Info().SlugName
+		return nil
+	})
+	if slugName == "" {
+		return nil
+	}
+	return callCaptcha(func(captcha Captcha) error {
+		if captcha.Info().SlugName == slugName {
+			return fn(captcha)
+		}
+		return nil
+	})
+}
+
+func CaptchaEnabled() (enabled bool) {
+	_ = callCaptcha(func(fn Captcha) error {
+		enabled = true
+		return nil
+	})
+	return
+}
+
+func coordinatedCaptchaPlugins(slugName string) (enabledSlugNames []string) {
+	isCaptcha := false
+	_ = callCaptcha(func(captcha Captcha) error {
+		name := captcha.Info().SlugName
+		if slugName == name {
+			isCaptcha = true
+		} else {
+			enabledSlugNames = append(enabledSlugNames, name)
+		}
+		return nil
+	})
+	if isCaptcha {
+		return enabledSlugNames
+	}
+	return nil
+}
diff --git a/plugin/plugin.go b/plugin/plugin.go
index 6aade8ee..df049417 100644
--- a/plugin/plugin.go
+++ b/plugin/plugin.go
@@ -21,6 +21,7 @@ package plugin
 
 import (
 	"encoding/json"
+	"sync"
 
 	"github.com/segmentfault/pacman/i18n"
 
@@ -94,6 +95,10 @@ func Register(p Base) {
 		registerReviewer(p.(Reviewer))
 	}
 
+	if _, ok := p.(Captcha); ok {
+		registerCaptcha(p.(Captcha))
+	}
+
 	if _, ok := p.(Embed); ok {
 		registerEmbed(p.(Embed))
 	}
@@ -142,11 +147,21 @@ func MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) {
 }
 
 type statusManager struct {
+	lock   sync.Mutex
 	status map[string]bool
 }
 
 func (m *statusManager) Enable(name string, enabled bool) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	if !enabled {
+		m.status[name] = enabled
+		return
+	}
 	m.status[name] = enabled
+	for _, slugName := range coordinatedCaptchaPlugins(name) {
+		m.status[slugName] = false
+	}
 }
 
 func (m *statusManager) IsEnabled(name string) bool {
diff --git a/plugin/reviewer.go b/plugin/reviewer.go
index 4c9165c6..488c6f9b 100644
--- a/plugin/reviewer.go
+++ b/plugin/reviewer.go
@@ -39,6 +39,10 @@ type ReviewContent struct {
 	// Review Language, the site language. e.g. en_US
 	// The plugin may reply the review result according to the language
 	Language string
+	// The user agent of the request web browser
+	UserAgent string
+	// The IP address of the request
+	IP string
 }
 
 type ReviewContentAuthor struct {
@@ -52,10 +56,20 @@ type ReviewContentAuthor struct {
 	Role int
 }
 
+type ReviewStatus string
+
+const (
+	ReviewStatusApproved       ReviewStatus = "approved"
+	ReviewStatusDeleteDirectly ReviewStatus = "delete_directly"
+	ReviewStatusNeedReview     ReviewStatus = "need_review"
+)
+
 // ReviewResult is a struct that contains the result of a review
 type ReviewResult struct {
 	// If the review is approved
 	Approved bool
+	// The status of the review
+	ReviewStatus ReviewStatus
 	// The reason for the result
 	Reason string
 }
diff --git a/script/plugin_list b/script/plugin_list
index 49324ca0..67ba306f 100644
--- a/script/plugin_list
+++ b/script/plugin_list
@@ -1,2 +1,3 @@
 github.com/apache/incubator-answer-plugins/connector-basic@latest
-github.com/apache/incubator-answer-plugins/reviewer-basic@latest
\ No newline at end of file
+github.com/apache/incubator-answer-plugins/reviewer-basic@latest
+github.com/apache/incubator-answer-plugins/captcha-basic@latest
\ No newline at end of file
diff --git a/ui/config-overrides.js b/ui/config-overrides.js
index b3ff6e8a..18237a1f 100644
--- a/ui/config-overrides.js
+++ b/ui/config-overrides.js
@@ -44,71 +44,62 @@ module.exports = {
       minChunks: 2,
       cacheGroups: {
         automaticNamePrefix: 'chunk',
-        components: {
-          test: /[\\/]components[\\/]/,
-          name: 'components',
+        mix1: {
+          test: (module, chunks) => {
+            return (
+              module.resource &&
+              (module.resource.includes('components') ||
+                /\/node_modules\/react-bootstrap\//.test(module.resource))
+            );
+          },
+          name: 'chunk-mix1',
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
           priority: 14,
           reuseExistingChunk: true,
           minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,
           chunks: 'initial',
         },
-        i18next: {
-          name: 'i18next',
-          test: /[\/]node_modules[\/](i18next)[\/]/,
+        mix2: {
+          name: 'chunk-mix2',
+          test: /[\/]node_modules[\/](i18next|lodash|marked|next-share)[\/]/,
           filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 12,
+          priority: 13,
           reuseExistingChunk: true,
           minChunks: 1,
           chunks: 'initial',
         },
-        reactBootstrap: {
-          name: 'react-bootstrap',
-          test: /[\/]node_modules[\/](react-bootstrap)[\/]/,
-          filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 11,
-          minChunks: 1,
-          chunks: 'initial',
-          reuseExistingChunk: true,
-        },
-        lodash: {
-          name: 'lodash',
-          test: /[\/]node_modules[\/](lodash)[\/]/,
+        mix3: {
+          name: 'chunk-mix3',
+          test: /[\/]node_modules[\/](@remix-run|@restart|axios|diff)[\/]/,
           filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 10,
+          priority: 12,
           reuseExistingChunk: true,
           minChunks: 1,
           chunks: 'initial',
         },
         codemirror: {
           name: 'codemirror',
-          test: /[\/]node_modules[\/](codemirror)[\/]/,
-          priority: 9,
-          reuseExistingChunk: true,
-          enforce: true,
-        },
-        nextShare: {
-          name: 'next-share',
-          test: /[\/]node_modules[\/](next-share)[\/]/,
-          filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 8,
+          test: /[\/]node_modules[\/](\@codemirror)[\/]/,
+          priority: 10,
           reuseExistingChunk: true,
-          minChunks: 1,
+          minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,
           chunks: 'initial',
+          enforce: true,
         },
-        marked: {
-          name: 'marked',
-          test: /[\/]node_modules[\/](marked)[\/]/,
-          filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 7,
+        lezer: {
+          name: 'lezer',
+          test: /[\/]node_modules[\/](\@lezer)[\/]/,
+          priority: 9,
           reuseExistingChunk: true,
-          minChunks: 1,
+          minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,
           chunks: 'initial',
+          enforce: true,
         },
         reactDom: {
           name: 'react-dom',
           test: /[\/]node_modules[\/](react-dom)[\/]/,
           filename: 'static/js/[name].[contenthash:8].chunk.js',
-          priority: 7,
+          priority: 8,
           reuseExistingChunk: true,
           chunks: 'all',
           enforce: true,
diff --git a/ui/package.json b/ui/package.json
index 63a1f85d..50ea3688 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,11 +16,15 @@
     "analyze": "source-map-explorer 'build/static/js/*.js'"
   },
   "dependencies": {
-    "axios": "^0.27.2",
+    "@codemirror/lang-markdown": "^6.2.4",
+    "@codemirror/language-data": "^6.5.0",
+    "@codemirror/state": "^6.4.1",
+    "@codemirror/view": "^6.26.1",
+    "axios": "^0.28.1",
     "bootstrap": "^5.3.2",
     "bootstrap-icons": "^1.10.5",
     "classnames": "^2.3.1",
-    "codemirror": "5.65.0",
+    "codemirror": "^6.0.1",
     "color": "^4.2.3",
     "copy-to-clipboard": "^3.3.2",
     "dayjs": "^1.11.5",
@@ -37,7 +41,7 @@
     "react-dom": "^18.2.0",
     "react-helmet-async": "^1.3.0",
     "react-i18next": "^11.18.3",
-    "react-router-dom": "^6.8.1",
+    "react-router-dom": "^6.22.3",
     "semver": "^7.3.8",
     "swr": "^1.3.0",
     "zustand": "^4.1.1"
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 0369458a..66974ab7 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -1,20 +1,3 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
 lockfileVersion: '6.0'
 
 settings:
@@ -25,9 +8,21 @@ importers:
 
   .:
     dependencies:
+      '@codemirror/lang-markdown':
+        specifier: ^6.2.4
+        version: 6.2.5
+      '@codemirror/language-data':
+        specifier: ^6.5.0
+        version: 6.5.1(@codemirror/view@6.26.3)
+      '@codemirror/state':
+        specifier: ^6.4.1
+        version: 6.4.1
+      '@codemirror/view':
+        specifier: ^6.26.1
+        version: 6.26.3
       axios:
-        specifier: ^0.27.2
-        version: 0.27.2
+        specifier: ^0.28.1
+        version: 0.28.1
       bootstrap:
         specifier: ^5.3.2
         version: 5.3.2(@popperjs/core@2.11.8)
@@ -38,8 +33,8 @@ importers:
         specifier: ^2.3.1
         version: 2.3.2
       codemirror:
-        specifier: 5.65.0
-        version: 5.65.0
+        specifier: ^6.0.1
+        version: 6.0.1(@lezer/common@1.2.1)
       color:
         specifier: ^4.2.3
         version: 4.2.3
@@ -89,8 +84,8 @@ importers:
         specifier: ^11.18.3
         version: 11.18.6(i18next@21.9.2)(react-dom@18.2.0)(react@18.2.0)
       react-router-dom:
-        specifier: ^6.8.1
-        version: 6.8.1(react-dom@18.2.0)(react@18.2.0)
+        specifier: ^6.22.3
+        version: 6.23.0(react-dom@18.2.0)(react@18.2.0)
       semver:
         specifier: ^7.3.8
         version: 7.3.8
@@ -231,37 +226,6 @@ importers:
         specifier: ^0.8.0
         version: 0.8.0
 
-  src/plugins/editor-chart:
-    dependencies:
-      mermaid:
-        specifier: ^9.1.7
-        version: 9.4.3
-    devDependencies:
-      '@typescript-eslint/eslint-plugin':
-        specifier: ^6.0.0
-        version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.3.2)
-      '@typescript-eslint/parser':
-        specifier: ^6.0.0
-        version: 6.11.0(eslint@8.53.0)(typescript@5.3.2)
-      '@vitejs/plugin-react-swc':
-        specifier: ^3.3.2
-        version: 3.5.0(vite@4.5.0)
-      eslint:
-        specifier: ^8.45.0
-        version: 8.53.0
-      eslint-plugin-react-hooks:
-        specifier: ^4.6.0
-        version: 4.6.0(eslint@8.53.0)
-      eslint-plugin-react-refresh:
-        specifier: ^0.4.3
-        version: 0.4.5(eslint@8.53.0)
-      typescript:
-        specifier: ^5.0.2
-        version: 5.3.2
-      vite:
-        specifier: ^4.4.5
-        version: 4.5.0(@types/node@16.11.59)(sass@1.54.9)
-
 packages:
 
   /@aashutoshrathi/word-wrap@1.2.6:
@@ -1599,8 +1563,318 @@ packages:
   /@bcoe/v8-coverage@0.2.3:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
 
-  /@braintree/sanitize-url@6.0.4:
-    resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
+  /@codemirror/autocomplete@6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1):
+    resolution: {integrity: sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==}
+    peerDependencies:
+      '@codemirror/language': ^6.0.0
+      '@codemirror/state': ^6.0.0
+      '@codemirror/view': ^6.0.0
+      '@lezer/common': ^1.0.0
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+    dev: false
+
+  /@codemirror/commands@6.5.0:
+    resolution: {integrity: sha512-rK+sj4fCAN/QfcY9BEzYMgp4wwL/q5aj/VfNSoH1RWPF9XS/dUwBkvlL3hpWgEjOqlpdN1uLC9UkjJ4tmyjJYg==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+    dev: false
+
+  /@codemirror/lang-angular@0.1.3:
+    resolution: {integrity: sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==}
+    dependencies:
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/lang-javascript': 6.2.2
+      '@codemirror/language': 6.10.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@codemirror/lang-cpp@6.0.2:
+    resolution: {integrity: sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@lezer/cpp': 1.1.2
+    dev: false
+
+  /@codemirror/lang-css@6.2.1(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/css': 1.1.8
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-go@6.0.0(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-mMT4YeYdKGjnffDBOhr1ur1glee4oV/rfMe28vzazNHZkSt7vSiuHiBcgr3L/79Cl2RIjFdpQ1XMD0/T8Rx64g==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/go': 1.0.0
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-html@6.4.9:
+    resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/lang-css': 6.2.1(@codemirror/view@6.26.3)
+      '@codemirror/lang-javascript': 6.2.2
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/css': 1.1.8
+      '@lezer/html': 1.3.9
+    dev: false
+
+  /@codemirror/lang-java@6.0.1:
+    resolution: {integrity: sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@lezer/java': 1.1.2
+    dev: false
+
+  /@codemirror/lang-javascript@6.2.2:
+    resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/lint': 6.5.0
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/javascript': 1.4.15
+    dev: false
+
+  /@codemirror/lang-json@6.0.1:
+    resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@lezer/json': 1.0.2
+    dev: false
+
+  /@codemirror/lang-less@6.0.2(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==}
+    dependencies:
+      '@codemirror/lang-css': 6.2.1(@codemirror/view@6.26.3)
+      '@codemirror/language': 6.10.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-liquid@6.2.1:
+    resolution: {integrity: sha512-J1Mratcm6JLNEiX+U2OlCDTysGuwbHD76XwuL5o5bo9soJtSbz2g6RU3vGHFyS5DC8rgVmFSzi7i6oBftm7tnA==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@codemirror/lang-markdown@6.2.5:
+    resolution: {integrity: sha512-Hgke565YcO4fd9pe2uLYxnMufHO5rQwRr+AAhFq8ABuhkrjyX8R5p5s+hZUTdV60O0dMRjxKhBLxz8pu/MkUVA==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/markdown': 1.3.0
+    dev: false
+
+  /@codemirror/lang-php@6.0.1:
+    resolution: {integrity: sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==}
+    dependencies:
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/php': 1.0.2
+    dev: false
+
+  /@codemirror/lang-python@6.1.5(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-hCm+8X6wrnXJCGf+QhmFu1AXkdTVG7dHy0Ly6SI1N3SRPptaMvwX6oNQonOXOMPvmcjiB0xq342KAxX3BYpijw==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/python': 1.1.13
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-rust@6.0.1:
+    resolution: {integrity: sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@lezer/rust': 1.0.2
+    dev: false
+
+  /@codemirror/lang-sass@6.0.2(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==}
+    dependencies:
+      '@codemirror/lang-css': 6.2.1(@codemirror/view@6.26.3)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/sass': 1.0.6
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-sql@6.6.3(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-fo5i3OD/7TmmqMtKycC4OaqfPsRxk0sKOb35g8cOtyUyyI2hfP2qXkDc7Asb6h7BiJK+MU/DYVPnQm6iNB5ZTw==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/lang-vue@0.1.3:
+    resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==}
+    dependencies:
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/lang-javascript': 6.2.2
+      '@codemirror/language': 6.10.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@codemirror/lang-wast@6.0.2:
+    resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@codemirror/lang-xml@6.1.0:
+    resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/xml': 1.0.5
+    dev: false
+
+  /@codemirror/lang-yaml@6.1.1(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/language': 6.10.1
+      '@codemirror/state': 6.4.1
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/yaml': 1.0.2
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/language-data@6.5.1(@codemirror/view@6.26.3):
+    resolution: {integrity: sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==}
+    dependencies:
+      '@codemirror/lang-angular': 0.1.3
+      '@codemirror/lang-cpp': 6.0.2
+      '@codemirror/lang-css': 6.2.1(@codemirror/view@6.26.3)
+      '@codemirror/lang-go': 6.0.0(@codemirror/view@6.26.3)
+      '@codemirror/lang-html': 6.4.9
+      '@codemirror/lang-java': 6.0.1
+      '@codemirror/lang-javascript': 6.2.2
+      '@codemirror/lang-json': 6.0.1
+      '@codemirror/lang-less': 6.0.2(@codemirror/view@6.26.3)
+      '@codemirror/lang-liquid': 6.2.1
+      '@codemirror/lang-markdown': 6.2.5
+      '@codemirror/lang-php': 6.0.1
+      '@codemirror/lang-python': 6.1.5(@codemirror/view@6.26.3)
+      '@codemirror/lang-rust': 6.0.1
+      '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.26.3)
+      '@codemirror/lang-sql': 6.6.3(@codemirror/view@6.26.3)
+      '@codemirror/lang-vue': 0.1.3
+      '@codemirror/lang-wast': 6.0.2
+      '@codemirror/lang-xml': 6.1.0
+      '@codemirror/lang-yaml': 6.1.1(@codemirror/view@6.26.3)
+      '@codemirror/language': 6.10.1
+      '@codemirror/legacy-modes': 6.4.0
+    transitivePeerDependencies:
+      - '@codemirror/view'
+    dev: false
+
+  /@codemirror/language@6.10.1:
+    resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+      style-mod: 4.1.2
+    dev: false
+
+  /@codemirror/legacy-modes@6.4.0:
+    resolution: {integrity: sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==}
+    dependencies:
+      '@codemirror/language': 6.10.1
+    dev: false
+
+  /@codemirror/lint@6.5.0:
+    resolution: {integrity: sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      crelt: 1.0.6
+    dev: false
+
+  /@codemirror/search@6.5.6:
+    resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+      crelt: 1.0.6
+    dev: false
+
+  /@codemirror/state@6.4.1:
+    resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==}
+    dev: false
+
+  /@codemirror/view@6.26.3:
+    resolution: {integrity: sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      style-mod: 4.1.2
+      w3c-keyname: 2.2.8
     dev: false
 
   /@commitlint/cli@17.1.2:
@@ -1914,204 +2188,6 @@ packages:
       postcss: 8.4.16
       postcss-selector-parser: 6.0.10
 
-  /@esbuild/android-arm64@0.18.20:
-    resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/android-arm@0.18.20:
-    resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [android]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/android-x64@0.18.20:
-    resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [android]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/darwin-arm64@0.18.20:
-    resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/darwin-x64@0.18.20:
-    resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [darwin]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/freebsd-arm64@0.18.20:
-    resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/freebsd-x64@0.18.20:
-    resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [freebsd]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-arm64@0.18.20:
-    resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-arm@0.18.20:
-    resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-ia32@0.18.20:
-    resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-loong64@0.18.20:
-    resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-mips64el@0.18.20:
-    resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-ppc64@0.18.20:
-    resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-riscv64@0.18.20:
-    resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-s390x@0.18.20:
-    resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/linux-x64@0.18.20:
-    resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [linux]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/netbsd-x64@0.18.20:
-    resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [netbsd]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/openbsd-x64@0.18.20:
-    resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [openbsd]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/sunos-x64@0.18.20:
-    resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [sunos]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/win32-arm64@0.18.20:
-    resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/win32-ia32@0.18.20:
-    resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@esbuild/win32-x64@0.18.20:
-    resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
   /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0):
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2496,6 +2572,133 @@ packages:
   /@leichtgewicht/ip-codec@2.0.4:
     resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
 
+  /@lezer/common@1.2.1:
+    resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==}
+    dev: false
+
+  /@lezer/cpp@1.1.2:
+    resolution: {integrity: sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/css@1.1.8:
+    resolution: {integrity: sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/go@1.0.0:
+    resolution: {integrity: sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/highlight@1.2.0:
+    resolution: {integrity: sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==}
+    dependencies:
+      '@lezer/common': 1.2.1
+    dev: false
+
+  /@lezer/html@1.3.9:
+    resolution: {integrity: sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/java@1.1.2:
+    resolution: {integrity: sha512-3j8X70JvYf0BZt8iSRLXLkt0Ry1hVUgH6wT32yBxH/Xi55nW2VMhc1Az4SKwu4YGSmxCm1fsqDDcHTuFjC8pmg==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/javascript@1.4.15:
+    resolution: {integrity: sha512-B082ZdjI0vo2AgLqD834GlRTE9gwRX8NzHzKq5uDwEnQ9Dq+A/CEhd3nf68tiNA2f9O+8jS1NeSTUYT9IAqcTw==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/json@1.0.2:
+    resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/lr@1.4.0:
+    resolution: {integrity: sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==}
+    dependencies:
+      '@lezer/common': 1.2.1
+    dev: false
+
+  /@lezer/markdown@1.3.0:
+    resolution: {integrity: sha512-ErbEQ15eowmJUyT095e9NJc3BI9yZ894fjSDtHftD0InkfUBGgnKSU6dvan9jqsZuNHg2+ag/1oyDRxNsENupQ==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+    dev: false
+
+  /@lezer/php@1.0.2:
+    resolution: {integrity: sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/python@1.1.13:
+    resolution: {integrity: sha512-AdbRAtdQq94PfTNd4kqMEJhH2fqa2JdoyyqqVewY6w34w2Gi6dg2JuOtOgR21Bi0zP9r0KjSSHOUq/tP7FVT8A==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/rust@1.0.2:
+    resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/sass@1.0.6:
+    resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/xml@1.0.5:
+    resolution: {integrity: sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
+  /@lezer/yaml@1.0.2:
+    resolution: {integrity: sha512-XCkwuxe+eumJ28nA9e1S6XKsXz9W7V/AG+WBiWOtiIuUpKcZ/bHuvN8bLxSDREIcybSRpEd/jvphh4vgm6Ed2g==}
+    dependencies:
+      '@lezer/common': 1.2.1
+      '@lezer/highlight': 1.2.0
+      '@lezer/lr': 1.4.0
+    dev: false
+
   /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1:
     resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
     dependencies:
@@ -2584,9 +2787,9 @@ packages:
       react: 18.2.0
     dev: false
 
-  /@remix-run/router@1.3.2:
-    resolution: {integrity: sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==}
-    engines: {node: '>=14'}
+  /@remix-run/router@1.16.0:
+    resolution: {integrity: sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==}
+    engines: {node: '>=14.0.0'}
     dev: false
 
   /@restart/hooks@0.4.15(react@18.2.0):
@@ -2786,129 +2989,12 @@ packages:
     transitivePeerDependencies:
       - supports-color
 
-  /@swc/core-darwin-arm64@1.3.100:
-    resolution: {integrity: sha512-XVWFsKe6ei+SsDbwmsuRkYck1SXRpO60Hioa4hoLwR8fxbA9eVp6enZtMxzVVMBi8ej5seZ4HZQeAWepbukiBw==}
-    engines: {node: '>=10'}
-    cpu: [arm64]
-    os: [darwin]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-darwin-x64@1.3.100:
-    resolution: {integrity: sha512-KF/MXrnH1nakm1wbt4XV8FS7kvqD9TGmVxeJ0U4bbvxXMvzeYUurzg3AJUTXYmXDhH/VXOYJE5N5RkwZZPs5iA==}
-    engines: {node: '>=10'}
-    cpu: [x64]
-    os: [darwin]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-linux-arm64-gnu@1.3.100:
-    resolution: {integrity: sha512-p8hikNnAEJrw5vHCtKiFT4hdlQxk1V7vqPmvUDgL/qe2menQDK/i12tbz7/3BEQ4UqUPnvwpmVn2d19RdEMNxw==}
-    engines: {node: '>=10'}
-    cpu: [arm64]
-    os: [linux]
-    libc: [glibc]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-linux-arm64-musl@1.3.100:
-    resolution: {integrity: sha512-BWx/0EeY89WC4q3AaIaBSGfQxkYxIlS3mX19dwy2FWJs/O+fMvF9oLk/CyJPOZzbp+1DjGeeoGFuDYpiNO91JA==}
-    engines: {node: '>=10'}
-    cpu: [arm64]
-    os: [linux]
-    libc: [musl]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-linux-x64-gnu@1.3.100:
-    resolution: {integrity: sha512-XUdGu3dxAkjsahLYnm8WijPfKebo+jHgHphDxaW0ovI6sTdmEGFDew7QzKZRlbYL2jRkUuuKuDGvD6lO5frmhA==}
-    engines: {node: '>=10'}
-    cpu: [x64]
-    os: [linux]
-    libc: [glibc]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-linux-x64-musl@1.3.100:
-    resolution: {integrity: sha512-PhoXKf+f0OaNW/GCuXjJ0/KfK9EJX7z2gko+7nVnEA0p3aaPtbP6cq1Ubbl6CMoPL+Ci3gZ7nYumDqXNc3CtLQ==}
-    engines: {node: '>=10'}
-    cpu: [x64]
-    os: [linux]
-    libc: [musl]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-win32-arm64-msvc@1.3.100:
-    resolution: {integrity: sha512-PwLADZN6F9cXn4Jw52FeP/MCLVHm8vwouZZSOoOScDtihjY495SSjdPnlosMaRSR4wJQssGwiD/4MbpgQPqbAw==}
-    engines: {node: '>=10'}
-    cpu: [arm64]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-win32-ia32-msvc@1.3.100:
-    resolution: {integrity: sha512-0f6nicKSLlDKlyPRl2JEmkpBV4aeDfRQg6n8mPqgL7bliZIcDahG0ej+HxgNjZfS3e0yjDxsNRa6sAqWU2Z60A==}
-    engines: {node: '>=10'}
-    cpu: [ia32]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core-win32-x64-msvc@1.3.100:
-    resolution: {integrity: sha512-b7J0rPoMkRTa3XyUGt8PwCaIBuYWsL2DqbirrQKRESzgCvif5iNpqaM6kjIjI/5y5q1Ycv564CB51YDpiS8EtQ==}
-    engines: {node: '>=10'}
-    cpu: [x64]
-    os: [win32]
-    requiresBuild: true
-    dev: true
-    optional: true
-
-  /@swc/core@1.3.100:
-    resolution: {integrity: sha512-7dKgTyxJjlrMwFZYb1auj3Xq0D8ZBe+5oeIgfMlRU05doXZypYJe0LAk0yjj3WdbwYzpF+T1PLxwTWizI0pckw==}
-    engines: {node: '>=10'}
-    requiresBuild: true
-    peerDependencies:
-      '@swc/helpers': ^0.5.0
-    peerDependenciesMeta:
-      '@swc/helpers':
-        optional: true
-    dependencies:
-      '@swc/counter': 0.1.2
-      '@swc/types': 0.1.5
-    optionalDependencies:
-      '@swc/core-darwin-arm64': 1.3.100
-      '@swc/core-darwin-x64': 1.3.100
-      '@swc/core-linux-arm64-gnu': 1.3.100
-      '@swc/core-linux-arm64-musl': 1.3.100
-      '@swc/core-linux-x64-gnu': 1.3.100
-      '@swc/core-linux-x64-musl': 1.3.100
-      '@swc/core-win32-arm64-msvc': 1.3.100
-      '@swc/core-win32-ia32-msvc': 1.3.100
-      '@swc/core-win32-x64-msvc': 1.3.100
-    dev: true
-
-  /@swc/counter@0.1.2:
-    resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==}
-    dev: true
-
   /@swc/helpers@0.5.3:
     resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==}
     dependencies:
       tslib: 2.6.2
     dev: false
 
-  /@swc/types@0.1.5:
-    resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
-    dev: true
-
   /@testing-library/dom@8.18.1:
     resolution: {integrity: sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==}
     engines: {node: '>=12'}
@@ -3072,7 +3158,7 @@ packages:
     resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==}
     dependencies:
       '@types/estree': 1.0.0
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
 
   /@types/eslint@8.56.2:
     resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==}
@@ -3149,12 +3235,8 @@ packages:
       pretty-format: 27.5.1
     dev: true
 
-  /@types/json-schema@7.0.13:
-    resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==}
-
   /@types/json-schema@7.0.15:
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
-    dev: true
 
   /@types/json5@0.0.29:
     resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
@@ -3341,35 +3423,6 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.3.2):
-    resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-    dependencies:
-      '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.2)
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.2)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.2)
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4
-      eslint: 8.53.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
-      natural-compare: 1.4.0
-      semver: 7.5.4
-      ts-api-utils: 1.0.3(typescript@5.3.2)
-      typescript: 5.3.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@typescript-eslint/experimental-utils@5.38.0(eslint@8.53.0)(typescript@4.9.5):
     resolution: {integrity: sha512-kzXBRfvGlicgGk4CYuRUqKvwc2s3wHXNssUWWJU18bhMRxriFm3BZWyQ6vEHBRpEIMKB6b7MIQHO+9lYlts19w==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -3422,27 +3475,6 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.2):
-    resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-    dependencies:
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.2)
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4
-      eslint: 8.53.0
-      typescript: 5.3.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@typescript-eslint/scope-manager@5.38.0:
     resolution: {integrity: sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -3497,26 +3529,6 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.2):
-    resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-    dependencies:
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.2)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.2)
-      debug: 4.3.4
-      eslint: 8.53.0
-      ts-api-utils: 1.0.3(typescript@5.3.2)
-      typescript: 5.3.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@typescript-eslint/types@5.38.0:
     resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -3567,34 +3579,13 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.2):
-    resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-    dependencies:
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4
-      globby: 11.1.0
-      is-glob: 4.0.3
-      semver: 7.5.4
-      ts-api-utils: 1.0.3(typescript@5.3.2)
-      typescript: 5.3.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@typescript-eslint/utils@5.38.0(eslint@8.53.0)(typescript@4.9.5):
     resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       '@typescript-eslint/scope-manager': 5.38.0
       '@typescript-eslint/types': 5.38.0
       '@typescript-eslint/typescript-estree': 5.38.0(typescript@4.9.5)
@@ -3612,7 +3603,7 @@ packages:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       '@types/semver': 7.5.5
       '@typescript-eslint/scope-manager': 6.11.0
       '@typescript-eslint/types': 6.11.0
@@ -3624,25 +3615,6 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.2):
-    resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
-      '@types/json-schema': 7.0.13
-      '@types/semver': 7.5.5
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.2)
-      eslint: 8.53.0
-      semver: 7.5.4
-    transitivePeerDependencies:
-      - supports-color
-      - typescript
-    dev: true
-
   /@typescript-eslint/visitor-keys@5.38.0:
     resolution: {integrity: sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -3661,17 +3633,6 @@ packages:
   /@ungap/structured-clone@1.2.0:
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
 
-  /@vitejs/plugin-react-swc@3.5.0(vite@4.5.0):
-    resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==}
-    peerDependencies:
-      vite: ^4 || ^5
-    dependencies:
-      '@swc/core': 1.3.100
-      vite: 4.5.0(@types/node@16.11.59)(sass@1.54.9)
-    transitivePeerDependencies:
-      - '@swc/helpers'
-    dev: true
-
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
     dependencies:
@@ -3950,7 +3911,6 @@ packages:
     resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
-    dev: true
 
   /acorn@8.8.0:
     resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
@@ -4253,11 +4213,12 @@ packages:
     resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==}
     engines: {node: '>=4'}
 
-  /axios@0.27.2:
-    resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
+  /axios@0.28.1:
+    resolution: {integrity: sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==}
     dependencies:
       follow-redirects: 1.15.2
       form-data: 4.0.0
+      proxy-from-env: 1.1.0
     transitivePeerDependencies:
       - debug
     dev: false
@@ -4761,8 +4722,18 @@ packages:
       chalk: 2.4.2
       q: 1.5.1
 
-  /codemirror@5.65.0:
-    resolution: {integrity: sha512-gWEnHKEcz1Hyz7fsQWpK7P0sPI2/kSkRX2tc7DFA6TmZuDN75x/1ejnH/Pn8adYKrLEA1V2ww6L00GudHZbSKw==}
+  /codemirror@6.0.1(@lezer/common@1.2.1):
+    resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
+    dependencies:
+      '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
+      '@codemirror/commands': 6.5.0
+      '@codemirror/language': 6.10.1
+      '@codemirror/lint': 6.5.0
+      '@codemirror/search': 6.5.6
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.26.3
+    transitivePeerDependencies:
+      - '@lezer/common'
     dev: false
 
   /collect-v8-coverage@1.0.1:
@@ -4953,18 +4924,6 @@ packages:
   /core-util-is@1.0.3:
     resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
 
-  /cose-base@1.0.3:
-    resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
-    dependencies:
-      layout-base: 1.0.2
-    dev: false
-
-  /cose-base@2.2.0:
-    resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
-    dependencies:
-      layout-base: 2.0.1
-    dev: false
-
   /cosmiconfig-typescript-loader@4.1.0(@types/node@14.18.29)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@4.9.5):
     resolution: {integrity: sha512-HbWIuR5O+XO5Oj9SZ5bzgrD4nN+rfhrm2PMb0FVx+t+XIvC45n8F0oTNnztXtspWGw0i2IzHaUWFD5LzV1JB4A==}
     engines: {node: '>=12', npm: '>=6'}
@@ -5003,6 +4962,10 @@ packages:
   /create-require@1.1.1:
     resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
 
+  /crelt@1.0.6:
+    resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+    dev: false
+
   /cross-spawn@7.0.3:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
@@ -5249,287 +5212,6 @@ packages:
       lodash.flow: 3.5.0
     dev: true
 
-  /cytoscape-cose-bilkent@4.1.0(cytoscape@3.27.0):
-    resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
-    peerDependencies:
-      cytoscape: ^3.2.0
-    dependencies:
-      cose-base: 1.0.3
-      cytoscape: 3.27.0
-    dev: false
-
-  /cytoscape-fcose@2.2.0(cytoscape@3.27.0):
-    resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
-    peerDependencies:
-      cytoscape: ^3.2.0
-    dependencies:
-      cose-base: 2.2.0
-      cytoscape: 3.27.0
-    dev: false
-
-  /cytoscape@3.27.0:
-    resolution: {integrity: sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg==}
-    engines: {node: '>=0.10'}
-    dependencies:
-      heap: 0.2.7
-      lodash: 4.17.21
-    dev: false
-
-  /d3-array@3.2.4:
-    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
-    engines: {node: '>=12'}
-    dependencies:
-      internmap: 2.0.3
-    dev: false
-
-  /d3-axis@3.0.0:
-    resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-brush@3.0.0:
-    resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-dispatch: 3.0.1
-      d3-drag: 3.0.0
-      d3-interpolate: 3.0.1
-      d3-selection: 3.0.0
-      d3-transition: 3.0.1(d3-selection@3.0.0)
-    dev: false
-
-  /d3-chord@3.0.1:
-    resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-path: 3.1.0
-    dev: false
-
-  /d3-color@3.1.0:
-    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-contour@4.0.2:
-    resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-array: 3.2.4
-    dev: false
-
-  /d3-delaunay@6.0.4:
-    resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
-    engines: {node: '>=12'}
-    dependencies:
-      delaunator: 5.0.0
-    dev: false
-
-  /d3-dispatch@3.0.1:
-    resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-drag@3.0.0:
-    resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-dispatch: 3.0.1
-      d3-selection: 3.0.0
-    dev: false
-
-  /d3-dsv@3.0.1:
-    resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
-    engines: {node: '>=12'}
-    hasBin: true
-    dependencies:
-      commander: 7.2.0
-      iconv-lite: 0.6.3
-      rw: 1.3.3
-    dev: false
-
-  /d3-ease@3.0.1:
-    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-fetch@3.0.1:
-    resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-dsv: 3.0.1
-    dev: false
-
-  /d3-force@3.0.0:
-    resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-dispatch: 3.0.1
-      d3-quadtree: 3.0.1
-      d3-timer: 3.0.1
-    dev: false
-
-  /d3-format@3.1.0:
-    resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-geo@3.1.0:
-    resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-array: 3.2.4
-    dev: false
-
-  /d3-hierarchy@3.1.2:
-    resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-interpolate@3.0.1:
-    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-color: 3.1.0
-    dev: false
-
-  /d3-path@3.1.0:
-    resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-polygon@3.0.1:
-    resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-quadtree@3.0.1:
-    resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-random@3.0.1:
-    resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-scale-chromatic@3.0.0:
-    resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-color: 3.1.0
-      d3-interpolate: 3.0.1
-    dev: false
-
-  /d3-scale@4.0.2:
-    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-array: 3.2.4
-      d3-format: 3.1.0
-      d3-interpolate: 3.0.1
-      d3-time: 3.1.0
-      d3-time-format: 4.1.0
-    dev: false
-
-  /d3-selection@3.0.0:
-    resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-shape@3.2.0:
-    resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-path: 3.1.0
-    dev: false
-
-  /d3-time-format@4.1.0:
-    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-time: 3.1.0
-    dev: false
-
-  /d3-time@3.1.0:
-    resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-array: 3.2.4
-    dev: false
-
-  /d3-timer@3.0.1:
-    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
-    engines: {node: '>=12'}
-    dev: false
-
-  /d3-transition@3.0.1(d3-selection@3.0.0):
-    resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
-    engines: {node: '>=12'}
-    peerDependencies:
-      d3-selection: 2 - 3
-    dependencies:
-      d3-color: 3.1.0
-      d3-dispatch: 3.0.1
-      d3-ease: 3.0.1
-      d3-interpolate: 3.0.1
-      d3-selection: 3.0.0
-      d3-timer: 3.0.1
-    dev: false
-
-  /d3-zoom@3.0.0:
-    resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-dispatch: 3.0.1
-      d3-drag: 3.0.0
-      d3-interpolate: 3.0.1
-      d3-selection: 3.0.0
-      d3-transition: 3.0.1(d3-selection@3.0.0)
-    dev: false
-
-  /d3@7.8.5:
-    resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==}
-    engines: {node: '>=12'}
-    dependencies:
-      d3-array: 3.2.4
-      d3-axis: 3.0.0
-      d3-brush: 3.0.0
-      d3-chord: 3.0.1
-      d3-color: 3.1.0
-      d3-contour: 4.0.2
-      d3-delaunay: 6.0.4
-      d3-dispatch: 3.0.1
-      d3-drag: 3.0.0
-      d3-dsv: 3.0.1
-      d3-ease: 3.0.1
-      d3-fetch: 3.0.1
-      d3-force: 3.0.0
-      d3-format: 3.1.0
-      d3-geo: 3.1.0
-      d3-hierarchy: 3.1.2
-      d3-interpolate: 3.0.1
-      d3-path: 3.1.0
-      d3-polygon: 3.0.1
-      d3-quadtree: 3.0.1
-      d3-random: 3.0.1
-      d3-scale: 4.0.2
-      d3-scale-chromatic: 3.0.0
-      d3-selection: 3.0.0
-      d3-shape: 3.2.0
-      d3-time: 3.1.0
-      d3-time-format: 4.1.0
-      d3-timer: 3.0.1
-      d3-transition: 3.0.1(d3-selection@3.0.0)
-      d3-zoom: 3.0.0
-    dev: false
-
-  /dagre-d3-es@7.0.9:
-    resolution: {integrity: sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==}
-    dependencies:
-      d3: 7.8.5
-      lodash-es: 4.17.21
-    dev: false
-
   /damerau-levenshtein@1.0.8:
     resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
 
@@ -5546,10 +5228,6 @@ packages:
       whatwg-mimetype: 2.3.0
       whatwg-url: 8.7.0
 
-  /dayjs@1.11.10:
-    resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
-    dev: false
-
   /dayjs@1.11.5:
     resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
     dev: false
@@ -5674,12 +5352,6 @@ packages:
   /defined@1.0.0:
     resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==}
 
-  /delaunator@5.0.0:
-    resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
-    dependencies:
-      robust-predicates: 3.0.2
-    dev: false
-
   /delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
@@ -5828,10 +5500,6 @@ packages:
     dependencies:
       domelementtype: 2.3.0
 
-  /dompurify@2.4.3:
-    resolution: {integrity: sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==}
-    dev: false
-
   /domutils@1.7.0:
     resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==, tarball: https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz}
     dependencies:
@@ -5889,10 +5557,6 @@ packages:
     resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==}
     dev: true
 
-  /elkjs@0.8.2:
-    resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==}
-    dev: false
-
   /emittery@0.10.2:
     resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==}
     engines: {node: '>=12'}
@@ -6068,36 +5732,6 @@ packages:
       is-date-object: 1.0.5
       is-symbol: 1.0.4
 
-  /esbuild@0.18.20:
-    resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
-    engines: {node: '>=12'}
-    hasBin: true
-    requiresBuild: true
-    optionalDependencies:
-      '@esbuild/android-arm': 0.18.20
-      '@esbuild/android-arm64': 0.18.20
-      '@esbuild/android-x64': 0.18.20
-      '@esbuild/darwin-arm64': 0.18.20
-      '@esbuild/darwin-x64': 0.18.20
-      '@esbuild/freebsd-arm64': 0.18.20
-      '@esbuild/freebsd-x64': 0.18.20
-      '@esbuild/linux-arm': 0.18.20
-      '@esbuild/linux-arm64': 0.18.20
-      '@esbuild/linux-ia32': 0.18.20
-      '@esbuild/linux-loong64': 0.18.20
-      '@esbuild/linux-mips64el': 0.18.20
-      '@esbuild/linux-ppc64': 0.18.20
-      '@esbuild/linux-riscv64': 0.18.20
-      '@esbuild/linux-s390x': 0.18.20
-      '@esbuild/linux-x64': 0.18.20
-      '@esbuild/netbsd-x64': 0.18.20
-      '@esbuild/openbsd-x64': 0.18.20
-      '@esbuild/sunos-x64': 0.18.20
-      '@esbuild/win32-arm64': 0.18.20
-      '@esbuild/win32-ia32': 0.18.20
-      '@esbuild/win32-x64': 0.18.20
-    dev: true
-
   /escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
@@ -6510,14 +6144,6 @@ packages:
     dependencies:
       eslint: 8.53.0
 
-  /eslint-plugin-react-refresh@0.4.5(eslint@8.53.0):
-    resolution: {integrity: sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==}
-    peerDependencies:
-      eslint: '>=7'
-    dependencies:
-      eslint: 8.53.0
-    dev: true
-
   /eslint-plugin-react@7.33.2(eslint@8.53.0):
     resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
     engines: {node: '>=4'}
@@ -6954,7 +6580,7 @@ packages:
         optional: true
     dependencies:
       '@babel/code-frame': 7.18.6
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       chalk: 4.1.2
       chokidar: 3.5.3
       cosmiconfig: 6.0.0
@@ -7258,10 +6884,6 @@ packages:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
 
-  /heap@0.2.7:
-    resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
-    dev: false
-
   /hoopy@0.1.4:
     resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
     engines: {node: '>= 6.0.0'}
@@ -7528,11 +7150,6 @@ packages:
       hasown: 2.0.0
       side-channel: 1.0.4
 
-  /internmap@2.0.3:
-    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
-    engines: {node: '>=12'}
-    dev: false
-
   /invariant@2.2.4:
     resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
     dependencies:
@@ -8421,7 +8038,7 @@ packages:
         optional: true
     dependencies:
       abab: 2.0.6
-      acorn: 8.10.0
+      acorn: 8.11.3
       acorn-globals: 6.0.0
       cssom: 0.4.4
       cssstyle: 2.3.0
@@ -8527,10 +8144,6 @@ packages:
       object.assign: 4.1.4
       object.values: 1.1.7
 
-  /khroma@2.1.0:
-    resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
-    dev: false
-
   /kind-of@6.0.3:
     resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
     engines: {node: '>=0.10.0'}
@@ -8552,14 +8165,6 @@ packages:
     dependencies:
       language-subtag-registry: 0.3.22
 
-  /layout-base@1.0.2:
-    resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
-    dev: false
-
-  /layout-base@2.0.1:
-    resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
-    dev: false
-
   /leven@3.1.0:
     resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
     engines: {node: '>=6'}
@@ -8662,10 +8267,6 @@ packages:
     dependencies:
       p-locate: 5.0.0
 
-  /lodash-es@4.17.21:
-    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
-    dev: false
-
   /lodash.debounce@4.0.8:
     resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
 
@@ -8807,27 +8408,6 @@ packages:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
 
-  /mermaid@9.4.3:
-    resolution: {integrity: sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==}
-    dependencies:
-      '@braintree/sanitize-url': 6.0.4
-      cytoscape: 3.27.0
-      cytoscape-cose-bilkent: 4.1.0(cytoscape@3.27.0)
-      cytoscape-fcose: 2.2.0(cytoscape@3.27.0)
-      d3: 7.8.5
-      dagre-d3-es: 7.0.9
-      dayjs: 1.11.10
-      dompurify: 2.4.3
-      elkjs: 0.8.2
-      khroma: 2.1.0
-      lodash-es: 4.17.21
-      non-layered-tidy-tree-layout: 2.0.2
-      stylis: 4.3.0
-      ts-dedent: 2.2.0
-      uuid: 9.0.1
-      web-worker: 1.2.0
-    dev: false
-
   /methods@1.1.2:
     resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
     engines: {node: '>= 0.6'}
@@ -8935,12 +8515,6 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
-  /nanoid@3.3.7:
-    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
-    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
-    hasBin: true
-    dev: true
-
   /natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
@@ -8987,10 +8561,6 @@ packages:
   /node-releases@2.0.6:
     resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
 
-  /non-layered-tidy-tree-layout@2.0.2:
-    resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
-    dev: false
-
   /normalize-package-data@2.5.0:
     resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
     dependencies:
@@ -10062,15 +9632,6 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
-  /postcss@8.4.32:
-    resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
-    engines: {node: ^10 || ^12 || >=14}
-    dependencies:
-      nanoid: 3.3.7
-      picocolors: 1.0.0
-      source-map-js: 1.0.2
-    dev: true
-
   /prelude-ls@1.1.2:
     resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
     engines: {node: '>= 0.8.0'}
@@ -10168,6 +9729,10 @@ packages:
       forwarded: 0.2.0
       ipaddr.js: 1.9.1
 
+  /proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+    dev: false
+
   /psl@1.9.0:
     resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
 
@@ -10418,26 +9983,26 @@ packages:
     resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==}
     engines: {node: '>=0.10.0'}
 
-  /react-router-dom@6.8.1(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==}
-    engines: {node: '>=14'}
+  /react-router-dom@6.23.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
       react-dom: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.3.2
+      '@remix-run/router': 1.16.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      react-router: 6.8.1(react@18.2.0)
+      react-router: 6.23.0(react@18.2.0)
     dev: false
 
-  /react-router@6.8.1(react@18.2.0):
-    resolution: {integrity: sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==}
-    engines: {node: '>=14'}
+  /react-router@6.23.0(react@18.2.0):
+    resolution: {integrity: sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.3.2
+      '@remix-run/router': 1.16.0
       react: 18.2.0
     dev: false
 
@@ -10822,10 +10387,6 @@ packages:
     dependencies:
       glob: 7.2.3
 
-  /robust-predicates@3.0.2:
-    resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
-    dev: false
-
   /rollup-plugin-terser@7.0.2(rollup@2.79.0):
     resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
     peerDependencies:
@@ -10844,14 +10405,6 @@ packages:
     optionalDependencies:
       fsevents: 2.3.3
 
-  /rollup@3.29.4:
-    resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
-    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
-    hasBin: true
-    optionalDependencies:
-      fsevents: 2.3.3
-    dev: true
-
   /run-applescript@5.0.0:
     resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==}
     engines: {node: '>=12'}
@@ -10864,10 +10417,6 @@ packages:
     dependencies:
       queue-microtask: 1.2.3
 
-  /rw@1.3.3:
-    resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
-    dev: false
-
   /safe-array-concat@1.0.1:
     resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
     engines: {node: '>=0.4'}
@@ -10947,7 +10496,7 @@ packages:
     resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==}
     engines: {node: '>= 8.9.0'}
     dependencies:
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       ajv: 6.12.6
       ajv-keywords: 3.5.2(ajv@6.12.6)
 
@@ -10955,7 +10504,7 @@ packages:
     resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==}
     engines: {node: '>= 8.9.0'}
     dependencies:
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       ajv: 6.12.6
       ajv-keywords: 3.5.2(ajv@6.12.6)
 
@@ -10963,7 +10512,7 @@ packages:
     resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       ajv: 6.12.6
       ajv-keywords: 3.5.2(ajv@6.12.6)
 
@@ -10980,7 +10529,7 @@ packages:
     resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==}
     engines: {node: '>= 12.13.0'}
     dependencies:
-      '@types/json-schema': 7.0.13
+      '@types/json-schema': 7.0.15
       ajv: 8.11.0
       ajv-formats: 2.1.1(ajv@8.11.0)
       ajv-keywords: 5.1.0(ajv@8.11.0)
@@ -11494,6 +11043,10 @@ packages:
     dependencies:
       webpack: 5.74.0
 
+  /style-mod@4.1.2:
+    resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
+    dev: false
+
   /stylehacks@5.1.0(postcss@8.4.16):
     resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==}
     engines: {node: ^10 || ^12 || >=14.0}
@@ -11504,10 +11057,6 @@ packages:
       postcss: 8.4.16
       postcss-selector-parser: 6.0.10
 
-  /stylis@4.3.0:
-    resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==}
-    dev: false
-
   /supports-color@5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
@@ -11713,7 +11262,7 @@ packages:
     hasBin: true
     dependencies:
       '@jridgewell/source-map': 0.3.2
-      acorn: 8.10.0
+      acorn: 8.11.3
       commander: 2.20.3
       source-map-support: 0.5.21
 
@@ -11823,20 +11372,6 @@ packages:
       typescript: 4.9.5
     dev: true
 
-  /ts-api-utils@1.0.3(typescript@5.3.2):
-    resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
-    engines: {node: '>=16.13.0'}
-    peerDependencies:
-      typescript: '>=4.2.0'
-    dependencies:
-      typescript: 5.3.2
-    dev: true
-
-  /ts-dedent@2.2.0:
-    resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
-    engines: {node: '>=6.10'}
-    dev: false
-
   /ts-node@10.9.1(@types/node@16.11.59)(typescript@4.9.5):
     resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
     hasBin: true
@@ -11989,12 +11524,6 @@ packages:
     engines: {node: '>=4.2.0'}
     hasBin: true
 
-  /typescript@5.3.2:
-    resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
-    engines: {node: '>=14.17'}
-    hasBin: true
-    dev: true
-
   /unbox-primitive@1.0.2:
     resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
     dependencies:
@@ -12139,11 +11668,6 @@ packages:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
 
-  /uuid@9.0.1:
-    resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
-    hasBin: true
-    dev: false
-
   /v8-compile-cache-lib@3.0.1:
     resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
 
@@ -12166,43 +11690,6 @@ packages:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
 
-  /vite@4.5.0(@types/node@16.11.59)(sass@1.54.9):
-    resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
-    engines: {node: ^14.18.0 || >=16.0.0}
-    hasBin: true
-    peerDependencies:
-      '@types/node': '>= 14'
-      less: '*'
-      lightningcss: ^1.21.0
-      sass: '*'
-      stylus: '*'
-      sugarss: '*'
-      terser: ^5.4.0
-    peerDependenciesMeta:
-      '@types/node':
-        optional: true
-      less:
-        optional: true
-      lightningcss:
-        optional: true
-      sass:
-        optional: true
-      stylus:
-        optional: true
-      sugarss:
-        optional: true
-      terser:
-        optional: true
-    dependencies:
-      '@types/node': 16.11.59
-      esbuild: 0.18.20
-      postcss: 8.4.32
-      rollup: 3.29.4
-      sass: 1.54.9
-    optionalDependencies:
-      fsevents: 2.3.3
-    dev: true
-
   /void-elements@3.1.0:
     resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
     engines: {node: '>=0.10.0'}
@@ -12213,6 +11700,10 @@ packages:
     dependencies:
       browser-process-hrtime: 1.0.0
 
+  /w3c-keyname@2.2.8:
+    resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+    dev: false
+
   /w3c-xmlserializer@2.0.0:
     resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==}
     engines: {node: '>=10'}
@@ -12242,10 +11733,6 @@ packages:
     dependencies:
       minimalistic-assert: 1.0.1
 
-  /web-worker@1.2.0:
-    resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==}
-    dev: false
-
   /webidl-conversions@4.0.2:
     resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
 
diff --git a/ui/public/index.html b/ui/public/index.html
index f5a9c141..f15dd970 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -89,6 +89,10 @@
     </div>
   </body>
   <script>
+    // disable react devtools
+    if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
+      window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
+    }
     /**
      * @description: Prompt that the browser version is too low
     */
diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx
index b8357105..b58d0c8c 100644
--- a/ui/src/components/Actions/index.tsx
+++ b/ui/src/components/Actions/index.tsx
@@ -25,7 +25,8 @@ import classNames from 'classnames';
 
 import { Icon } from '@/components';
 import { loggedUserInfoStore } from '@/stores';
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { tryNormalLogged } from '@/utils/guard';
 import { bookmark, postVote } from '@/services';
 import * as Types from '@/common/interface';
@@ -56,7 +57,7 @@ const Index: FC<Props> = ({ className, data, source }) => {
   const { username = '' } = loggedUserInfoStore((state) => state.user);
   const toast = useToast();
   const { t } = useTranslation();
-  const vCaptcha = useCaptchaModal('vote');
+  const vCaptcha = useCaptchaPlugin('vote');
 
   useEffect(() => {
     if (data) {
@@ -70,6 +71,42 @@ const Index: FC<Props> = ({ className, data, source }) => {
     }
   }, []);
 
+  const submitVote = (type) => {
+    const isCancel = (type === 'up' && like) || (type === 'down' && hate);
+    const imgCode: Types.ImgCodeReq = {
+      captcha_id: undefined,
+      captcha_code: undefined,
+    };
+    vCaptcha?.resolveCaptchaReq?.(imgCode);
+
+    postVote(
+      {
+        object_id: data?.id,
+        is_cancel: isCancel,
+        ...imgCode,
+      },
+      type,
+    )
+      .then(async (res) => {
+        await vCaptcha?.close();
+        setVotes(res.votes);
+        setLike(res.vote_status === 'vote_up');
+        setHated(res.vote_status === 'vote_down');
+      })
+      .catch((err) => {
+        if (err?.isError) {
+          vCaptcha?.handleCaptchaError(err.list);
+        }
+        const errMsg = err?.value;
+        if (errMsg) {
+          toast.onShow({
+            msg: errMsg,
+            variant: 'danger',
+          });
+        }
+      });
+  };
+
   const handleVote = (type: 'up' | 'down') => {
     if (!tryNormalLogged(true)) {
       return;
@@ -82,39 +119,14 @@ const Index: FC<Props> = ({ className, data, source }) => {
       });
       return;
     }
-    const isCancel = (type === 'up' && like) || (type === 'down' && hate);
+
+    if (!vCaptcha) {
+      submitVote(type);
+      return;
+    }
+
     vCaptcha.check(() => {
-      const imgCode: Types.ImgCodeReq = {
-        captcha_id: undefined,
-        captcha_code: undefined,
-      };
-      vCaptcha.resolveCaptchaReq(imgCode);
-      postVote(
-        {
-          object_id: data?.id,
-          is_cancel: isCancel,
-          ...imgCode,
-        },
-        type,
-      )
-        .then(async (res) => {
-          await vCaptcha.close();
-          setVotes(res.votes);
-          setLike(res.vote_status === 'vote_up');
-          setHated(res.vote_status === 'vote_down');
-        })
-        .catch((err) => {
-          if (err?.isError) {
-            vCaptcha.handleCaptchaError(err.list);
-          }
-          const errMsg = err?.value;
-          if (errMsg) {
-            toast.onShow({
-              msg: errMsg,
-              variant: 'danger',
-            });
-          }
-        });
+      submitVote(type);
     });
   };
 
diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx
index 05ae5b2d..12368193 100644
--- a/ui/src/components/Comment/components/Form/index.tsx
+++ b/ui/src/components/Comment/components/Form/index.tsx
@@ -65,6 +65,7 @@ const Index = ({
       }
     });
   };
+
   return (
     <div
       className={classNames(
diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx
index 81345e7e..69c705b8 100644
--- a/ui/src/components/Comment/index.tsx
+++ b/ui/src/components/Comment/index.tsx
@@ -35,6 +35,7 @@ import {
   bgFadeOut,
 } from '@/utils';
 import { tryNormalLogged } from '@/utils/guard';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import {
   useQueryComments,
   addComment,
@@ -66,14 +67,14 @@ const Comment = ({ objectId, mode, commentId }) => {
   const reportModal = useReportModal();
 
   const addCaptcha = useCaptchaModal('comment');
-  const editCaptcha = useCaptchaModal('edit');
-  const dCaptcha = useCaptchaModal('delete');
-  const vCaptcha = useCaptchaModal('vote');
+  const editCaptcha = useCaptchaPlugin('edit');
+  const dCaptcha = useCaptchaPlugin('delete');
+  const vCaptcha = useCaptchaPlugin('vote');
 
   const { t } = useTranslation('translation', { keyPrefix: 'comment' });
 
   useEffect(() => {
-    if (pageIndex === 0 && commentId) {
+    if (pageIndex === 0 && commentId && comments.length !== 0) {
       setTimeout(() => {
         const el = document.getElementById(commentId);
         scrollToElementTop(el);
@@ -84,7 +85,7 @@ const Comment = ({ objectId, mode, commentId }) => {
     return () => {
       updateCurrentReplyId('');
     };
-  }, []);
+  }, [comments]);
 
   useEffect(() => {
     if (!data) {
@@ -136,6 +137,85 @@ const Comment = ({ objectId, mode, commentId }) => {
     );
   };
 
+  const submitUpdateComment = (params, item) => {
+    const up = {
+      ...params,
+      comment_id: item.comment_id,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    editCaptcha?.resolveCaptchaReq(up);
+
+    return updateComment(up)
+      .then(async (res) => {
+        await editCaptcha?.close();
+        setComments(
+          comments.map((comment) => {
+            if (comment.comment_id === item.comment_id) {
+              comment.showEdit = false;
+              comment.parsed_text = res.parsed_text;
+              comment.original_text = res.original_text;
+            }
+            return comment;
+          }),
+        );
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const captchaErr = editCaptcha?.handleCaptchaError(err.list);
+          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
+          if (!(captchaErr && err.list.length === 1)) {
+            return Promise.reject(err);
+          }
+        }
+        return Promise.resolve();
+      });
+  };
+
+  const submitAddComment = (params, item) => {
+    const req = {
+      ...params,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    addCaptcha?.resolveCaptchaReq(req);
+
+    return addComment(req)
+      .then(async (res) => {
+        await addCaptcha?.close();
+        if (item.type === 'reply') {
+          const index = comments.findIndex(
+            (comment) => comment.comment_id === item.comment_id,
+          );
+          updateCurrentReplyId('');
+          comments.splice(index + 1, 0, res);
+          setComments([...comments]);
+        } else {
+          setComments([
+            ...comments.map((comment) => {
+              if (comment.comment_id === item.comment_id) {
+                updateCurrentReplyId('');
+              }
+              return comment;
+            }),
+            res,
+          ]);
+        }
+
+        setVisibleComment(false);
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          const captchaErr = addCaptcha?.handleCaptchaError(ex.list);
+          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
+          if (!(captchaErr && ex.list.length === 1)) {
+            return Promise.reject(ex);
+          }
+        }
+        return Promise.resolve();
+      });
+  };
+
   const handleSendReply = (item) => {
     const users = matchedUsers(item.value);
     const userNames = unionBy(users.map((user) => user.userName));
@@ -153,80 +233,36 @@ const Comment = ({ objectId, mode, commentId }) => {
     };
 
     if (item.type === 'edit') {
-      return editCaptcha.check(() => {
-        const up = {
-          ...params,
-          comment_id: item.comment_id,
-          captcha_code: undefined,
-          captcha_id: undefined,
-        };
-        editCaptcha.resolveCaptchaReq(up);
-
-        return updateComment(up)
-          .then(async (res) => {
-            await editCaptcha.close();
-            setComments(
-              comments.map((comment) => {
-                if (comment.comment_id === item.comment_id) {
-                  comment.showEdit = false;
-                  comment.parsed_text = res.parsed_text;
-                  comment.original_text = res.original_text;
-                }
-                return comment;
-              }),
-            );
-          })
-          .catch((err) => {
-            if (err.isError) {
-              editCaptcha.handleCaptchaError(err.list);
-            }
-          });
-      });
+      if (!editCaptcha) {
+        return submitUpdateComment(params, item);
+      }
+      return editCaptcha.check(() => submitUpdateComment(params, item));
     }
 
-    return addCaptcha.check(() => {
-      const req = {
-        ...params,
-        captcha_code: undefined,
-        captcha_id: undefined,
-      };
-      addCaptcha.resolveCaptchaReq(req);
-
-      return addComment(req)
-        .then(async (res) => {
-          await addCaptcha.close();
-          if (item.type === 'reply') {
-            const index = comments.findIndex(
-              (comment) => comment.comment_id === item.comment_id,
-            );
-            updateCurrentReplyId('');
-            comments.splice(index + 1, 0, res);
-            setComments([...comments]);
-          } else {
-            setComments([
-              ...comments.map((comment) => {
-                if (comment.comment_id === item.comment_id) {
-                  updateCurrentReplyId('');
-                }
-                return comment;
-              }),
-              res,
-            ]);
-          }
+    if (!addCaptcha) {
+      return submitAddComment(params, item);
+    }
 
-          setVisibleComment(false);
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            const captchaErr = addCaptcha.handleCaptchaError(ex.list);
-            // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
-            if (!(captchaErr && ex.list.length === 1)) {
-              return Promise.reject(ex);
-            }
-          }
-          return Promise.resolve();
-        });
-    });
+    return addCaptcha.check(() => submitAddComment(params, item));
+  };
+
+  const submitDeleteComment = (id) => {
+    const imgCode = { captcha_id: undefined, captcha_code: undefined };
+    dCaptcha?.resolveCaptchaReq(imgCode);
+
+    deleteComment(id, imgCode)
+      .then(async () => {
+        await dCaptcha?.close();
+        if (pageIndex === 0) {
+          mutate();
+        }
+        setComments(comments.filter((item) => item.comment_id !== id));
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
   };
 
   const handleDelete = (id) => {
@@ -236,67 +272,64 @@ const Comment = ({ objectId, mode, commentId }) => {
       confirmBtnVariant: 'danger',
       confirmText: t('delete', { keyPrefix: 'btns' }),
       onConfirm: () => {
+        if (!dCaptcha) {
+          submitDeleteComment(id);
+          return;
+        }
         dCaptcha.check(() => {
-          const imgCode = { captcha_id: undefined, captcha_code: undefined };
-          dCaptcha.resolveCaptchaReq(imgCode);
-
-          deleteComment(id, imgCode)
-            .then(async () => {
-              await dCaptcha.close();
-              if (pageIndex === 0) {
-                mutate();
-              }
-              setComments(comments.filter((item) => item.comment_id !== id));
-            })
-            .catch((ex) => {
-              if (ex.isError) {
-                dCaptcha.handleCaptchaError(ex.list);
-              }
-            });
+          submitDeleteComment(id);
         });
       },
     });
   };
 
+  const submitVoteComment = (id, is_cancel) => {
+    const imgCode: Types.ImgCodeReq = {
+      captcha_id: undefined,
+      captcha_code: undefined,
+    };
+    vCaptcha?.resolveCaptchaReq(imgCode);
+
+    postVote(
+      {
+        object_id: id,
+        is_cancel,
+        ...imgCode,
+      },
+      'up',
+    )
+      .then(async () => {
+        await vCaptcha?.close();
+        setComments(
+          comments.map((item) => {
+            if (item.comment_id === id) {
+              item.vote_count = is_cancel
+                ? item.vote_count - 1
+                : item.vote_count + 1;
+              item.is_vote = !is_cancel;
+            }
+            return item;
+          }),
+        );
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          vCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
   const handleVote = (id, is_cancel) => {
     if (!tryNormalLogged(true)) {
       return;
     }
 
+    if (!vCaptcha) {
+      submitVoteComment(id, is_cancel);
+      return;
+    }
+
     vCaptcha.check(() => {
-      const imgCode: Types.ImgCodeReq = {
-        captcha_id: undefined,
-        captcha_code: undefined,
-      };
-      vCaptcha.resolveCaptchaReq(imgCode);
-
-      postVote(
-        {
-          object_id: id,
-          is_cancel,
-          ...imgCode,
-        },
-        'up',
-      )
-        .then(async () => {
-          await vCaptcha.close();
-          setComments(
-            comments.map((item) => {
-              if (item.comment_id === id) {
-                item.vote_count = is_cancel
-                  ? item.vote_count - 1
-                  : item.vote_count + 1;
-                item.is_vote = !is_cancel;
-              }
-              return item;
-            }),
-          );
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            vCaptcha.handleCaptchaError(ex.list);
-          }
-        });
+      submitVoteComment(id, is_cancel);
     });
   };
 
diff --git a/ui/src/components/Editor/ToolBars/blockquote.tsx b/ui/src/components/Editor/ToolBars/blockquote.tsx
index 30a99141..68d73cfc 100644
--- a/ui/src/components/Editor/ToolBars/blockquote.tsx
+++ b/ui/src/components/Editor/ToolBars/blockquote.tsx
@@ -29,7 +29,7 @@ const BlockQuote = () => {
 
   const item = {
     label: 'blockquote',
-    keyMap: ['Ctrl-Q'],
+    keyMap: ['Ctrl-q'],
     tip: `${t('blockquote.text')} (Ctrl+Q)`,
   };
 
diff --git a/ui/src/components/Editor/ToolBars/bold.tsx b/ui/src/components/Editor/ToolBars/bold.tsx
index 6731ee07..480872ae 100644
--- a/ui/src/components/Editor/ToolBars/bold.tsx
+++ b/ui/src/components/Editor/ToolBars/bold.tsx
@@ -28,8 +28,8 @@ const Bold = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'bold',
-    keyMap: ['Ctrl-B'],
-    tip: `${t('bold.text')} (Ctrl+B)`,
+    keyMap: ['Ctrl-b'],
+    tip: `${t('bold.text')} (Ctrl+b)`,
   };
   const DEFAULTTEXT = t('bold.text');
 
diff --git a/ui/src/components/Editor/ToolBars/code.tsx b/ui/src/components/Editor/ToolBars/code.tsx
index a3cdbffc..df52d163 100644
--- a/ui/src/components/Editor/ToolBars/code.tsx
+++ b/ui/src/components/Editor/ToolBars/code.tsx
@@ -156,8 +156,8 @@ const Code = () => {
 
   const item = {
     label: 'code',
-    keyMap: ['Ctrl-K'],
-    tip: `${t('code.text')} (Ctrl+K)`,
+    keyMap: ['Ctrl-k'],
+    tip: `${t('code.text')} (Ctrl+k)`,
   };
 
   const [code, setCode] = useState({
diff --git a/ui/src/components/Editor/ToolBars/formula.tsx b/ui/src/components/Editor/ToolBars/formula.tsx
deleted file mode 100644
index 9b273e87..00000000
--- a/ui/src/components/Editor/ToolBars/formula.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { FC, useState, memo } from 'react';
-import { Dropdown } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
-
-const Formula: FC<IEditorContext> = ({ editor, wrapText }) => {
-  const { t } = useTranslation('translation', { keyPrefix: 'editor' });
-  const formulaList = [
-    {
-      type: 'line',
-      label: t('formula.options.inline'),
-    },
-    {
-      type: 'block',
-      label: t('formula.options.block'),
-    },
-  ];
-  const item = {
-    label: 'formula',
-    tip: t('formula.text'),
-  };
-  const [isShow, setShowState] = useState(false);
-  const [isLocked, setLockState] = useState(false);
-
-  const handleClick = (type, label) => {
-    if (!editor) {
-      return;
-    }
-    if (type === 'line') {
-      wrapText('\\\\( ', ' \\\\)', label);
-    } else {
-      const cursor = editor.getCursor();
-
-      wrapText('\n$$\n', '\n$$\n', label);
-
-      editor.setSelection(
-        { line: cursor.line + 2, ch: 0 },
-        { line: cursor.line + 2, ch: label.length },
-      );
-    }
-    editor?.focus();
-    setShowState(false);
-  };
-  const onAddFormula = () => {
-    if (isLocked) {
-      return;
-    }
-    setShowState(!isShow);
-  };
-
-  const handleMouseEnter = () => {
-    setLockState(true);
-  };
-
-  const handleMouseLeave = () => {
-    setLockState(false);
-  };
-  return (
-    <ToolItem
-      as="dropdown"
-      {...item}
-      isShow={isShow}
-      onClick={onAddFormula}
-      onBlur={onAddFormula}>
-      <Dropdown.Menu
-        onMouseEnter={handleMouseEnter}
-        onMouseLeave={handleMouseLeave}>
-        {formulaList.map((formula) => {
-          return (
-            <Dropdown.Item
-              key={formula.label}
-              onClick={(e) => {
-                e.preventDefault();
-                handleClick(formula.type, formula.label);
-              }}>
-              {formula.label}
-            </Dropdown.Item>
-          );
-        })}
-      </Dropdown.Menu>
-    </ToolItem>
-  );
-};
-
-export default memo(Formula);
diff --git a/ui/src/components/Editor/ToolBars/heading.tsx b/ui/src/components/Editor/ToolBars/heading.tsx
index 9ae243ef..5c22b606 100644
--- a/ui/src/components/Editor/ToolBars/heading.tsx
+++ b/ui/src/components/Editor/ToolBars/heading.tsx
@@ -61,8 +61,8 @@ const Heading = () => {
   ];
   const item = {
     label: 'heading',
-    keyMap: ['Ctrl-H'],
-    tip: `${t('heading.text')} (Ctrl+H)`,
+    keyMap: ['Ctrl-h'],
+    tip: `${t('heading.text')} (Ctrl+h)`,
   };
   const [isShow, setShowState] = useState(false);
   const [isLocked, setLockState] = useState(false);
diff --git a/ui/src/components/Editor/ToolBars/hr.tsx b/ui/src/components/Editor/ToolBars/hr.tsx
index 76d08ea0..ac988a87 100644
--- a/ui/src/components/Editor/ToolBars/hr.tsx
+++ b/ui/src/components/Editor/ToolBars/hr.tsx
@@ -28,8 +28,8 @@ const Hr = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'hr',
-    keyMap: ['Ctrl-R'],
-    tip: `${t('hr.text')} (Ctrl+R)`,
+    keyMap: ['Ctrl-r'],
+    tip: `${t('hr.text')} (Ctrl+r)`,
   };
   const handleClick = (ctx) => {
     context = ctx;
diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx
index 1bc429a0..60099193 100644
--- a/ui/src/components/Editor/ToolBars/image.tsx
+++ b/ui/src/components/Editor/ToolBars/image.tsx
@@ -21,11 +21,11 @@ import { useEffect, useState, memo } from 'react';
 import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
-import type { Editor } from 'codemirror';
+import { EditorState, StateEffect } from '@codemirror/state';
 
 import { Modal as AnswerModal } from '@/components';
 import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
+import { IEditorContext, Editor } from '../types';
 import { uploadImage } from '@/services';
 
 let context: IEditorContext;
@@ -37,7 +37,7 @@ const Image = ({ editorInstance }) => {
 
   const item = {
     label: 'image',
-    keyMap: ['Ctrl-G'],
+    keyMap: ['Ctrl-g'],
     tip: `${t('image.text')} (Ctrl+G)`,
   };
   const [currentTab, setCurrentTab] = useState('localImage');
@@ -94,16 +94,16 @@ const Image = ({ editorInstance }) => {
 
     return Promise.all(promises);
   };
-  function dragenter(_, e) {
+  function dragenter(e) {
     e.stopPropagation();
     e.preventDefault();
   }
 
-  function dragover(_, e) {
+  function dragover(e) {
     e.stopPropagation();
     e.preventDefault();
   }
-  const drop = async (_, e) => {
+  const drop = async (e) => {
     const fileList = e.dataTransfer.files;
 
     const bool = verifyImageSize(fileList);
@@ -112,7 +112,9 @@ const Image = ({ editorInstance }) => {
       return;
     }
 
+    // const startPos = editor.getCursor(''); codemirror 6
     const startPos = editor.getCursor();
+
     const endPos = { ...startPos, ch: startPos.ch + loadingText.length };
 
     editor.replaceSelection(loadingText);
@@ -136,26 +138,30 @@ const Image = ({ editorInstance }) => {
     }
   };
 
-  const paste = async (_, event) => {
+  const paste = async (event) => {
     const clipboard = event.clipboardData;
 
     const bool = verifyImageSize(clipboard.files);
 
     if (bool) {
       event.preventDefault();
-      editor.setOption('readOnly', true);
-      const startPos = editor.getCursor('');
+      const startPos = editor.getCursor();
       const endPos = { ...startPos, ch: startPos.ch + loadingText.length };
 
       editor.replaceSelection(loadingText);
+      editor.dispatch({
+        effects: StateEffect.appendConfig.of([EditorState.readOnly.of(true)]),
+      });
       const urls = await upload(clipboard.files);
       const text = urls.map(({ name, url }) => {
         return `![${name}](${url})`;
       });
 
       editor.replaceRange(text.join('\n'), startPos, endPos);
+      editor.dispatch({
+        effects: StateEffect.appendConfig.of([EditorState.readOnly.of(false)]),
+      });
 
-      editor.setOption('readOnly', false);
       return;
     }
 
diff --git a/ui/src/components/Editor/ToolBars/index.ts b/ui/src/components/Editor/ToolBars/index.ts
index f3bc8209..ce04587d 100644
--- a/ui/src/components/Editor/ToolBars/index.ts
+++ b/ui/src/components/Editor/ToolBars/index.ts
@@ -18,7 +18,6 @@
  */
 
 import Table from './table';
-import Formula from './formula';
 import OL from './ol';
 import UL from './ul';
 import Indent from './indent';
@@ -36,7 +35,6 @@ import Chart from './chart';
 
 export {
   Table,
-  Formula,
   OL,
   UL,
   Indent,
diff --git a/ui/src/components/Editor/ToolBars/italic.tsx b/ui/src/components/Editor/ToolBars/italic.tsx
index 8237e974..f9b0e955 100644
--- a/ui/src/components/Editor/ToolBars/italic.tsx
+++ b/ui/src/components/Editor/ToolBars/italic.tsx
@@ -28,8 +28,8 @@ const Italic = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'italic',
-    keyMap: ['Ctrl-I'],
-    tip: `${t('italic.text')} (Ctrl+I)`,
+    keyMap: ['Ctrl-i'],
+    tip: `${t('italic.text')} (Ctrl+i)`,
   };
   const DEFAULTTEXT = t('italic.text');
 
diff --git a/ui/src/components/Editor/ToolBars/link.tsx b/ui/src/components/Editor/ToolBars/link.tsx
index 1fadcd63..f5b7c216 100644
--- a/ui/src/components/Editor/ToolBars/link.tsx
+++ b/ui/src/components/Editor/ToolBars/link.tsx
@@ -29,8 +29,8 @@ const Link = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'link',
-    keyMap: ['Ctrl-L'],
-    tip: `${t('link.text')} (Ctrl+L)`,
+    keyMap: ['Ctrl-l'],
+    tip: `${t('link.text')} (Ctrl+l)`,
   };
   const [visible, setVisible] = useState(false);
   const [link, setLink] = useState({
diff --git a/ui/src/components/Editor/ToolBars/ol.tsx b/ui/src/components/Editor/ToolBars/ol.tsx
index 468d2baf..76179cf6 100644
--- a/ui/src/components/Editor/ToolBars/ol.tsx
+++ b/ui/src/components/Editor/ToolBars/ol.tsx
@@ -28,8 +28,8 @@ const OL = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'orderedList',
-    keyMap: ['Ctrl-O'],
-    tip: `${t('ordered_list.text')} (Ctrl+O)`,
+    keyMap: ['Ctrl-o'],
+    tip: `${t('ordered_list.text')} (Ctrl+o)`,
   };
 
   const handleClick = (ctx) => {
diff --git a/ui/src/components/Editor/ToolBars/ul.tsx b/ui/src/components/Editor/ToolBars/ul.tsx
index 6c5d0b81..c0c5df52 100644
--- a/ui/src/components/Editor/ToolBars/ul.tsx
+++ b/ui/src/components/Editor/ToolBars/ul.tsx
@@ -28,8 +28,8 @@ const UL = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'editor' });
   const item = {
     label: 'unorderedList',
-    keyMap: ['Ctrl-U'],
-    tip: `${t('unordered_list.text')} (Ctrl+U)`,
+    keyMap: ['Ctrl-u'],
+    tip: `${t('unordered_list.text')} (Ctrl+u)`,
   };
 
   const handleClick = (ctx) => {
diff --git a/ui/src/components/Editor/types.ts b/ui/src/components/Editor/types.ts
index ebc7dbca..ee16d6b3 100644
--- a/ui/src/components/Editor/types.ts
+++ b/ui/src/components/Editor/types.ts
@@ -17,8 +17,54 @@
  * under the License.
  */
 
-import type { Editor } from 'codemirror';
+import { EditorView, Command } from '@codemirror/view';
 
+export interface Position {
+  ch: number;
+  line: number;
+  sticky?: string | undefined;
+}
+export interface ExtendEditor {
+  addKeyMap: (keyMap: Record<string, Command>) => void;
+  on: (
+    event:
+      | 'change'
+      | 'focus'
+      | 'blur'
+      | 'dragenter'
+      | 'dragover'
+      | 'drop'
+      | 'paste',
+    callback: (e?) => void,
+  ) => void;
+  getValue: () => string;
+  setValue: (value: string) => void;
+  off: (
+    event:
+      | 'change'
+      | 'focus'
+      | 'blur'
+      | 'dragenter'
+      | 'dragover'
+      | 'drop'
+      | 'paste',
+    callback: (e?) => void,
+  ) => void;
+  getSelection: () => string;
+  replaceSelection: (value: string) => void;
+  focus: () => void;
+  wrapText: (before: string, after?: string, defaultText?: string) => void;
+  replaceLines: (
+    replace: Parameters<Array<string>['map']>[0],
+    symbolLen?: number,
+  ) => void;
+  appendBlock: (content: string) => void;
+  getCursor: () => Position;
+  replaceRange: (value: string, from: Position, to: Position) => void;
+  setSelection: (anchor: Position, head?: Position) => void;
+}
+
+export type Editor = EditorView & ExtendEditor;
 export interface CodeMirrorEditor extends Editor {
   display: any;
 
diff --git a/ui/src/components/Editor/utils/extension.ts b/ui/src/components/Editor/utils/extension.ts
new file mode 100644
index 00000000..4118fed4
--- /dev/null
+++ b/ui/src/components/Editor/utils/extension.ts
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EditorSelection, StateEffect } from '@codemirror/state';
+import { EditorView, keymap, KeyBinding } from '@codemirror/view';
+
+import { Editor, Position } from '../types';
+
+const createEditorUtils = (editor: Editor) => {
+  editor.focus = () => {
+    editor.contentDOM.focus();
+  };
+
+  editor.getCursor = () => {
+    const range = editor.state.selection.ranges[0];
+    const line = editor.state.doc.lineAt(range.from).number;
+    const { from, to } = editor.state.doc.line(line);
+    return { from, to, ch: range.from - from, line };
+  };
+
+  editor.addKeyMap = (keyMap) => {
+    const array = Object.entries(keyMap).map(([key, value]) => {
+      const keyBinding: KeyBinding = {
+        key,
+        preventDefault: true,
+        run: value,
+      };
+      return keyBinding;
+    });
+
+    editor.dispatch({
+      effects: StateEffect.appendConfig.of(keymap.of(array)),
+    });
+  };
+
+  editor.getSelection = () => {
+    return editor.state.sliceDoc(
+      editor.state.selection.main.from,
+      editor.state.selection.main.to,
+    );
+  };
+
+  editor.replaceSelection = (value: string) => {
+    editor.dispatch({
+      changes: [
+        {
+          from: editor.state.selection.main.from,
+          to: editor.state.selection.main.to,
+          insert: value,
+        },
+      ],
+      selection: EditorSelection.cursor(
+        editor.state.selection.main.from + value.length,
+      ),
+    });
+  };
+
+  editor.setSelection = (anchor: Position, head?: Position) => {
+    editor.dispatch({
+      selection: EditorSelection.create([
+        EditorSelection.range(
+          editor.state.doc.line(anchor.line).from + anchor.ch,
+          head
+            ? editor.state.doc.line(head.line).from + head.ch
+            : editor.state.doc.line(anchor.line).from + anchor.ch,
+        ),
+      ]),
+    });
+  };
+
+  editor.on = (event, callback) => {
+    if (event === 'change') {
+      const change = EditorView.updateListener.of((update) => {
+        if (update.docChanged) {
+          callback();
+        }
+      });
+
+      editor.dispatch({
+        effects: StateEffect.appendConfig.of(change),
+      });
+    }
+    if (event === 'focus') {
+      editor.contentDOM.addEventListener('focus', callback);
+    }
+    if (event === 'blur') {
+      editor.contentDOM.addEventListener('blur', callback);
+    }
+
+    if (event === 'dragenter') {
+      editor.contentDOM.addEventListener('dragenter', callback);
+    }
+
+    if (event === 'dragover') {
+      editor.contentDOM.addEventListener('dragover', callback);
+    }
+
+    if (event === 'drop') {
+      editor.contentDOM.addEventListener('drop', callback);
+    }
+
+    if (event === 'paste') {
+      editor.contentDOM.addEventListener('paste', callback);
+    }
+  };
+
+  editor.off = (event, callback) => {
+    if (event === 'focus') {
+      editor.contentDOM.removeEventListener('focus', callback);
+    }
+
+    if (event === 'blur') {
+      editor.contentDOM.removeEventListener('blur', callback);
+    }
+
+    if (event === 'dragenter') {
+      editor.contentDOM.removeEventListener('dragenter', callback);
+    }
+
+    if (event === 'dragover') {
+      editor.contentDOM.removeEventListener('dragover', callback);
+    }
+
+    if (event === 'drop') {
+      editor.contentDOM.removeEventListener('drop', callback);
+    }
+
+    if (event === 'paste') {
+      editor.contentDOM.removeEventListener('paste', callback);
+    }
+  };
+
+  editor.getValue = () => {
+    return editor.state.doc.toString();
+  };
+
+  editor.setValue = (value: string) => {
+    editor.dispatch({
+      changes: { from: 0, to: editor.state.doc.length, insert: value },
+    });
+  };
+
+  editor.wrapText = (before: string, after = before, defaultText) => {
+    const range = editor.state.selection.ranges[0];
+    const selection = editor.state.sliceDoc(range.from, range.to);
+    const text = `${before}${selection || defaultText}${after}`;
+
+    editor.dispatch({
+      changes: [
+        {
+          from: range.from,
+          to: range.to,
+          insert: text,
+        },
+      ],
+      selection: EditorSelection.range(
+        range.from + before.length,
+        range.to + before.length,
+      ),
+    });
+  };
+
+  editor.replaceLines = (
+    replace: Parameters<Array<string>['map']>[0],
+    symbolLen = 0,
+  ) => {
+    const range = editor.state.selection.ranges[0];
+    const line = editor.state.doc.lineAt(range.from).number;
+    const { from, to } = editor.state.doc.line(line);
+    const lines = editor.state.sliceDoc(from, to).split('\n');
+
+    const insert = lines.map(replace).join('\n');
+    const selectionStart = from;
+    const selectionEnd = from + insert.length;
+
+    editor.dispatch({
+      changes: [
+        {
+          from,
+          to,
+          insert,
+        },
+      ],
+      selection: EditorSelection.create([
+        EditorSelection.range(selectionStart + symbolLen, selectionEnd),
+      ]),
+    });
+  };
+
+  editor.appendBlock = (content: string) => {
+    const range = editor.state.selection.ranges[0];
+    const line = editor.state.doc.lineAt(range.from).number;
+    const { from, to } = editor.state.doc.line(line);
+
+    let insert = `\n\n${content}`;
+
+    let selection = EditorSelection.single(to, to + content.length);
+    if (from === to) {
+      insert = `${content}\n`;
+      selection = EditorSelection.create([
+        EditorSelection.cursor(to + content.length),
+      ]);
+    }
+
+    editor.dispatch({
+      changes: [
+        {
+          from: to,
+          insert,
+        },
+      ],
+      selection,
+    });
+  };
+
+  editor.replaceRange = (
+    value: string,
+    selectionStart: Position,
+    selectionEnd: Position,
+  ) => {
+    const from =
+      editor.state.doc.line(selectionStart.line).from + selectionStart.ch;
+    const to = editor.state.doc.line(selectionEnd.line).from + selectionEnd.ch;
+    editor.dispatch({
+      changes: [
+        {
+          from,
+          to,
+          insert: value,
+        },
+      ],
+      selection: EditorSelection.cursor(from + value.length),
+    });
+  };
+
+  return editor;
+};
+
+export default createEditorUtils;
diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts
index 4930c768..d380620b 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -17,86 +17,17 @@
  * under the License.
  */
 
-import { useEffect, useState, useRef } from 'react';
-
-import type { Editor, Position } from 'codemirror';
-import type CodeMirror from 'codemirror';
-import 'codemirror/lib/codemirror.css';
-
-export function createEditorUtils(
-  codemirror: typeof CodeMirror,
-  editor: Editor,
-) {
-  editor.wrapText = (before: string, after = before, defaultText) => {
-    const range = editor.somethingSelected()
-      ? editor.listSelections()[0]
-      : editor.findWordAt(editor.getCursor());
-
-    const from = range.from();
-    const to = range.to();
-    const text = editor.getRange(from, to) || defaultText;
-    const fromBefore = codemirror.Pos(from.line, from.ch - before.length);
-    const toAfter = codemirror.Pos(to.line, to.ch + after.length);
+import { useEffect, useState } from 'react';
 
-    if (
-      editor.getRange(fromBefore, from) === before &&
-      editor.getRange(to, toAfter) === after
-    ) {
-      editor.replaceRange(text, fromBefore, toAfter);
-      editor.setSelection(
-        fromBefore,
-        codemirror.Pos(fromBefore.line, fromBefore.ch + text.length),
-      );
-    } else {
-      editor.replaceRange(before + text + after, from, to);
-      const cursor = editor.getCursor();
-
-      editor.setSelection(
-        codemirror.Pos(cursor.line, cursor.ch - after.length - text.length),
-        codemirror.Pos(cursor.line, cursor.ch - after.length),
-      );
-    }
-  };
-  editor.replaceLines = (
-    replace: Parameters<Array<string>['map']>[0],
-    symbolLen = 0,
-  ) => {
-    const [selection] = editor.listSelections();
-
-    const range = [
-      codemirror.Pos(selection.from().line, 0),
-      codemirror.Pos(selection.to().line),
-    ] as const;
-    const lines = editor.getRange(...range).split('\n');
-
-    editor.replaceRange(lines.map(replace).join('\n'), ...range);
-    const newRange = range;
-
-    if (symbolLen > 0) {
-      newRange[0].ch = symbolLen;
-    }
-    editor.setSelection(...newRange);
-  };
-  editor.appendBlock = (content: string): Position => {
-    const cursor = editor.getCursor();
+import { minimalSetup } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+import { EditorView, placeholder } from '@codemirror/view';
+import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
+import { languages } from '@codemirror/language-data';
 
-    let emptyLine = -1;
+import { Editor } from '../types';
 
-    for (let i = cursor.line; i < editor.lineCount(); i += 1) {
-      if (!editor.getLine(i).trim()) {
-        emptyLine = i;
-        break;
-      }
-    }
-    if (emptyLine === -1) {
-      editor.replaceRange('\n', codemirror.Pos(editor.lineCount()));
-      emptyLine = editor.lineCount();
-    }
-
-    editor.replaceRange(`\n${content}`, codemirror.Pos(emptyLine));
-    return codemirror.Pos(emptyLine + 1, 0);
-  };
-}
+import createEditorUtils from './extension';
 
 export function htmlRender(el: HTMLElement | null) {
   if (!el) return;
@@ -127,7 +58,7 @@ export function htmlRender(el: HTMLElement | null) {
     div.appendChild(table);
   });
 
-  // add rel nofollow for link not inlcludes domain
+  // add rel nofollow for link not includes domain
   el.querySelectorAll('a').forEach((a) => {
     const base = window.location.origin;
     const targetUrl = new URL(a.href, base);
@@ -140,85 +71,77 @@ export function htmlRender(el: HTMLElement | null) {
 
 export const useEditor = ({
   editorRef,
-  placeholder,
+  placeholder: placeholderText,
   autoFocus,
   onChange,
   onFocus,
   onBlur,
 }) => {
-  const [editor, setEditor] = useState<CodeMirror.Editor | null>(null);
+  const [editor, setEditor] = useState<Editor | null>(null);
   const [value, setValue] = useState<string>('');
-  const isMountedRef = useRef(false);
-
-  const onEnter = (cm) => {
-    const cursor = cm.getCursor();
-    const text = cm.getLine(cursor.line);
-    const doc = cm.getDoc();
-
-    const olRegexData = text.match(/^(\s{0,})(\d+)\.\s/);
-    const ulRegexData = text.match(/^(\s{0,})(-|\*)\s/);
-    const blockquoteData = text.match(/^>\s+?/g);
-
-    if (olRegexData && text !== olRegexData[0]) {
-      const num = olRegexData[2];
-
-      doc.replaceSelection(`\n${olRegexData[1]}${Number(num) + 1}. `);
-    } else if (ulRegexData && text !== ulRegexData[0]) {
-      doc.replaceSelection(`\n${ulRegexData[1]}${ulRegexData[2]} `);
-    } else if (blockquoteData && text !== blockquoteData[0]) {
-      doc.replaceSelection(`\n> `);
-    } else if (
-      text.trim() === '>' ||
-      text.trim().match(/^\d{1,}\.$/) ||
-      text.trim().match(/^(\*|-)$/)
-    ) {
-      doc.replaceRange(`\n`, { ...cursor, ch: 0 }, cursor);
-    } else {
-      doc.replaceSelection(`\n`);
-    }
-  };
-
   const init = async () => {
-    if (isMountedRef.current) {
-      return false;
-    }
-    isMountedRef.current = true;
-
-    const { default: codeMirror } = await import('codemirror');
-    await import('codemirror/mode/markdown/markdown');
-    await import('codemirror/addon/display/placeholder');
+    const theme = EditorView.theme(
+      {
+        '&': {
+          height: '100%',
+        },
+        '&.cm-focused': {
+          outline: 'none',
+        },
+        '.cm-content': {
+          width: '100%',
+          padding: '1rem',
+        },
+        '.cm-line': {
+          whiteSpace: 'pre-wrap',
+          wordWrap: 'break-word',
+          wordBreak: 'break-all',
+        },
+      },
+      { dark: false },
+    );
+
+    const startState = EditorState.create({
+      extensions: [
+        minimalSetup,
+        markdown({
+          codeLanguages: languages,
+          base: markdownLanguage,
+        }),
+        theme,
+        placeholder(placeholderText),
+        EditorView.lineWrapping,
+      ],
+    });
 
-    const cm = codeMirror(editorRef?.current, {
-      mode: 'markdown',
-      lineWrapping: true,
-      placeholder,
+    const view = new EditorView({
+      parent: editorRef.current,
+      state: startState,
     });
 
+    const cm = createEditorUtils(view as Editor);
+
     if (autoFocus) {
       setTimeout(() => {
         cm.focus();
       }, 10);
     }
 
-    cm.on('change', (e) => {
-      const newValue = e.getValue();
+    cm.on('change', () => {
+      const newValue = cm.getValue();
       setValue(newValue);
     });
 
     cm.on('focus', () => {
       onFocus?.();
     });
+
     cm.on('blur', () => {
       onBlur?.();
     });
 
     setEditor(cm);
-    createEditorUtils(codeMirror, cm);
 
-    cm.setSize('100%', '100%');
-    cm.addKeyMap({
-      Enter: onEnter,
-    });
     return cm;
   };
 
@@ -227,11 +150,10 @@ export const useEditor = ({
   }, [value]);
 
   useEffect(() => {
-    if (!(editorRef.current instanceof HTMLElement)) {
+    if (!(editorRef.current instanceof HTMLElement) || editor) {
       return;
     }
     init();
-  }, [editorRef]);
-
+  }, [editor]);
   return editor;
 };
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/components/Mentions/index.scss
similarity index 88%
copy from ui/src/pages/Admin/index.scss
copy to ui/src/components/Mentions/index.scss
index c723b8ac..69c73f88 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/components/Mentions/index.scss
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-.admin-container {
-  padding-top: 1.5rem;
-  padding-bottom: 1.5rem;
+.bg-gray-200 {
+  color: var(--bs-body-color);
+  background-color: var(--an-invite-answer-item-active);
 }
diff --git a/ui/src/components/Mentions/index.tsx b/ui/src/components/Mentions/index.tsx
index 9b61f0bf..ae5ea2ff 100644
--- a/ui/src/components/Mentions/index.tsx
+++ b/ui/src/components/Mentions/index.tsx
@@ -20,8 +20,11 @@
 import React, { useEffect, useRef, useState, FC } from 'react';
 import { Dropdown } from 'react-bootstrap';
 
+import { useSearchUserStaff } from '@/services';
 import * as Types from '@/common/interface';
 
+import './index.scss';
+
 interface IProps {
   children: React.ReactNode;
   pageUsers;
@@ -37,6 +40,17 @@ const Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {
   const [users, setUsers] = useState<Types.PageUser[]>([]);
   const [cursor, setCursor] = useState(0);
   const [isRequested, setRequestedState] = useState(false);
+  const { data: staffUserList = [] } = useSearchUserStaff(val);
+  const mapStaffUsers =
+    staffUserList
+      ?.map((item) => ({
+        displayName: item.display_name,
+        userName: item.username,
+      }))
+      ?.filter(
+        (item) =>
+          users.findIndex((user) => user.userName === item.userName) < 0,
+      ) || [];
 
   const searchUser = () => {
     const element = dropdownRef.current?.children[0];
@@ -57,6 +71,7 @@ const Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {
     if (str.substring(str.lastIndexOf(' '), selectionStart).indexOf('@') < 0) {
       return;
     }
+
     setValue(str.substring(1));
 
     if (!str.substring(1)) {
@@ -103,7 +118,7 @@ const Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {
     setValue('');
   };
   const filterData = val
-    ? users.filter(
+    ? [...users, ...mapStaffUsers].filter(
         (item) =>
           item.displayName?.indexOf(val) === 0 ||
           item.userName?.indexOf(val) === 0,
diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx
index e04416f2..47b4463f 100644
--- a/ui/src/components/Operate/index.tsx
+++ b/ui/src/components/Operate/index.tsx
@@ -23,7 +23,8 @@ import { Link, useNavigate } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
 import { Modal } from '@/components';
-import { useReportModal, useToast, useCaptchaModal } from '@/hooks';
+import { useReportModal, useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { QuestionOperationReq } from '@/common/interface';
 import Share from '../Share';
 import {
@@ -63,7 +64,7 @@ const Index: FC<IProps> = ({
   const toast = useToast();
   const navigate = useNavigate();
   const reportModal = useReportModal();
-  const dCaptcha = useCaptchaModal('delete');
+  const dCaptcha = useCaptchaPlugin('delete');
 
   const refreshQuestion = () => {
     callback?.('default');
@@ -88,6 +89,55 @@ const Index: FC<IProps> = ({
     });
   };
 
+  const submitDeleteQuestion = () => {
+    const req = {
+      id: qid,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    dCaptcha?.resolveCaptchaReq(req);
+
+    deleteQuestion(req)
+      .then(async () => {
+        await dCaptcha?.close();
+        toast.onShow({
+          msg: t('post_deleted', { keyPrefix: 'messages' }),
+          variant: 'success',
+        });
+        callback?.('delete_question');
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
+
+  const submitDeleteAnswer = () => {
+    const req = {
+      id: aid,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    dCaptcha?.resolveCaptchaReq(req);
+
+    deleteAnswer(req)
+      .then(async () => {
+        await dCaptcha?.close();
+        // refresh page
+        toast.onShow({
+          msg: t('tip_answer_deleted'),
+          variant: 'success',
+        });
+        callback?.('delete_answer');
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
+
   const handleDelete = () => {
     if (type === 'question') {
       Modal.confirm({
@@ -97,28 +147,12 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
+          if (!dCaptcha) {
+            submitDeleteQuestion();
+            return;
+          }
           dCaptcha.check(() => {
-            const req = {
-              id: qid,
-              captcha_code: undefined,
-              captcha_id: undefined,
-            };
-            dCaptcha.resolveCaptchaReq(req);
-
-            deleteQuestion(req)
-              .then(async () => {
-                await dCaptcha.close();
-                toast.onShow({
-                  msg: t('post_deleted', { keyPrefix: 'messages' }),
-                  variant: 'success',
-                });
-                callback?.('delete_question');
-              })
-              .catch((ex) => {
-                if (ex.isError) {
-                  dCaptcha.handleCaptchaError(ex.list);
-                }
-              });
+            submitDeleteQuestion();
           });
         },
       });
@@ -132,29 +166,12 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
+          if (!dCaptcha) {
+            submitDeleteAnswer();
+            return;
+          }
           dCaptcha.check(() => {
-            const req = {
-              id: aid,
-              captcha_code: undefined,
-              captcha_id: undefined,
-            };
-            dCaptcha.resolveCaptchaReq(req);
-
-            deleteAnswer(req)
-              .then(async () => {
-                await dCaptcha.close();
-                // refresh page
-                toast.onShow({
-                  msg: t('tip_answer_deleted'),
-                  variant: 'success',
-                });
-                callback?.('delete_answer');
-              })
-              .catch((ex) => {
-                if (ex.isError) {
-                  dCaptcha.handleCaptchaError(ex.list);
-                }
-              });
+            submitDeleteAnswer();
           });
         },
       });
diff --git a/ui/src/components/PageTags/index.tsx b/ui/src/components/PageTags/index.tsx
index 6bed2125..86d9a979 100644
--- a/ui/src/components/PageTags/index.tsx
+++ b/ui/src/components/PageTags/index.tsx
@@ -32,6 +32,7 @@ const Index: FC = () => {
   );
   const appVersion = siteInfoStore((_) => _.version);
   const hashVersion = siteInfoStore((_) => _.revision);
+  const siteName = siteInfoStore((_) => _.siteInfo).name;
   const setAppGenerator = () => {
     if (!appVersion) {
       return;
@@ -61,6 +62,11 @@ const Index: FC = () => {
       );
     }
   };
+  // properties used for social media tags
+  const openGraphType = 'website';
+  const twitterType = 'summary';
+  const { href } = window.location;
+  const { hostname } = new URL(href);
 
   useEffect(() => {
     setDocLang();
@@ -86,6 +92,25 @@ const Index: FC = () => {
       {doInsertCustomCSS && (
         <link rel="stylesheet" href={`${process.env.PUBLIC_URL}/custom.css`} />
       )}
+      {/* Social media meta share tags start here */}
+      <meta property="og:type" content={openGraphType} />
+      <meta property="og:title" name="twitter:title" content={pageTitle} />
+      <meta property="og:site_name" content={siteName} />
+      <meta property="og:url" content={href} />
+      {description && <meta property="og:description" content={description} />}
+      <meta
+        property="og:image"
+        itemProp="image primaryImageOfPage"
+        content={square_icon || favicon || '/favicon.ico'}
+      />
+      <meta name="twitter:card" content={twitterType} />
+      <meta name="twitter:domain" content={hostname} />
+      {description && <meta name="twitter:description" content={description} />}
+      <meta
+        name="twitter:image"
+        content={square_icon || favicon || '/favicon.ico'}
+      />
+      {/* Social media meta share tags end here */}
     </Helmet>
   );
 };
diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx
index d0afb692..51fc4655 100644
--- a/ui/src/components/SchemaForm/index.tsx
+++ b/ui/src/components/SchemaForm/index.tsx
@@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next';
 import isEmpty from 'lodash/isEmpty';
 import classnames from 'classnames';
 
+import { scrollToElementTop } from '@/utils';
 import type * as Type from '@/common/interface';
 
 import type {
@@ -201,6 +202,7 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
       if (onChange instanceof Function) {
         onChange({ ...formData });
       }
+      scrollToElementTop(document.getElementById(errors[0]));
       return false;
     }
     const syncErrors = await syncValidator();
@@ -216,6 +218,7 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
       if (onChange instanceof Function) {
         onChange({ ...formData });
       }
+      scrollToElementTop(document.getElementById(syncErrors[0].key));
       return false;
     }
     return true;
diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss
index d8bcc8f1..1fb0e7b1 100644
--- a/ui/src/components/SideNav/index.scss
+++ b/ui/src/components/SideNav/index.scss
@@ -33,6 +33,9 @@
   .nav-link {
     color: var(--an-side-nav-link);
   }
+  .nav-link:focus-visible {
+    box-shadow: none;
+  }
   .nav-link:hover {
     color: var(--an-side-nav-link-hover-color);
     background-color: var(--bs-gray-100);
@@ -48,7 +51,6 @@
     width: 1px;
     height: 100%;
     background-color: var(--bs-border-color);
-    min-height: calc(100vh - 62px - 74px);
   }
 }
 
diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx
index e96e2875..163da2bd 100644
--- a/ui/src/components/Unactivate/index.tsx
+++ b/ui/src/components/Unactivate/index.tsx
@@ -26,7 +26,7 @@ import type { ImgCodeReq, FormDataType } from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
 import { resendEmail } from '@/services';
 import { handleFormError } from '@/utils';
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 interface IProps {
   visible?: boolean;
@@ -44,12 +44,12 @@ const Index: React.FC<IProps> = () => {
     },
   });
 
-  const emailCaptcha = useCaptchaModal('email');
+  const emailCaptcha = useCaptchaPlugin('email');
 
   const submit = () => {
     let req: ImgCodeReq = {};
-    const imgCode = emailCaptcha.getCaptcha();
-    if (imgCode.verify) {
+    const imgCode = emailCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
       req = {
         captcha_code: imgCode.captcha_code,
         captcha_id: imgCode.captcha_id,
@@ -57,12 +57,12 @@ const Index: React.FC<IProps> = () => {
     }
     resendEmail(req)
       .then(async () => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         setSuccess(true);
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -71,6 +71,10 @@ const Index: React.FC<IProps> = () => {
 
   const onSentEmail = (evt) => {
     evt.preventDefault();
+    if (!emailCaptcha) {
+      submit();
+      return;
+    }
     emailCaptcha.check(() => {
       submit();
     });
diff --git a/ui/src/hooks/usePrompt/index.tsx b/ui/src/hooks/usePrompt/index.tsx
index 7655adce..095dbd53 100644
--- a/ui/src/hooks/usePrompt/index.tsx
+++ b/ui/src/hooks/usePrompt/index.tsx
@@ -36,6 +36,7 @@ const usePromptWithUnload = ({
   beforeUnload = true,
 }: PromptProps) => {
   const { t } = useTranslation('translation', { keyPrefix: 'prompt' });
+
   usePrompt({
     when,
     message: `${t('leave_page')} ${t('changes_not_save')}`,
diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx
index e20b8eb6..843472cd 100644
--- a/ui/src/hooks/useReportModal/index.tsx
+++ b/ui/src/hooks/useReportModal/index.tsx
@@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next';
 
 import ReactDOM from 'react-dom/client';
 
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type * as Type from '@/common/interface';
 import {
   reportList,
@@ -65,7 +66,7 @@ const useReportModal = (callback?: () => void) => {
   const [show, setShow] = useState(false);
   const [list, setList] = useState<any[]>([]);
 
-  const rCaptcha = useCaptchaModal('report');
+  const rCaptcha = useCaptchaPlugin('report');
 
   useEffect(() => {
     const div = document.createElement('div');
@@ -168,6 +169,34 @@ const useReportModal = (callback?: () => void) => {
     return true;
   };
 
+  const submitReport = (data) => {
+    const flagReq = {
+      source: data.type,
+      report_type: reportType.type,
+      object_id: data.id,
+      content: content.value,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    rCaptcha?.resolveCaptchaReq(flagReq);
+
+    postReport(flagReq)
+      .then(async () => {
+        await rCaptcha?.close();
+        toast.onShow({
+          msg: t('flag_success', { keyPrefix: 'toast' }),
+          variant: 'warning',
+        });
+        onClose();
+        asyncCallback();
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          rCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
+
   const handleSubmit = () => {
     if (!params) {
       return;
@@ -205,32 +234,12 @@ const useReportModal = (callback?: () => void) => {
       return;
     }
     if (!params.isBackend && params.action === 'flag') {
+      if (!rCaptcha) {
+        submitReport(params);
+        return;
+      }
       rCaptcha.check(() => {
-        const flagReq = {
-          source: params.type,
-          report_type: reportType.type,
-          object_id: params.id,
-          content: content.value,
-          captcha_code: undefined,
-          captcha_id: undefined,
-        };
-        rCaptcha.resolveCaptchaReq(flagReq);
-
-        postReport(flagReq)
-          .then(async () => {
-            await rCaptcha.close();
-            toast.onShow({
-              msg: t('flag_success', { keyPrefix: 'toast' }),
-              variant: 'warning',
-            });
-            onClose();
-            asyncCallback();
-          })
-          .catch((ex) => {
-            if (ex.isError) {
-              rCaptcha.handleCaptchaError(ex.list);
-            }
-          });
+        submitReport(params);
       });
     }
 
diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts
index 03f775cb..86188900 100644
--- a/ui/src/i18n/init.ts
+++ b/ui/src/i18n/init.ts
@@ -21,7 +21,7 @@ import { initReactI18next } from 'react-i18next';
 
 import i18next from 'i18next';
 import en_US from '@i18n/en_US.yaml';
-import zh_CN from '@i18n/zh_CN.yaml';
+// import zh_CN from '@i18n/zh_CN.yaml';
 
 import { DEFAULT_LANG, LANG_RESOURCE_STORAGE_KEY } from '@/common/constants';
 import Storage from '@/utils/storage';
@@ -34,9 +34,9 @@ const initResources = {
   en_US: {
     translation: en_US.ui,
   },
-  zh_CN: {
-    translation: zh_CN.ui,
-  },
+  // zh_CN: {
+  //   translation: zh_CN.ui,
+  // },
 };
 
 const storageLang = Storage.get(LANG_RESOURCE_STORAGE_KEY);
@@ -44,8 +44,7 @@ if (
   storageLang &&
   storageLang.resources &&
   storageLang.lng &&
-  storageLang.lng !== 'en_US' &&
-  storageLang.lng !== 'zh_CN'
+  storageLang.lng !== 'en_US'
 ) {
   initResources[storageLang.lng] = {
     translation: storageLang.resources,
diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx
index 091fe2db..42339263 100644
--- a/ui/src/pages/Admin/Answers/index.tsx
+++ b/ui/src/pages/Admin/Answers/index.tsx
@@ -76,7 +76,7 @@ const Answers: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <div className="d-flex justify-content-between align-items-center mb-3">
+      <div className="d-flex flex-wrap justify-content-between align-items-center mb-3">
         <QueryGroup
           data={answerFilterItems}
           currentSort={curFilter}
@@ -91,12 +91,13 @@ const Answers: FC = () => {
           type="search"
           placeholder={t('filter.placeholder')}
           style={{ width: '12.25rem' }}
+          className="mt-3 mt-sm-0"
         />
       </div>
       <Table responsive>
         <thead>
           <tr>
-            <th>{t('post')}</th>
+            <th className="min-w-15">{t('post')}</th>
             <th style={{ width: '11%' }}>{t('votes')}</th>
             <th style={{ width: '14%' }}>{t('created')}</th>
             <th style={{ width: '11%' }}>{t('status')}</th>
@@ -127,9 +128,7 @@ const Answers: FC = () => {
                       className="ms-2 text-success"
                     />
                   )}
-                  <div
-                    className="text-truncate-2 small"
-                    style={{ maxWidth: '30rem' }}>
+                  <div className="text-truncate-2 small max-w-30">
                     {escapeRemove(li.description)}
                   </div>
                 </td>
diff --git a/ui/src/pages/Admin/Branding/index.tsx b/ui/src/pages/Admin/Branding/index.tsx
index 74c3d40e..bec7fc0f 100644
--- a/ui/src/pages/Admin/Branding/index.tsx
+++ b/ui/src/pages/Admin/Branding/index.tsx
@@ -25,7 +25,7 @@ import { FormDataType } from '@/common/interface';
 import { brandSetting, getBrandSetting } from '@/services';
 import { brandingStore } from '@/stores';
 import { useToast } from '@/hooks';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 
 const uploadType = 'branding';
 const Index: FC = () => {
@@ -145,6 +145,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/CssAndHtml/index.tsx b/ui/src/pages/Admin/CssAndHtml/index.tsx
index ae64a697..de376ad7 100644
--- a/ui/src/pages/Admin/CssAndHtml/index.tsx
+++ b/ui/src/pages/Admin/CssAndHtml/index.tsx
@@ -24,7 +24,7 @@ import type * as Type from '@/common/interface';
 import { getPageCustom, putPageCustom } from '@/services';
 import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { customizeStore } from '@/stores';
 
 const Index: FC = () => {
@@ -124,6 +124,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx
index 3c6ff51a..6a1b080f 100644
--- a/ui/src/pages/Admin/General/index.tsx
+++ b/ui/src/pages/Admin/General/index.tsx
@@ -26,7 +26,7 @@ import { useToast } from '@/hooks';
 import { siteInfoStore } from '@/stores';
 import { useGeneralSetting, updateGeneralSetting } from '@/services';
 import Pattern from '@/common/pattern';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 
 const General: FC = () => {
   const { t } = useTranslation('translation', {
@@ -157,6 +157,8 @@ const General: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx
index f4808f97..9c622048 100644
--- a/ui/src/pages/Admin/Interface/index.tsx
+++ b/ui/src/pages/Admin/Interface/index.tsx
@@ -39,7 +39,7 @@ import {
   loadLanguageOptions,
   setupAppTimeZone,
 } from '@/utils/localize';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 
 const Interface: FC = () => {
   const { t } = useTranslation('translation', {
@@ -141,6 +141,8 @@ const Interface: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Legal/index.tsx b/ui/src/pages/Admin/Legal/index.tsx
index d0dd2e7a..83561566 100644
--- a/ui/src/pages/Admin/Legal/index.tsx
+++ b/ui/src/pages/Admin/Legal/index.tsx
@@ -26,7 +26,7 @@ import type * as Type from '@/common/interface';
 import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
 import { getLegalSetting, putLegalSetting } from '@/services';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 
 const Legal: FC = () => {
   const { t } = useTranslation('translation', {
@@ -90,6 +90,8 @@ const Legal: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Login/index.tsx b/ui/src/pages/Admin/Login/index.tsx
index c58a293b..c43cfcff 100644
--- a/ui/src/pages/Admin/Login/index.tsx
+++ b/ui/src/pages/Admin/Login/index.tsx
@@ -24,7 +24,7 @@ import type * as Type from '@/common/interface';
 import { getLoginSetting, putLoginSetting } from '@/services';
 import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { loginSettingStore } from '@/stores';
 
 const Index: FC = () => {
@@ -132,6 +132,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Plugins/Installed/index.tsx b/ui/src/pages/Admin/Plugins/Installed/index.tsx
index 6df6d716..f1ced442 100644
--- a/ui/src/pages/Admin/Plugins/Installed/index.tsx
+++ b/ui/src/pages/Admin/Plugins/Installed/index.tsx
@@ -103,10 +103,10 @@ const Users: FC = () => {
           />
         </Stack>
       </div>
-      <Table>
+      <Table responsive>
         <thead>
           <tr>
-            <th>{t('name')}</th>
+            <th className="min-w-15">{t('name')}</th>
             <th style={{ width: '17%' }}>{t('version')}</th>
             <th style={{ width: '11%' }}>{t('status')}</th>
             {curFilter !== 'deleted' ? (
diff --git a/ui/src/pages/Admin/Privileges/index.tsx b/ui/src/pages/Admin/Privileges/index.tsx
index dbe58f0c..5276912a 100644
--- a/ui/src/pages/Admin/Privileges/index.tsx
+++ b/ui/src/pages/Admin/Privileges/index.tsx
@@ -29,7 +29,7 @@ import {
   AdminSettingsPrivilege,
   AdminSettingsPrivilegeReq,
 } from '@/services';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { ADMIN_PRIVILEGE_CUSTOM_LEVEL } from '@/common/constants';
 
 const Index: FC = () => {
@@ -83,10 +83,10 @@ const Index: FC = () => {
           validator: (value: string) => {
             const val = Number(value);
             if (Number.isNaN(val)) {
-              return 'the input should be number';
+              return t('msg.should_be_number');
             }
             if (val < 1) {
-              return 'number should be equal or larger than 1';
+              return t('msg.number_larger_1');
             }
             return true;
           },
@@ -148,6 +148,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx
index 716120d3..4a3c669a 100644
--- a/ui/src/pages/Admin/Questions/index.tsx
+++ b/ui/src/pages/Admin/Questions/index.tsx
@@ -74,7 +74,7 @@ const Questions: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <div className="d-flex justify-content-between align-items-center mb-3">
+      <div className="d-flex flex-wrap justify-content-between align-items-center mb-3">
         <QueryGroup
           data={questionFilterItems}
           currentSort={curFilter}
@@ -89,12 +89,13 @@ const Questions: FC = () => {
           placeholder={t('filter.placeholder')}
           onChange={handleFilter}
           style={{ width: '12.25rem' }}
+          className="mt-3 mt-sm-0"
         />
       </div>
-      <Table>
+      <Table responsive>
         <thead>
           <tr>
-            <th>{t('post')}</th>
+            <th className="min-w-15">{t('post')}</th>
             <th style={{ width: '8%' }}>{t('votes')}</th>
             <th style={{ width: '8%' }}>{t('answers')}</th>
             <th style={{ width: '15%' }}>{t('created')}</th>
diff --git a/ui/src/pages/Admin/Seo/index.tsx b/ui/src/pages/Admin/Seo/index.tsx
index 0424eae8..e539595b 100644
--- a/ui/src/pages/Admin/Seo/index.tsx
+++ b/ui/src/pages/Admin/Seo/index.tsx
@@ -24,7 +24,7 @@ import type * as Type from '@/common/interface';
 import { getSeoSetting, putSeoSetting } from '@/services';
 import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { seoSettingStore } from '@/stores';
 
 const Index: FC = () => {
@@ -90,6 +90,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/SettingsUsers/index.tsx b/ui/src/pages/Admin/SettingsUsers/index.tsx
index a83c2e3b..e2714790 100644
--- a/ui/src/pages/Admin/SettingsUsers/index.tsx
+++ b/ui/src/pages/Admin/SettingsUsers/index.tsx
@@ -29,7 +29,7 @@ import {
   putUsersSetting,
   AdminSettingsUsers,
 } from '@/services';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import * as Type from '@/common/interface';
 import { siteInfoStore } from '@/stores';
 
@@ -168,6 +168,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx
index 3103461f..40725c31 100644
--- a/ui/src/pages/Admin/Smtp/index.tsx
+++ b/ui/src/pages/Admin/Smtp/index.tsx
@@ -25,7 +25,7 @@ import { useToast } from '@/hooks';
 import { useSmtpSetting, updateSmtpSetting } from '@/services';
 import pattern from '@/common/pattern';
 import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 
 const Smtp: FC = () => {
   const { t } = useTranslation('translation', {
@@ -182,6 +182,8 @@ const Smtp: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx
index 346e2e05..0d219816 100644
--- a/ui/src/pages/Admin/Themes/index.tsx
+++ b/ui/src/pages/Admin/Themes/index.tsx
@@ -24,7 +24,7 @@ import type * as Type from '@/common/interface';
 import { getThemeSetting, putThemeSetting } from '@/services';
 import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { themeSettingStore } from '@/stores';
 import { setupAppTheme } from '@/utils/localize';
 
@@ -135,6 +135,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx
index c119e864..c8f4fab4 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -165,7 +165,7 @@ const Users: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('title')}</h3>
-      <div className="d-flex justify-content-between align-items-center mb-3">
+      <div className="d-flex flex-wrap justify-content-between align-items-center mb-3">
         <Stack direction="horizontal" gap={3}>
           <QueryGroup
             data={UserFilterKeys}
@@ -190,14 +190,17 @@ const Users: FC = () => {
           onChange={handleFilter}
           placeholder={t('filter.placeholder')}
           style={{ width: '12.25rem' }}
+          className="mt-3 mt-sm-0"
         />
       </div>
-      <Table>
+      <Table responsive>
         <thead>
           <tr>
             <th>{t('name')}</th>
             <th style={{ width: '12%' }}>{t('reputation')}</th>
-            <th style={{ width: '20%' }}>{t('email')}</th>
+            <th style={{ width: '20%' }} className="min-w-15">
+              {t('email')}
+            </th>
             <th className="text-nowrap" style={{ width: '15%' }}>
               {t('created_at')}
             </th>
diff --git a/ui/src/pages/Admin/Write/index.tsx b/ui/src/pages/Admin/Write/index.tsx
index 7c837435..42806111 100644
--- a/ui/src/pages/Admin/Write/index.tsx
+++ b/ui/src/pages/Admin/Write/index.tsx
@@ -27,7 +27,7 @@ import {
   getRequireAndReservedTag,
   postRequireAndReservedTag,
 } from '@/services';
-import { handleFormError } from '@/utils';
+import { handleFormError, scrollToElementTop } from '@/utils';
 import { writeSettingStore } from '@/stores';
 
 const Index: FC = () => {
@@ -121,6 +121,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         }
       });
   };
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/pages/Admin/index.scss
index c723b8ac..27a9af01 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/pages/Admin/index.scss
@@ -21,3 +21,21 @@
   padding-top: 1.5rem;
   padding-bottom: 1.5rem;
 }
+
+.min-w-15 {
+  min-width: 15rem;
+}
+
+.max-w-30 {
+  max-width: 30rem;
+}
+
+@media screen and (max-width: 768px) {
+  .max-w-30 {
+    max-width: 15rem;
+  }
+}
+
+.table tr th {
+  white-space: nowrap;
+}
diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx
index 69723d29..14a2b30b 100644
--- a/ui/src/pages/Install/index.tsx
+++ b/ui/src/pages/Install/index.tsx
@@ -29,7 +29,12 @@ import {
   installBaseInfo,
   checkConfigFileExists,
 } from '@/services';
-import { Storage, handleFormError, scrollToDocTop } from '@/utils';
+import {
+  Storage,
+  handleFormError,
+  scrollToDocTop,
+  scrollToElementTop,
+} from '@/utils';
 import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
 
 import {
@@ -201,6 +206,8 @@ const Index: FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
         } else {
           handleErr(err);
         }
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index f0a061bd..cf7a2f1b 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -75,7 +75,7 @@ const Layout: FC = () => {
         }}>
         <Header />
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
-        <div className="position-relative page-wrap">
+        <div className="position-relative page-wrap d-flex flex-column flex-fill">
           {httpStatusCode ? (
             <HttpErrorContent httpCode={httpStatusCode} />
           ) : (
diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx
index 9a1ff14a..30939a2e 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -27,7 +27,7 @@ import classNames from 'classnames';
 import isEqual from 'lodash/isEqual';
 import debounce from 'lodash/debounce';
 
-import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePageTags, usePromptWithUnload } from '@/hooks';
 import { Editor, EditorRef, TagSelector } from '@/components';
 import type * as Type from '@/common/interface';
 import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
@@ -40,8 +40,14 @@ import {
   getTagsBySlugName,
   saveQuestionWithAnswer,
 } from '@/services';
-import { handleFormError, SaveDraft, storageExpires } from '@/utils';
+import {
+  handleFormError,
+  SaveDraft,
+  storageExpires,
+  scrollToElementTop,
+} from '@/utils';
 import { pathFactory } from '@/router/pathFactory';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 import SearchQuestion from './components/SearchQuestion';
 
@@ -87,7 +93,7 @@ const Ask = () => {
   const [formData, setFormData] = useState<FormDataItem>(initFormData);
   const [immData, setImmData] = useState<FormDataItem>(initFormData);
   const [checked, setCheckState] = useState(false);
-  const contentChangedRef = useRef(false);
+  const [blockState, setBlockState] = useState(false);
   const [focusType, setForceType] = useState('');
   const [hasDraft, setHasDraft] = useState(false);
   const resetForm = () => {
@@ -120,8 +126,8 @@ const Ask = () => {
 
   const isEdit = qid !== undefined;
 
-  const saveCaptcha = useCaptchaModal('question');
-  const editCaptcha = useCaptchaModal('edit');
+  const saveCaptcha = useCaptchaPlugin('question');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   const removeDraft = () => {
     saveDraft.save.cancel();
@@ -165,9 +171,9 @@ const Ask = () => {
           tags.value.map((v) => v.slug_name),
         )
       ) {
-        contentChangedRef.current = true;
+        setBlockState(true);
       } else {
-        contentChangedRef.current = false;
+        setBlockState(false);
       }
       return;
     }
@@ -188,15 +194,15 @@ const Ask = () => {
         },
         callback: () => setHasDraft(true),
       });
-      contentChangedRef.current = true;
+      setBlockState(true);
     } else {
       removeDraft();
-      contentChangedRef.current = false;
+      setBlockState(false);
     }
   }, [formData]);
 
   usePromptWithUnload({
-    when: contentChangedRef.current,
+    when: blockState,
   });
 
   const { data: revisions = [] } = useQueryRevisions(qid);
@@ -276,6 +282,83 @@ const Ask = () => {
     }
   };
 
+  const submitModifyQuestion = (params) => {
+    const ep = {
+      ...params,
+      id: qid,
+      edit_summary: formData.edit_summary.value,
+    };
+    const imgCode = editCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
+      ep.captcha_code = imgCode.captcha_code;
+      ep.captcha_id = imgCode.captcha_id;
+    }
+    modifyQuestion(ep)
+      .then(async (res) => {
+        await editCaptcha?.close();
+        navigate(pathFactory.questionLanding(qid, res?.url_title), {
+          state: { isReview: res?.wait_for_review },
+        });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          editCaptcha?.handleCaptchaError(err.list);
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  const submitQuestion = async (params) => {
+    const imgCode = saveCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
+      params.captcha_code = imgCode.captcha_code;
+      params.captcha_id = imgCode.captcha_id;
+    }
+    let res;
+    if (checked) {
+      res = await saveQuestionWithAnswer({
+        ...params,
+        answer_content: formData.answer_content.value,
+      }).catch((err) => {
+        if (err.isError) {
+          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);
+          if (!(captchaErr && err.list.length === 1)) {
+            const data = handleFormError(err, formData);
+            setFormData({ ...data });
+            const ele = document.getElementById(err.list[0].error_field);
+            scrollToElementTop(ele);
+          }
+        }
+      });
+    } else {
+      res = await saveQuestion(params).catch((err) => {
+        if (err.isError) {
+          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);
+          if (!(captchaErr && err.list.length === 1)) {
+            const data = handleFormError(err, formData);
+            setFormData({ ...data });
+            const ele = document.getElementById(err.list[0].error_field);
+            scrollToElementTop(ele);
+          }
+        }
+      });
+    }
+
+    const id = res?.id || res?.question?.id;
+    if (id) {
+      await saveCaptcha?.close();
+      if (checked) {
+        navigate(pathFactory.questionLanding(id, res?.question?.url_title));
+      } else {
+        navigate(pathFactory.questionLanding(id, res?.url_title));
+      }
+    }
+    removeDraft();
+  };
+
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
     event.stopPropagation();
@@ -287,77 +370,22 @@ const Ask = () => {
     };
 
     if (isEdit) {
+      if (!editCaptcha) {
+        setBlockState(false);
+        submitModifyQuestion(params);
+        return;
+      }
       editCaptcha.check(() => {
-        contentChangedRef.current = false;
-        const ep = {
-          ...params,
-          id: qid,
-          edit_summary: formData.edit_summary.value,
-        };
-        const imgCode = editCaptcha.getCaptcha();
-        if (imgCode.verify) {
-          ep.captcha_code = imgCode.captcha_code;
-          ep.captcha_id = imgCode.captcha_id;
-        }
-        modifyQuestion(ep)
-          .then(async (res) => {
-            await editCaptcha.close();
-            navigate(pathFactory.questionLanding(qid, res?.url_title), {
-              state: { isReview: res?.wait_for_review },
-            });
-          })
-          .catch((err) => {
-            if (err.isError) {
-              editCaptcha.handleCaptchaError(err.list);
-              const data = handleFormError(err, formData);
-              setFormData({ ...data });
-            }
-          });
+        submitModifyQuestion(params);
       });
     } else {
-      saveCaptcha.check(async () => {
-        contentChangedRef.current = false;
-        const imgCode = saveCaptcha.getCaptcha();
-        if (imgCode.verify) {
-          params.captcha_code = imgCode.captcha_code;
-          params.captcha_id = imgCode.captcha_id;
-        }
-        let res;
-        if (checked) {
-          res = await saveQuestionWithAnswer({
-            ...params,
-            answer_content: formData.answer_content.value,
-          }).catch((err) => {
-            if (err.isError) {
-              const captchaErr = saveCaptcha.handleCaptchaError(err.list);
-              if (!(captchaErr && err.list.length === 1)) {
-                const data = handleFormError(err, formData);
-                setFormData({ ...data });
-              }
-            }
-          });
-        } else {
-          res = await saveQuestion(params).catch((err) => {
-            if (err.isError) {
-              const captchaErr = saveCaptcha.handleCaptchaError(err.list);
-              if (!(captchaErr && err.list.length === 1)) {
-                const data = handleFormError(err, formData);
-                setFormData({ ...data });
-              }
-            }
-          });
-        }
-
-        const id = res?.id || res?.question?.id;
-        if (id) {
-          await saveCaptcha.close();
-          if (checked) {
-            navigate(pathFactory.questionLanding(id, res?.question?.url_title));
-          } else {
-            navigate(pathFactory.questionLanding(id, res?.url_title));
-          }
-        }
-        removeDraft();
+      if (!saveCaptcha) {
+        setBlockState(false);
+        submitQuestion(params);
+        return;
+      }
+      saveCaptcha?.check(async () => {
+        submitQuestion(params);
       });
     }
   };
@@ -426,7 +454,7 @@ const Ask = () => {
               </Form.Control.Feedback>
               {bool && <SearchQuestion similarQuestions={similarQuestions} />}
             </Form.Group>
-            <Form.Group controlId="body">
+            <Form.Group controlId="content">
               <Form.Label>{t('form.fields.body.label')}</Form.Label>
               <Form.Control
                 defaultValue={formData.content.value}
diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
index 82f9fa27..b8e29942 100644
--- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
@@ -27,7 +27,7 @@ import classNames from 'classnames';
 import { Avatar } from '@/components';
 import { getInviteUser, putInviteUser } from '@/services';
 import type * as Type from '@/common/interface';
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 import PeopleDropdown from './PeopleDropdown';
 
@@ -44,7 +44,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
 
   const [editing, setEditing] = useState(false);
   const [users, setUsers] = useState<Type.UserInfoBase[]>([]);
-  const iaCaptcha = useCaptchaModal('invitation_answer');
+  const iaCaptcha = useCaptchaPlugin('invitation_answer');
 
   const initInviteUsers = () => {
     if (!questionId) {
@@ -74,28 +74,35 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
     setUsers(userList);
   };
 
+  const submitInviteUser = () => {
+    const names = users.map((_) => {
+      return _.username;
+    });
+    const imgCode: Type.ImgCodeReq = {};
+    iaCaptcha?.resolveCaptchaReq(imgCode);
+    putInviteUser(questionId, names, imgCode)
+      .then(async () => {
+        await iaCaptcha?.close();
+        setEditing(false);
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          iaCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
+
   const saveInviteUsers = () => {
     if (!users) {
       return;
     }
-    const names = users.map((_) => {
-      return _.username;
-    });
-    iaCaptcha.check(() => {
-      const imgCode: Type.ImgCodeReq = {};
-      iaCaptcha.resolveCaptchaReq(imgCode);
-      putInviteUser(questionId, names, imgCode)
-        .then(async () => {
-          await iaCaptcha.close();
-          setEditing(false);
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            iaCaptcha.handleCaptchaError(ex.list);
-          }
-          console.error('putInviteUser error: ', ex);
-        });
-    });
+
+    if (!iaCaptcha) {
+      submitInviteUser();
+      return;
+    }
+
+    iaCaptcha.check(() => submitInviteUser());
   };
 
   useEffect(() => {
diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
index 9bc8be44..456fbe37 100644
--- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
@@ -25,7 +25,8 @@ import { Link } from 'react-router-dom';
 import { marked } from 'marked';
 import classNames from 'classnames';
 
-import { usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePromptWithUnload } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { Editor, Modal, TextArea } from '@/components';
 import { FormDataType, PostAnswerReq } from '@/common/interface';
 import { postAnswer } from '@/services';
@@ -63,8 +64,9 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
   const [editorFocusState, setEditorFocusState] = useState(false);
   const [hasDraft, setHasDraft] = useState(false);
   const [showTips, setShowTips] = useState(data.loggedUserRank < 100);
-  const aCaptcha = useCaptchaModal('answer');
+  const aCaptcha = useCaptchaPlugin('answer');
   const writeInfo = writeSettingStore((state) => state.write);
+  const [editorCanSave, setEditorCanSave] = useState(false);
 
   usePromptWithUnload({
     when: Boolean(formData.content.value),
@@ -80,6 +82,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
   useEffect(() => {
     const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);
     if (draft?.questionId === data.qid && draft?.content) {
+      setShowEditor(true);
       setFormData({
         content: {
           value: draft.content,
@@ -87,9 +90,11 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
           errorMsg: '',
         },
       });
-      setShowEditor(true);
       setHasDraft(true);
     }
+    setTimeout(() => {
+      setEditorCanSave(true);
+    }, 100);
   }, []);
 
   useEffect(() => {
@@ -152,6 +157,40 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
     }
   };
 
+  const submitAnswer = () => {
+    const params: PostAnswerReq = {
+      question_id: data?.qid,
+      content: formData.content.value,
+      html: marked.parse(formData.content.value),
+    };
+    const imgCode = aCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
+      params.captcha_code = imgCode.captcha_code;
+      params.captcha_id = imgCode.captcha_id;
+    }
+    postAnswer(params)
+      .then(async (res) => {
+        await aCaptcha?.close();
+        setShowEditor(false);
+        setFormData({
+          content: {
+            value: '',
+            isInvalid: false,
+            errorMsg: '',
+          },
+        });
+        removeDraft();
+        callback?.(res.info);
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          aCaptcha?.handleCaptchaError(ex.list);
+          const stateData = handleFormError(ex, formData);
+          setFormData({ ...stateData });
+        }
+      });
+  };
+
   const handleSubmit = () => {
     if (!guard.tryNormalLogged(true)) {
       return;
@@ -159,40 +198,11 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
     if (!checkValidated()) {
       return;
     }
-
-    aCaptcha.check(() => {
-      const params: PostAnswerReq = {
-        question_id: data?.qid,
-        content: formData.content.value,
-        html: marked.parse(formData.content.value),
-      };
-      const imgCode = aCaptcha.getCaptcha();
-      if (imgCode.verify) {
-        params.captcha_code = imgCode.captcha_code;
-        params.captcha_id = imgCode.captcha_id;
-      }
-      postAnswer(params)
-        .then(async (res) => {
-          await aCaptcha.close();
-          setShowEditor(false);
-          setFormData({
-            content: {
-              value: '',
-              isInvalid: false,
-              errorMsg: '',
-            },
-          });
-          removeDraft();
-          callback?.(res.info);
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            aCaptcha.handleCaptchaError(ex.list);
-            const stateData = handleFormError(ex, formData);
-            setFormData({ ...stateData });
-          }
-        });
-    });
+    if (!aCaptcha) {
+      submitAnswer();
+      return;
+    }
+    aCaptcha.check(() => submitAnswer());
   };
 
   const clickBtn = () => {
@@ -260,13 +270,15 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
                 value={formData.content.value}
                 autoFocus={editorFocusState}
                 onChange={(val) => {
-                  setFormData({
-                    content: {
-                      value: val,
-                      isInvalid: false,
-                      errorMsg: '',
-                    },
-                  });
+                  if (editorCanSave) {
+                    setFormData({
+                      content: {
+                        value: val,
+                        isInvalid: false,
+                        errorMsg: '',
+                      },
+                    });
+                  }
                 }}
                 onFocus={() => {
                   setFocusType('answer');
diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx
index fbb3e327..49e7b33a 100644
--- a/ui/src/pages/Questions/EditAnswer/index.tsx
+++ b/ui/src/pages/Questions/EditAnswer/index.tsx
@@ -26,7 +26,8 @@ import dayjs from 'dayjs';
 import classNames from 'classnames';
 
 import { handleFormError, scrollToDocTop } from '@/utils';
-import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePageTags, usePromptWithUnload } from '@/hooks';
+import { useCaptchaPlugin, useRenderHtmlPlugin } from '@/utils/pluginKit';
 import { pathFactory } from '@/router/pathFactory';
 import { Editor, EditorRef, Icon, htmlRender } from '@/components';
 import type * as Type from '@/common/interface';
@@ -35,7 +36,6 @@ import {
   modifyAnswer,
   useQueryRevisions,
 } from '@/services';
-import { useRenderHtmlPlugin } from '@/utils/pluginKit';
 
 import './index.scss';
 
@@ -71,7 +71,7 @@ const Index = () => {
   const [formData, setFormData] = useState<FormDataItem>(initFormData);
   const [immData, setImmData] = useState(initFormData);
   const [contentChanged, setContentChanged] = useState(false);
-  const editCaptcha = useCaptchaModal('edit');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   useEffect(() => {
     if (data?.info?.content) {
@@ -153,6 +153,39 @@ const Index = () => {
     return bol;
   };
 
+  const submitEditAnswer = () => {
+    const params: Type.AnswerParams = {
+      content: formData.content.value,
+      html: editorRef.current.getHtml(),
+      question_id: qid,
+      id: aid,
+      edit_summary: formData.description.value,
+    };
+    editCaptcha?.resolveCaptchaReq(params);
+
+    modifyAnswer(params)
+      .then(async (res) => {
+        await editCaptcha?.close();
+        navigate(
+          pathFactory.answerLanding({
+            questionId: qid,
+            slugTitle: data?.question?.url_title,
+            answerId: aid,
+          }),
+          {
+            state: { isReview: res?.wait_for_review },
+          },
+        );
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          editCaptcha?.handleCaptchaError(ex.list);
+          const stateData = handleFormError(ex, formData);
+          setFormData({ ...stateData });
+        }
+      });
+  };
+
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     setContentChanged(false);
 
@@ -163,38 +196,11 @@ const Index = () => {
       return;
     }
 
-    editCaptcha.check(() => {
-      const params: Type.AnswerParams = {
-        content: formData.content.value,
-        html: editorRef.current.getHtml(),
-        question_id: qid,
-        id: aid,
-        edit_summary: formData.description.value,
-      };
-      editCaptcha.resolveCaptchaReq(params);
-
-      modifyAnswer(params)
-        .then(async (res) => {
-          await editCaptcha.close();
-          navigate(
-            pathFactory.answerLanding({
-              questionId: qid,
-              slugTitle: data?.question?.url_title,
-              answerId: aid,
-            }),
-            {
-              state: { isReview: res?.wait_for_review },
-            },
-          );
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            editCaptcha.handleCaptchaError(ex.list);
-            const stateData = handleFormError(ex, formData);
-            setFormData({ ...stateData });
-          }
-        });
-    });
+    if (!editCaptcha) {
+      submitEditAnswer();
+      return;
+    }
+    editCaptcha.check(() => submitEditAnswer());
   };
   const handleSelectedRevision = (e) => {
     const index = e.target.value;
diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx
index e0bf27ff..f1b26c18 100644
--- a/ui/src/pages/Questions/index.tsx
+++ b/ui/src/pages/Questions/index.tsx
@@ -53,7 +53,7 @@ const Questions: FC = () => {
     Storage.set(QUESTIONS_ORDER_STORAGE_KEY, curOrder);
   }
   const reqParams: Type.QueryQuestionsReq = {
-    page_size: 20,
+    page_size: 10,
     page: curPage,
     order: curOrder as Type.QuestionOrderBy,
   };
diff --git a/ui/src/pages/Review/components/ApproveDropdown/index.tsx b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
index f6a62471..b2c05c96 100644
--- a/ui/src/pages/Review/components/ApproveDropdown/index.tsx
+++ b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
@@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next';
 
 import { Modal } from '@/components';
 import { putFlagReviewAction } from '@/services';
-import { useCaptchaModal, useReportModal, useToast } from '@/hooks';
+import { useReportModal, useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type * as Type from '@/common/interface';
 import EditPostModal from '../EditPostModal';
 
@@ -46,12 +47,52 @@ const Index: FC<IProps> = ({
   const [showEditPostModal, setShowEditPostModal] = useState(false);
   const closeModal = useReportModal(approveCallback);
   const toast = useToast();
-  const dCaptcha = useCaptchaModal('delete');
+  const dCaptcha = useCaptchaPlugin('delete');
 
   const handleEditPostModalState = () => {
     setShowEditPostModal(!showEditPostModal);
   };
 
+  const submitReviewAction = () => {
+    const req: Type.PutFlagReviewParams = {
+      operation_type: 'delete_post',
+      flag_id: String(itemData?.flag_id),
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    dCaptcha?.resolveCaptchaReq(req);
+
+    delete req.captcha_code;
+    delete req.captcha_id;
+
+    putFlagReviewAction(req)
+      .then(async () => {
+        await dCaptcha?.close();
+        let msg = '';
+        if (objectType === 'question') {
+          msg = t('post_deleted', { keyPrefix: 'messages' });
+        }
+        if (objectType === 'answer') {
+          msg = t('tip_answer_deleted');
+        }
+        if (objectType === 'answer' || objectType === 'question') {
+          toast.onShow({
+            msg,
+            variant: 'success',
+          });
+        }
+        approveCallback();
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
   const handleDelete = () => {
     let content = '';
 
@@ -78,45 +119,11 @@ const Index: FC<IProps> = ({
       confirmBtnVariant: 'danger',
       confirmText: t('delete', { keyPrefix: 'btns' }),
       onConfirm: () => {
-        dCaptcha.check(() => {
-          const req: Type.PutFlagReviewParams = {
-            operation_type: 'delete_post',
-            flag_id: String(itemData?.flag_id),
-            captcha_code: undefined,
-            captcha_id: undefined,
-          };
-          dCaptcha.resolveCaptchaReq(req);
-
-          delete req.captcha_code;
-          delete req.captcha_id;
-
-          putFlagReviewAction(req)
-            .then(async () => {
-              await dCaptcha.close();
-              let msg = '';
-              if (objectType === 'question') {
-                msg = t('post_deleted', { keyPrefix: 'messages' });
-              }
-              if (objectType === 'answer') {
-                msg = t('tip_answer_deleted');
-              }
-              if (objectType === 'answer' || objectType === 'question') {
-                toast.onShow({
-                  msg,
-                  variant: 'success',
-                });
-              }
-              approveCallback();
-            })
-            .catch((ex) => {
-              if (ex.isError) {
-                dCaptcha.handleCaptchaError(ex.list);
-              }
-            })
-            .finally(() => {
-              setIsLoading(false);
-            });
-        });
+        if (!dCaptcha) {
+          submitReviewAction();
+          return;
+        }
+        dCaptcha.check(() => submitReviewAction());
       },
       onCancel: () => {
         setIsLoading(false);
diff --git a/ui/src/pages/Review/components/EditPostModal/index.tsx b/ui/src/pages/Review/components/EditPostModal/index.tsx
index 0eefd311..9e7ae5e9 100644
--- a/ui/src/pages/Review/components/EditPostModal/index.tsx
+++ b/ui/src/pages/Review/components/EditPostModal/index.tsx
@@ -24,13 +24,15 @@ import { useTranslation } from 'react-i18next';
 import classNames from 'classnames';
 
 import { putFlagReviewAction } from '@/services';
-import { useCaptchaModal, usePageUsers } from '@/hooks';
+import { usePageUsers } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { Editor, TagSelector, Mentions, TextArea } from '@/components';
 import {
   // matchedUsers,
   parseUserInfo,
   handleFormError,
   parseEditMentionUser,
+  scrollToElementTop,
 } from '@/utils';
 import type * as Type from '@/common/interface';
 
@@ -89,7 +91,7 @@ const Index: FC<Props> = ({
   const [loaded, setLoaded] = useState(false);
   const pageUsers = usePageUsers();
 
-  const editCaptcha = useCaptchaModal('edit');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   const onClose = (bol) => {
     if (bol) {
@@ -157,17 +159,19 @@ const Index: FC<Props> = ({
     setFormData({
       ...formData,
     });
-    return bol;
-  };
-
-  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    event.stopPropagation();
 
-    if (!checkValidated()) {
-      return;
+    if (!bol) {
+      const errObj = Object.keys(formData).filter(
+        (key) => formData[key].isInvalid,
+      );
+      const ele = document.getElementById(errObj[0]);
+      scrollToElementTop(ele);
     }
 
+    return bol;
+  };
+
+  const submitFlagReviewAction = () => {
     const params: Type.PutFlagReviewParams = {
       title: formData.title.value,
       content: formData.content.value,
@@ -191,28 +195,44 @@ const Index: FC<Props> = ({
       delete params.title;
       delete params.tags;
     }
+    if (objectType === 'question') {
+      const imgCode = editCaptcha?.getCaptcha();
+      if (imgCode?.verify) {
+        params.captcha_code = imgCode.captcha_code;
+        params.captcha_id = imgCode.captcha_id;
+      }
+    }
+    putFlagReviewAction(params)
+      .then(async () => {
+        await editCaptcha?.close();
+        onClose(true);
+      })
+      .catch((err) => {
+        if (err.isError) {
+          editCaptcha?.handleCaptchaError(err.list);
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
 
-    editCaptcha.check(() => {
... 1315 lines suppressed ...