Procházet zdrojové kódy

feat: add English-speaking feature;

jimmylee před 3 měsíci
rodič
revize
fdd280c6f8
57 změnil soubory, kde provedl 22438 přidání a 28 odebrání
  1. 6 0
      .env
  2. 8 0
      .env.example
  3. 5 0
      .gitignore
  4. 4 0
      env.d.ts
  5. 11966 0
      package-lock.json
  6. 3 0
      package.json
  7. 26 0
      public/pcm-recorder-worklet.js
  8. 126 0
      scripts/test-dialogue-stream-fallback.mjs
  9. 64 0
      scripts/test-speaking-api-config.mjs
  10. 30 1
      src/components/CollapsibleToolbar/index2.vue
  11. 104 0
      src/services/speaking.ts
  12. 168 0
      src/store/speaking.ts
  13. 342 0
      src/types/englishSpeaking.ts
  14. 8 1
      src/views/Editor/Canvas/Operate/index.vue
  15. 6 0
      src/views/Editor/Canvas/index.vue
  16. 46 0
      src/views/Editor/CanvasTool/index2.vue
  17. 172 0
      src/views/Editor/EnglishSpeaking/SpeakingPanel.vue
  18. 239 0
      src/views/Editor/EnglishSpeaking/components/AIGenerationForm.vue
  19. 123 0
      src/views/Editor/EnglishSpeaking/components/CreationModeSwitch.vue
  20. 116 0
      src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue
  21. 145 0
      src/views/Editor/EnglishSpeaking/components/ExerciseTypeGrid.vue
  22. 123 0
      src/views/Editor/EnglishSpeaking/components/RecommendCard.vue
  23. 62 0
      src/views/Editor/EnglishSpeaking/components/UnitInfoCard.vue
  24. 170 0
      src/views/Editor/EnglishSpeaking/composables/useAudioPlayer.ts
  25. 300 0
      src/views/Editor/EnglishSpeaking/composables/useAudioRecorder.ts
  26. 615 0
      src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts
  27. 1082 0
      src/views/Editor/EnglishSpeaking/configs/TopicDiscussionConfig.vue
  28. 113 0
      src/views/Editor/EnglishSpeaking/data/curriculum.json
  29. 52 0
      src/views/Editor/EnglishSpeaking/data/topicDiscussionTasks.json
  30. 63 0
      src/views/Editor/EnglishSpeaking/layers/Layer1Home.vue
  31. 336 0
      src/views/Editor/EnglishSpeaking/layers/Layer2Speaking.vue
  32. 374 0
      src/views/Editor/EnglishSpeaking/preview/DetailedReport.vue
  33. 1936 0
      src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue
  34. 336 0
      src/views/Editor/EnglishSpeaking/preview/OverallReport.vue
  35. 155 0
      src/views/Editor/EnglishSpeaking/preview/StudentPreview.vue
  36. 490 0
      src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue
  37. 678 0
      src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
  38. 320 0
      src/views/Editor/EnglishSpeaking/services/llmService.ts
  39. 18 0
      src/views/Editor/EnglishSpeaking/services/speakingApiConfig.ts
  40. 60 0
      src/views/Editor/EnglishSpeaking/services/speechService.ts
  41. 2 0
      src/views/Editor/index3.vue
  42. 117 0
      src/views/Student/components/SpeakingClassPanel/AISummary.vue
  43. 68 0
      src/views/Student/components/SpeakingClassPanel/NotStartedTip.vue
  44. 99 0
      src/views/Student/components/SpeakingClassPanel/SortMenu.vue
  45. 98 0
      src/views/Student/components/SpeakingClassPanel/StudentGrid.vue
  46. 200 0
      src/views/Student/components/SpeakingClassPanel/StudentReportModal.vue
  47. 252 0
      src/views/Student/components/SpeakingClassPanel/index.vue
  48. 31 0
      src/views/Student/components/SpeakingClassPanel/types.ts
  49. 173 0
      src/views/Student/components/SpeakingClassPanel/useClassSummary.ts
  50. 63 1
      src/views/Student/index.vue
  51. 2 2
      src/views/components/ThumbnailSlide/ThumbnailElement.vue
  52. 13 5
      src/views/components/element/FrameElement/BaseFrameElement.vue
  53. 41 13
      src/views/components/element/FrameElement/index.vue
  54. 95 2
      src/views/lang/cn.json
  55. 95 2
      src/views/lang/en.json
  56. 94 1
      src/views/lang/hk.json
  57. 5 0
      vite.config.ts

+ 6 - 0
.env

@@ -0,0 +1,6 @@
+VITE_AZURE_SPEECH_KEY=5b401f52c9064cec9247f4190cf99ea4
+VITE_AZURE_SPEECH_REGION=eastasia
+# VITE_SPEAKING_API_HOST=http://localhost:8000
+
+# 学生录音上送方式:websocket(默认,流式) | http(单次 POST /speak)
+# VITE_SPEAKING_TRANSPORT=http

+ 8 - 0
.env.example

@@ -0,0 +1,8 @@
+VITE_AZURE_SPEECH_KEY=
+VITE_AZURE_SPEECH_REGION=
+VITE_SPEAKING_API_HOST=https://ppt-english-speaking-api.cocorobo.cn
+
+# 学生录音上送方式:websocket(默认,流式) | http(单次 POST /speak)
+# 留空或 'websocket'/'ws' → WebSocket;'http' → /speak。WS 失败时不会自动降级,
+# 由用户点"重试"按钮触发 /speak 重发。
+VITE_SPEAKING_TRANSPORT=websocket

+ 5 - 0
.gitignore

@@ -29,3 +29,8 @@ coverage
 *.sw?
 
 *.tsbuildinfo
+
+
+.claude
+.agent
+

+ 4 - 0
env.d.ts

@@ -1,2 +1,6 @@
 // eslint-disable-next-line spaced-comment
 /// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_SPEAKING_API_HOST?: string
+}

+ 11966 - 0
package-lock.json

@@ -0,0 +1,11966 @@
+{
+  "name": "pptist",
+  "version": "2.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "pptist",
+      "version": "2.0.0",
+      "dependencies": {
+        "@icon-park/vue-next": "^1.4.2",
+        "@microsoft/fetch-event-source": "^2.0.1",
+        "animate.css": "^4.1.1",
+        "axios": "^1.7.9",
+        "clipboard": "^2.0.11",
+        "crypto-js": "^4.2.0",
+        "dexie": "^4.0.11",
+        "echarts": "^5.6.0",
+        "echarts-wordcloud": "^2.1.0",
+        "file-saver": "^2.0.5",
+        "hfmath": "^0.0.2",
+        "html-to-image": "^1.11.13",
+        "html2canvas": "^1.4.1",
+        "katex": "^0.16.22",
+        "lodash": "^4.17.21",
+        "markdown-it": "^14.1.1",
+        "mitt": "^3.0.1",
+        "nanoid": "^5.0.7",
+        "number-precision": "^1.6.0",
+        "pinia": "^3.0.2",
+        "pptxgenjs": "^3.12.0",
+        "pptxtojson": "1.12.0",
+        "prosemirror-commands": "^1.6.0",
+        "prosemirror-dropcursor": "^1.8.1",
+        "prosemirror-gapcursor": "^1.3.2",
+        "prosemirror-history": "^1.3.2",
+        "prosemirror-inputrules": "^1.4.0",
+        "prosemirror-keymap": "^1.2.2",
+        "prosemirror-model": "^1.22.2",
+        "prosemirror-schema-basic": "^1.2.3",
+        "prosemirror-schema-list": "^1.4.1",
+        "prosemirror-state": "^1.4.3",
+        "prosemirror-view": "^1.33.9",
+        "qs": "^6.14.0",
+        "rtf.js": "^3.0.9",
+        "svg-arc-to-cubic-bezier": "^3.2.0",
+        "svg-pathdata": "^7.2.0",
+        "tinycolor2": "^1.6.0",
+        "tippy.js": "^6.3.7",
+        "utif": "^3.1.0",
+        "utif2": "^4.1.0",
+        "uuid": "^13.0.0",
+        "vue": "^3.5.17",
+        "vuedraggable": "^4.1.0",
+        "wangeditor": "^4.7.15",
+        "webcodecs-encoder": "^0.3.2",
+        "y-websocket": "^3.0.0",
+        "yjs": "^13.6.27"
+      },
+      "devDependencies": {
+        "@commitlint/cli": "^18.4.3",
+        "@commitlint/config-conventional": "^18.4.3",
+        "@rushstack/eslint-patch": "^1.3.3",
+        "@tsconfig/node18": "^18.2.2",
+        "@types/crypto-js": "^4.2.1",
+        "@types/file-saver": "^2.0.7",
+        "@types/html2canvas": "^1.0.0",
+        "@types/lodash": "^4.14.202",
+        "@types/markdown-it": "^14.1.2",
+        "@types/node": "^18.19.3",
+        "@types/qs": "^6.14.0",
+        "@types/svg-arc-to-cubic-bezier": "^3.2.2",
+        "@types/tinycolor2": "^1.4.6",
+        "@vitejs/plugin-vue": "^5.1.0",
+        "@vue/eslint-config-typescript": "^12.0.0",
+        "@vue/tsconfig": "^0.5.0",
+        "eslint": "^8.49.0",
+        "eslint-plugin-vue": "^9.17.0",
+        "husky": "^8.0.3",
+        "npm-run-all2": "^6.1.1",
+        "sass": "1.69.6",
+        "typescript": "~5.3.0",
+        "vite": "^5.3.5",
+        "vite-plugin-vue-inspector": "^5.4.0",
+        "vue-tsc": "^2.0.29"
+      }
+    },
+    "node_modules/@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmmirror.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.27.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+      "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+      "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/helper-replace-supers": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/traverse": "^7.28.6",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+      "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.5",
+        "@babel/types": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+      "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+      "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+      "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.27.1",
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-decorators": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz",
+      "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/plugin-syntax-decorators": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-decorators": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
+      "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-attributes": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+      "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-meta": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+      "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+      "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+      "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/plugin-syntax-typescript": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/runtime-corejs3": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
+      "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==",
+      "dependencies": {
+        "core-js-pure": "^3.43.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@commitlint/cli": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-18.4.3.tgz",
+      "integrity": "sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/format": "^18.4.3",
+        "@commitlint/lint": "^18.4.3",
+        "@commitlint/load": "^18.4.3",
+        "@commitlint/read": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "execa": "^5.0.0",
+        "lodash.isfunction": "^3.0.9",
+        "resolve-from": "5.0.0",
+        "resolve-global": "1.0.0",
+        "yargs": "^17.0.0"
+      },
+      "bin": {
+        "commitlint": "cli.js"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/cli/node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@commitlint/config-conventional": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-18.4.3.tgz",
+      "integrity": "sha512-729eRRaNta7JZF07qf6SAGSghoDEp9mH7yHU0m7ff0q89W97wDrWCyZ3yoV3mcQJwbhlmVmZPTkPcm7qiAu8WA==",
+      "dev": true,
+      "dependencies": {
+        "conventional-changelog-conventionalcommits": "^7.0.2"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/config-validator": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-18.4.3.tgz",
+      "integrity": "sha512-FPZZmTJBARPCyef9ohRC9EANiQEKSWIdatx5OlgeHKu878dWwpyeFauVkhzuBRJFcCA4Uvz/FDtlDKs008IHcA==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/types": "^18.4.3",
+        "ajv": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/config-validator/node_modules/ajv": {
+      "version": "8.12.0",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true
+    },
+    "node_modules/@commitlint/ensure": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-18.4.3.tgz",
+      "integrity": "sha512-MI4fwD9TWDVn4plF5+7JUyLLbkOdzIRBmVeNlk4dcGlkrVA+/l5GLcpN66q9LkFsFv6G2X31y89ApA3hqnqIFg==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/types": "^18.4.3",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.kebabcase": "^4.1.1",
+        "lodash.snakecase": "^4.1.1",
+        "lodash.startcase": "^4.4.0",
+        "lodash.upperfirst": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/execute-rule": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-18.4.3.tgz",
+      "integrity": "sha512-t7FM4c+BdX9WWZCPrrbV5+0SWLgT3kCq7e7/GhHCreYifg3V8qyvO127HF796vyFql75n4TFF+5v1asOOWkV1Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/format": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/format/-/format-18.4.3.tgz",
+      "integrity": "sha512-8b+ItXYHxAhRAXFfYki5PpbuMMOmXYuzLxib65z2XTqki59YDQJGpJ/wB1kEE5MQDgSTQWtKUrA8n9zS/1uIDQ==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/types": "^18.4.3",
+        "chalk": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/is-ignored": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-18.4.3.tgz",
+      "integrity": "sha512-ZseOY9UfuAI32h9w342Km4AIaTieeFskm2ZKdrG7r31+c6zGBzuny9KQhwI9puc0J3GkUquEgKJblCl7pMnjwg==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/types": "^18.4.3",
+        "semver": "7.5.4"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/lint": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/lint/-/lint-18.4.3.tgz",
+      "integrity": "sha512-18u3MRgEXNbnYkMOWoncvq6QB8/90m9TbERKgdPqVvS+zQ/MsuRhdvHYCIXGXZxUb0YI4DV2PC4bPneBV/fYuA==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/is-ignored": "^18.4.3",
+        "@commitlint/parse": "^18.4.3",
+        "@commitlint/rules": "^18.4.3",
+        "@commitlint/types": "^18.4.3"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/load": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/load/-/load-18.4.3.tgz",
+      "integrity": "sha512-v6j2WhvRQJrcJaj5D+EyES2WKTxPpxENmNpNG3Ww8MZGik3jWRXtph0QTzia5ZJyPh2ib5aC/6BIDymkUUM58Q==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/config-validator": "^18.4.3",
+        "@commitlint/execute-rule": "^18.4.3",
+        "@commitlint/resolve-extends": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "@types/node": "^18.11.9",
+        "chalk": "^4.1.0",
+        "cosmiconfig": "^8.3.6",
+        "cosmiconfig-typescript-loader": "^5.0.0",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.merge": "^4.6.2",
+        "lodash.uniq": "^4.5.0",
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/load/node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@commitlint/message": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/message/-/message-18.4.3.tgz",
+      "integrity": "sha512-ddJ7AztWUIoEMAXoewx45lKEYEOeOlBVWjk8hDMUGpprkuvWULpaXczqdjwVtjrKT3JhhN+gMs8pm5G3vB2how==",
+      "dev": true,
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/parse": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/parse/-/parse-18.4.3.tgz",
+      "integrity": "sha512-eoH7CXM9L+/Me96KVcfJ27EIIbA5P9sqw3DqjJhRYuhaULIsPHFs5S5GBDCqT0vKZQDx0DgxhMpW6AQbnKrFtA==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/types": "^18.4.3",
+        "conventional-changelog-angular": "^7.0.0",
+        "conventional-commits-parser": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/read": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/read/-/read-18.4.3.tgz",
+      "integrity": "sha512-H4HGxaYA6OBCimZAtghL+B+SWu8ep4X7BwgmedmqWZRHxRLcX2q0bWBtUm5FsMbluxbOfrJwOs/Z0ah4roP/GQ==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/top-level": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "fs-extra": "^11.0.0",
+        "git-raw-commits": "^2.0.11",
+        "minimist": "^1.2.6"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/resolve-extends": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-18.4.3.tgz",
+      "integrity": "sha512-30sk04LZWf8+SDgJrbJCjM90gTg2LxsD9cykCFeFu+JFHvBFq5ugzp2eO/DJGylAdVaqxej3c7eTSE64hR/lnw==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/config-validator": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "import-fresh": "^3.0.0",
+        "lodash.mergewith": "^4.6.2",
+        "resolve-from": "^5.0.0",
+        "resolve-global": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/resolve-extends/node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@commitlint/rules": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/rules/-/rules-18.4.3.tgz",
+      "integrity": "sha512-8KIeukDf45BiY+Lul1T0imSNXF0sMrlLG6JpLLKolkmYVQ6PxxoNOriwyZ3UTFFpaVbPy0rcITaV7U9JCAfDTA==",
+      "dev": true,
+      "dependencies": {
+        "@commitlint/ensure": "^18.4.3",
+        "@commitlint/message": "^18.4.3",
+        "@commitlint/to-lines": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "execa": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/to-lines": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-18.4.3.tgz",
+      "integrity": "sha512-fy1TAleik4Zfru1RJ8ZU6cOSvgSVhUellxd3WZV1D5RwHZETt1sZdcA4mQN2y3VcIZsUNKkW0Mq8CM9/L9harQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/top-level": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-18.4.3.tgz",
+      "integrity": "sha512-E6fJPBLPFL5R8+XUNSYkj4HekIOuGMyJo3mIx2PkYc3clel+pcWQ7TConqXxNWW4x1ugigiIY2RGot55qUq1hw==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@commitlint/types": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/types/-/types-18.4.3.tgz",
+      "integrity": "sha512-cvzx+vtY/I2hVBZHCLrpoh+sA0hfuzHwDc+BAFPimYLjJkpHnghQM+z8W/KyLGkygJh3BtI3xXXq+dKjnSWEmA==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=v18"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+      "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+      "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.56.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.56.0.tgz",
+      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.11.13",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+      "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+      "dev": true,
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^2.0.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+      "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+      "dev": true
+    },
+    "node_modules/@icon-park/vue-next": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmmirror.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz",
+      "integrity": "sha512-+QklF255wkfBOabY+xw6FAI0Bwln/RhdwCunNy/9sKdKuChtaU67QZqU67KGAvZUTeeBgsL+yaHHxqfQeGZXEQ==",
+      "engines": {
+        "node": ">= 8.0.0",
+        "npm": ">= 5.0.0"
+      },
+      "peerDependencies": {
+        "vue": "3.x"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@microsoft/fetch-event-source": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
+      "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.8",
+      "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz",
+      "integrity": "sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.0.tgz",
+      "integrity": "sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz",
+      "integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz",
+      "integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.0.tgz",
+      "integrity": "sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.0.tgz",
+      "integrity": "sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.0.tgz",
+      "integrity": "sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.0.tgz",
+      "integrity": "sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.0.tgz",
+      "integrity": "sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.0.tgz",
+      "integrity": "sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.0.tgz",
+      "integrity": "sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz",
+      "integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.0.tgz",
+      "integrity": "sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.0.tgz",
+      "integrity": "sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.0.tgz",
+      "integrity": "sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.0.tgz",
+      "integrity": "sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rushstack/eslint-patch": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
+      "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node18": {
+      "version": "18.2.2",
+      "resolved": "https://registry.npmmirror.com/@tsconfig/node18/-/node18-18.2.2.tgz",
+      "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==",
+      "dev": true
+    },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.1.tgz",
+      "integrity": "sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==",
+      "dev": true
+    },
+    "node_modules/@types/dom-webcodecs": {
+      "version": "0.1.18",
+      "resolved": "https://registry.npmmirror.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.18.tgz",
+      "integrity": "sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "dev": true
+    },
+    "node_modules/@types/file-saver": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz",
+      "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+      "dev": true
+    },
+    "node_modules/@types/html2canvas": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
+      "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
+      "deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.",
+      "dev": true,
+      "dependencies": {
+        "html2canvas": "*"
+      }
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true
+    },
+    "node_modules/@types/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+      "dev": true
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.14.202",
+      "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.202.tgz",
+      "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
+      "dev": true
+    },
+    "node_modules/@types/markdown-it": {
+      "version": "14.1.2",
+      "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+      "dev": true,
+      "dependencies": {
+        "@types/linkify-it": "^5",
+        "@types/mdurl": "^2"
+      }
+    },
+    "node_modules/@types/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+      "dev": true
+    },
+    "node_modules/@types/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/@types/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.4",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.4.tgz",
+      "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true
+    },
+    "node_modules/@types/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+      "dev": true
+    },
+    "node_modules/@types/semver": {
+      "version": "7.5.6",
+      "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.5.6.tgz",
+      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "dev": true
+    },
+    "node_modules/@types/svg-arc-to-cubic-bezier": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmmirror.com/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.2.tgz",
+      "integrity": "sha512-XQtKy9lmkKlV+c3Jelo7kxNPw7qOqIq3GcnOhywGZHF7zw5D5m+Ssigbmf3Turbe/A8Ur+lRh8TYjuxXKvyivw==",
+      "dev": true
+    },
+    "node_modules/@types/tinycolor2": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
+      "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==",
+      "dev": true
+    },
+    "node_modules/@types/wicg-file-system-access": {
+      "version": "2020.9.8",
+      "resolved": "https://registry.npmmirror.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz",
+      "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==",
+      "license": "MIT"
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
+      "integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/type-utils": "6.16.0",
+        "@typescript-eslint/utils": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "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"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.16.0.tgz",
+      "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
+      "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
+      "integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "@typescript-eslint/utils": "6.16.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.16.0.tgz",
+      "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
+      "dev": true,
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
+      "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.16.0.tgz",
+      "integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
+      "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.16.0",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@ungap/structured-clone": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+      "dev": true
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.1.0.tgz",
+      "integrity": "sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==",
+      "dev": true,
+      "dependencies": {
+        "@volar/source-map": "2.4.0-alpha.18"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==",
+      "dev": true
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==",
+      "dev": true,
+      "dependencies": {
+        "@volar/language-core": "2.4.0-alpha.18",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/babel-helper-vue-transform-on": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
+      "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/babel-plugin-jsx": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
+      "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/plugin-syntax-jsx": "^7.27.1",
+        "@babel/template": "^7.27.2",
+        "@babel/traverse": "^7.28.0",
+        "@babel/types": "^7.28.2",
+        "@vue/babel-helper-vue-transform-on": "1.5.0",
+        "@vue/babel-plugin-resolve-type": "1.5.0",
+        "@vue/shared": "^3.5.18"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/babel-plugin-jsx/node_modules/@vue/shared": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
+      "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/babel-plugin-resolve-type": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
+      "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/parser": "^7.28.0",
+        "@vue/compiler-sfc": "^3.5.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-core": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
+      "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.31",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-dom": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
+      "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.31",
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-sfc": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
+      "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.31",
+        "@vue/compiler-dom": "3.5.31",
+        "@vue/compiler-ssr": "3.5.31",
+        "@vue/shared": "3.5.31",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-ssr": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
+      "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.31",
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/shared": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
+      "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/babel-plugin-resolve-type/node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
+      "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
+      "dependencies": {
+        "@babel/parser": "^7.27.5",
+        "@vue/shared": "3.5.17",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
+      "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
+      "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
+      "dependencies": {
+        "@babel/parser": "^7.27.5",
+        "@vue/compiler-core": "3.5.17",
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.17",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
+      "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.2.tgz",
+      "integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.2"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz",
+      "integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.2",
+        "birpc": "^0.2.19",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.1"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz",
+      "integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/eslint-config-typescript": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
+      "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "^6.7.0",
+        "@typescript-eslint/parser": "^6.7.0",
+        "vue-eslint-parser": "^9.3.1"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0",
+        "eslint-plugin-vue": "^9.0.0",
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "2.0.29",
+      "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.0.29.tgz",
+      "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
+      "dev": true,
+      "dependencies": {
+        "@volar/language-core": "~2.4.0-alpha.18",
+        "@vue/compiler-dom": "^3.4.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.4.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.17.tgz",
+      "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
+      "dependencies": {
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
+      "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
+      "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/runtime-core": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
+      "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17"
+      },
+      "peerDependencies": {
+        "vue": "3.5.17"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.17.tgz",
+      "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
+      "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
+      "dev": true
+    },
+    "node_modules/@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/acorn": {
+      "version": "8.11.3",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.11.3.tgz",
+      "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "node_modules/animate.css": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
+      "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+    },
+    "node_modules/array-ify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz",
+      "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+      "dev": true
+    },
+    "node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.7.9",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz",
+      "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.13",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
+      "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "0.2.19",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-0.2.19.tgz",
+      "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase-keys": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmmirror.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+      "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+      "dev": true,
+      "dependencies": {
+        "camelcase": "^5.3.1",
+        "map-obj": "^4.0.0",
+        "quick-lru": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001782",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz",
+      "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/ci-info": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz",
+      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/compare-func": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz",
+      "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+      "dev": true,
+      "dependencies": {
+        "array-ify": "^1.0.0",
+        "dot-prop": "^5.1.0"
+      }
+    },
+    "node_modules/computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "node_modules/conventional-changelog-angular": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz",
+      "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==",
+      "dev": true,
+      "dependencies": {
+        "compare-func": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/conventional-changelog-conventionalcommits": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz",
+      "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==",
+      "dev": true,
+      "dependencies": {
+        "compare-func": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/conventional-commits-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
+      "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==",
+      "dev": true,
+      "dependencies": {
+        "is-text-path": "^2.0.0",
+        "JSONStream": "^1.3.5",
+        "meow": "^12.0.1",
+        "split2": "^4.0.0"
+      },
+      "bin": {
+        "conventional-commits-parser": "cli.mjs"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
+      "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+      "dependencies": {
+        "is-what": "^4.1.8"
+      },
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-js-pure": {
+      "version": "3.47.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz",
+      "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==",
+      "hasInstallScript": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+    },
+    "node_modules/cosmiconfig": {
+      "version": "8.3.6",
+      "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
+      "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+      "dev": true,
+      "dependencies": {
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0",
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.9.5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/cosmiconfig-typescript-loader": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz",
+      "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==",
+      "dev": true,
+      "dependencies": {
+        "jiti": "^1.19.1"
+      },
+      "engines": {
+        "node": ">=v16"
+      },
+      "peerDependencies": {
+        "@types/node": "*",
+        "cosmiconfig": ">=8.2",
+        "typescript": ">=4"
+      }
+    },
+    "node_modules/cosmiconfig/node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true
+    },
+    "node_modules/cosmiconfig/node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true
+    },
+    "node_modules/cosmiconfig/node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+    },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+    },
+    "node_modules/dargs": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/dargs/-/dargs-7.0.0.tgz",
+      "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/decamelize-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+      "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
+      "dev": true,
+      "dependencies": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/decamelize-keys/node_modules/map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
+    "node_modules/dexie": {
+      "version": "4.0.11",
+      "resolved": "https://registry.npmmirror.com/dexie/-/dexie-4.0.11.tgz",
+      "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A=="
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/dot-prop": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz",
+      "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+      "dev": true,
+      "dependencies": {
+        "is-obj": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/echarts": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "node_modules/echarts-wordcloud": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
+      "integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
+      "license": "ISC",
+      "peerDependencies": {
+        "echarts": "^5.0.1"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.330",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz",
+      "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "8.56.0",
+      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.56.0.tgz",
+      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.56.0",
+        "@humanwhocodes/config-array": "^0.11.13",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-vue": {
+      "version": "9.19.2",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz",
+      "integrity": "sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.13",
+        "semver": "^7.5.4",
+        "vue-eslint-parser": "^9.3.1",
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.0",
+        "human-signals": "^2.1.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.1",
+        "onetime": "^5.1.2",
+        "signal-exit": "^3.0.3",
+        "strip-final-newline": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "node_modules/fastq": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.16.0.tgz",
+      "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/find-yarn-workspace-root": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+      "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "micromatch": "^4.0.2"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.9.tgz",
+      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "dev": true
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.2.0",
+      "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz",
+      "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz",
+      "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==",
+      "dev": true,
+      "dependencies": {
+        "dargs": "^7.0.0",
+        "lodash": "^4.17.15",
+        "meow": "^8.0.0",
+        "split2": "^3.0.0",
+        "through2": "^4.0.0"
+      },
+      "bin": {
+        "git-raw-commits": "cli.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/hosted-git-info": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+      "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/meow": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmmirror.com/meow/-/meow-8.1.2.tgz",
+      "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/minimist": "^1.2.0",
+        "camelcase-keys": "^6.2.2",
+        "decamelize-keys": "^1.1.0",
+        "hard-rejection": "^2.1.0",
+        "minimist-options": "4.1.0",
+        "normalize-package-data": "^3.0.0",
+        "read-pkg-up": "^7.0.1",
+        "redent": "^3.0.0",
+        "trim-newlines": "^3.0.0",
+        "type-fest": "^0.18.0",
+        "yargs-parser": "^20.2.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/normalize-package-data": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+      "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+      "dev": true,
+      "dependencies": {
+        "hosted-git-info": "^4.0.1",
+        "is-core-module": "^2.5.0",
+        "semver": "^7.3.4",
+        "validate-npm-package-license": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/split2": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmmirror.com/split2/-/split2-3.2.2.tgz",
+      "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
+      "dev": true,
+      "dependencies": {
+        "readable-stream": "^3.0.0"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/through2": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/through2/-/through2-4.0.2.tgz",
+      "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+      "dev": true,
+      "dependencies": {
+        "readable-stream": "3"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/type-fest": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.18.1.tgz",
+      "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/git-raw-commits/node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/global-dirs": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmmirror.com/global-dirs/-/global-dirs-0.1.1.tgz",
+      "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==",
+      "dev": true,
+      "dependencies": {
+        "ini": "^1.3.4"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true
+    },
+    "node_modules/hard-rejection": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/hard-rejection/-/hard-rejection-2.1.0.tgz",
+      "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/hfmath": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmmirror.com/hfmath/-/hfmath-0.0.2.tgz",
+      "integrity": "sha512-cKUi0yiQLGfLgs8+3Iw5nAiqSH13Knp7vCf0G1vlF5nfiKKO1XmxNagMvyp0F4ZvUNaHpRGTkmc7nowCy1S58g=="
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+    },
+    "node_modules/hosted-git-info": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+      "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^10.0.1"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/html-to-image": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.13.tgz",
+      "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="
+    },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/https": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/https/-/https-1.0.0.tgz",
+      "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="
+    },
+    "node_modules/human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.17.0"
+      }
+    },
+    "node_modules/husky": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz",
+      "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
+      "dev": true,
+      "bin": {
+        "husky": "lib/bin.js"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.0.tgz",
+      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/image-size/-/image-size-1.1.0.tgz",
+      "integrity": "sha512-asnTHw2K8OlqT5kVnQwX+AGKQqpvLo95LbNzQ/C0ln3yzentZmAdd0ygoD004VC4Kkd4PV7J2iaPQkqwp9yuTw==",
+      "dependencies": {
+        "queue": "6.0.2"
+      },
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+    },
+    "node_modules/immutable": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz",
+      "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
+      "dev": true
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "dev": true
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "dev": true,
+      "dependencies": {
+        "hasown": "^2.0.0"
+      }
+    },
+    "node_modules/is-docker": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz",
+      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz",
+      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-text-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-text-path/-/is-text-path-2.0.0.tgz",
+      "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==",
+      "dev": true,
+      "dependencies": {
+        "text-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "4.1.16",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
+      "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz",
+      "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/isomorphic.js": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
+      "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
+      "funding": {
+        "type": "GitHub Sponsors ❤",
+        "url": "https://github.com/sponsors/dmonad"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.0",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "dev": true,
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz",
+      "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==",
+      "dev": true,
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/json-stable-stringify": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+      "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "isarray": "^2.0.5",
+        "jsonify": "^0.0.1",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "node_modules/json-stable-stringify/node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/jsonify/-/jsonify-0.0.1.tgz",
+      "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+      "license": "Public Domain",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+      "dev": true,
+      "engines": [
+        "node >= 0.2.0"
+      ]
+    },
+    "node_modules/JSONStream": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmmirror.com/JSONStream/-/JSONStream-1.3.5.tgz",
+      "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+      "dev": true,
+      "dependencies": {
+        "jsonparse": "^1.2.0",
+        "through": ">=2.2.7 <3"
+      },
+      "bin": {
+        "JSONStream": "bin.js"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "node_modules/katex": {
+      "version": "0.16.22",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
+      "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
+      "funding": [
+        "https://opencollective.com/katex",
+        "https://github.com/sponsors/katex"
+      ],
+      "dependencies": {
+        "commander": "^8.3.0"
+      },
+      "bin": {
+        "katex": "cli.js"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/klaw-sync": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/klaw-sync/-/klaw-sync-6.0.0.tgz",
+      "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.11"
+      }
+    },
+    "node_modules/kolorist": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+      "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lib0": {
+      "version": "0.2.114",
+      "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
+      "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
+      "dependencies": {
+        "isomorphic.js": "^0.2.4"
+      },
+      "bin": {
+        "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
+        "0gentesthtml": "bin/gentesthtml.js",
+        "0serve": "bin/0serve.js"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "type": "GitHub Sponsors ❤",
+        "url": "https://github.com/sponsors/dmonad"
+      }
+    },
+    "node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz",
+      "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      }
+    },
+    "node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "dev": true
+    },
+    "node_modules/lodash.isfunction": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+      "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
+      "dev": true
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "dev": true
+    },
+    "node_modules/lodash.kebabcase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+      "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+      "dev": true
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "node_modules/lodash.mergewith": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+      "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+      "dev": true
+    },
+    "node_modules/lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+      "dev": true
+    },
+    "node_modules/lodash.startcase": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+      "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+      "dev": true
+    },
+    "node_modules/lodash.uniq": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+      "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+      "dev": true
+    },
+    "node_modules/lodash.upperfirst": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmmirror.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+      "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+      "dev": true
+    },
+    "node_modules/lru-cache": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.1.0.tgz",
+      "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+      "dev": true,
+      "engines": {
+        "node": "14 || >=16.14"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/map-obj": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-4.3.0.tgz",
+      "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/markdown-it": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
+    },
+    "node_modules/memorystream": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/meow": {
+      "version": "12.1.1",
+      "resolved": "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz",
+      "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+      "dev": true,
+      "engines": {
+        "node": ">=16.10"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz",
+      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "dependencies": {
+        "braces": "^3.0.2",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
+    },
+    "node_modules/minimist-options": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/minimist-options/-/minimist-options-4.1.0.tgz",
+      "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+      "dev": true,
+      "dependencies": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0",
+        "kind-of": "^6.0.3"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+    },
+    "node_modules/mp4-muxer": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/mp4-muxer/-/mp4-muxer-2.3.0.tgz",
+      "integrity": "sha512-eupg1QMfcVJy/nZ2smUbC4OcEKdKBaLxKe+oeoDlklB5Dkb+4mv4CdPVYtzo1ZnzBKuVCclDk/znQwFQJIC32g==",
+      "deprecated": "This library is superseded by Mediabunny. Please migrate to it.",
+      "license": "MIT",
+      "dependencies": {
+        "@types/dom-webcodecs": "^0.1.6",
+        "@types/wicg-file-system-access": "^2020.9.5"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true
+    },
+    "node_modules/nanoid": {
+      "version": "5.0.7",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
+      "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.36",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+      "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-package-data": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
+      "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
+      "dev": true,
+      "dependencies": {
+        "hosted-git-info": "^7.0.0",
+        "is-core-module": "^2.8.1",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/npm-run-all2": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-6.1.1.tgz",
+      "integrity": "sha512-lWLbkPZ5BSdXtN8lR+0rc8caKoPdymycpZksyDEC9MOBvfdwTXZ0uVhb7bMcGeXv2/BKtfQuo6Zn3zfc8rxNXA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "cross-spawn": "^7.0.3",
+        "memorystream": "^0.3.1",
+        "minimatch": "^9.0.0",
+        "pidtree": "^0.6.0",
+        "read-pkg": "^8.0.0",
+        "shell-quote": "^1.7.3"
+      },
+      "bin": {
+        "npm-run-all": "bin/npm-run-all/index.js",
+        "npm-run-all2": "bin/npm-run-all/index.js",
+        "run-p": "bin/run-p/index.js",
+        "run-s": "bin/run-s/index.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0",
+        "npm": ">= 8"
+      }
+    },
+    "node_modules/npm-run-all2/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      }
+    },
+    "node_modules/number-precision": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz",
+      "integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ=="
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/open": {
+      "version": "7.4.2",
+      "resolved": "https://registry.npmmirror.com/open/-/open-7.4.2.tgz",
+      "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0",
+        "is-wsl": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "dev": true,
+      "dependencies": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/orderedmap": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz",
+      "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-7.1.1.tgz",
+      "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.21.4",
+        "error-ex": "^1.3.2",
+        "json-parse-even-better-errors": "^3.0.0",
+        "lines-and-columns": "^2.0.3",
+        "type-fest": "^3.8.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/parse-json/node_modules/type-fest": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-3.13.1.tgz",
+      "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
+    "node_modules/patch-package": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/patch-package/-/patch-package-8.0.1.tgz",
+      "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
+      "license": "MIT",
+      "dependencies": {
+        "@yarnpkg/lockfile": "^1.1.0",
+        "chalk": "^4.1.2",
+        "ci-info": "^3.7.0",
+        "cross-spawn": "^7.0.3",
+        "find-yarn-workspace-root": "^2.0.0",
+        "fs-extra": "^10.0.0",
+        "json-stable-stringify": "^1.0.2",
+        "klaw-sync": "^6.0.0",
+        "minimist": "^1.2.6",
+        "open": "^7.4.2",
+        "semver": "^7.5.3",
+        "slash": "^2.0.0",
+        "tmp": "^0.2.4",
+        "yaml": "^2.2.2"
+      },
+      "bin": {
+        "patch-package": "index.js"
+      },
+      "engines": {
+        "node": ">=14",
+        "npm": ">5"
+      }
+    },
+    "node_modules/patch-package/node_modules/fs-extra": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/patch-package/node_modules/slash": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/slash/-/slash-2.0.0.tgz",
+      "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/pidtree": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz",
+      "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+      "dev": true,
+      "bin": {
+        "pidtree": "bin/pidtree.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.2.tgz",
+      "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.4.4",
+        "vue": "^2.7.0 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.0.15",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
+      "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+      "dev": true,
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss/node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/pptxgenjs": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmmirror.com/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
+      "integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
+      "dependencies": {
+        "@types/node": "^18.7.3",
+        "https": "^1.0.0",
+        "image-size": "^1.0.0",
+        "jszip": "^3.7.1"
+      }
+    },
+    "node_modules/pptxtojson": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmmirror.com/pptxtojson/-/pptxtojson-1.12.0.tgz",
+      "integrity": "sha512-kXdCqXlCLdiGpM3womaQIpGYrQEzl8/jdxSpWWtYNj3Pxvr9n39tGpuBxeZSdenttwzM7Fb/yBKUPzxMFXLIJQ==",
+      "license": "MIT",
+      "dependencies": {
+        "jszip": "^3.10.1",
+        "tinycolor2": "1.6.0",
+        "txml": "^5.1.1"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
+    "node_modules/prosemirror-commands": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
+      "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
+      "dependencies": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.0.0"
+      }
+    },
+    "node_modules/prosemirror-dropcursor": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
+      "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
+      "dependencies": {
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.1.0",
+        "prosemirror-view": "^1.1.0"
+      }
+    },
+    "node_modules/prosemirror-gapcursor": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
+      "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
+      "dependencies": {
+        "prosemirror-keymap": "^1.0.0",
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-view": "^1.0.0"
+      }
+    },
+    "node_modules/prosemirror-history": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz",
+      "integrity": "sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==",
+      "dependencies": {
+        "prosemirror-state": "^1.2.2",
+        "prosemirror-transform": "^1.0.0",
+        "prosemirror-view": "^1.31.0",
+        "rope-sequence": "^1.3.0"
+      }
+    },
+    "node_modules/prosemirror-inputrules": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
+      "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
+      "dependencies": {
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.0.0"
+      }
+    },
+    "node_modules/prosemirror-keymap": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
+      "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
+      "dependencies": {
+        "prosemirror-state": "^1.0.0",
+        "w3c-keyname": "^2.2.0"
+      }
+    },
+    "node_modules/prosemirror-model": {
+      "version": "1.22.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-model/-/prosemirror-model-1.22.2.tgz",
+      "integrity": "sha512-I4lS7HHIW47D0Xv/gWmi4iUWcQIDYaJKd8Hk4+lcSps+553FlQrhmxtItpEvTr75iAruhzVShVp6WUwsT6Boww==",
+      "dependencies": {
+        "orderedmap": "^2.0.0"
+      }
+    },
+    "node_modules/prosemirror-schema-basic": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz",
+      "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==",
+      "dependencies": {
+        "prosemirror-model": "^1.19.0"
+      }
+    },
+    "node_modules/prosemirror-schema-list": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
+      "integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
+      "dependencies": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.7.3"
+      }
+    },
+    "node_modules/prosemirror-state": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmmirror.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
+      "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
+      "dependencies": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-transform": "^1.0.0",
+        "prosemirror-view": "^1.27.0"
+      }
+    },
+    "node_modules/prosemirror-transform": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
+      "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
+      "dependencies": {
+        "prosemirror-model": "^1.0.0"
+      }
+    },
+    "node_modules/prosemirror-view": {
+      "version": "1.33.9",
+      "resolved": "https://registry.npmmirror.com/prosemirror-view/-/prosemirror-view-1.33.9.tgz",
+      "integrity": "sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==",
+      "dependencies": {
+        "prosemirror-model": "^1.20.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.1.0"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/queue/-/queue-6.0.2.tgz",
+      "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+      "dependencies": {
+        "inherits": "~2.0.3"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
+    "node_modules/quick-lru": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz",
+      "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-8.1.0.tgz",
+      "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.1",
+        "normalize-package-data": "^6.0.0",
+        "parse-json": "^7.0.0",
+        "type-fest": "^4.2.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "dev": true
+    },
+    "node_modules/read-pkg-up/node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true
+    },
+    "node_modules/read-pkg-up/node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true
+    },
+    "node_modules/read-pkg-up/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "dependencies": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "dev": true,
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz",
+      "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.9.0.tgz",
+      "integrity": "sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/redent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+      "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+      "dev": true,
+      "dependencies": {
+        "indent-string": "^4.0.0",
+        "strip-indent": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/resolve-global": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-global/-/resolve-global-1.0.0.tgz",
+      "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==",
+      "dev": true,
+      "dependencies": {
+        "global-dirs": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.19.0.tgz",
+      "integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.5"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.19.0",
+        "@rollup/rollup-android-arm64": "4.19.0",
+        "@rollup/rollup-darwin-arm64": "4.19.0",
+        "@rollup/rollup-darwin-x64": "4.19.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.19.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.19.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.19.0",
+        "@rollup/rollup-linux-arm64-musl": "4.19.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.19.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.19.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.19.0",
+        "@rollup/rollup-linux-x64-gnu": "4.19.0",
+        "@rollup/rollup-linux-x64-musl": "4.19.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.19.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.19.0",
+        "@rollup/rollup-win32-x64-msvc": "4.19.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/rope-sequence": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz",
+      "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
+    },
+    "node_modules/rtf.js": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/rtf.js/-/rtf.js-3.0.9.tgz",
+      "integrity": "sha512-I1GpDat4i548WzmeZXv27f/743984fvEeeBS8BC01/Sop17pMlUl3M7DYcdcB3PUvOZTrFIMxGZx8qw7cSMAKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "codepage": "^1.15.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "node_modules/sass": {
+      "version": "1.69.6",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.6.tgz",
+      "integrity": "sha512-qbRr3k9JGHWXCvZU77SD2OTwUlC+gNT+61JOLcmLm+XqH4h/5D+p4IIsxvpkB89S9AwJOyb5+rWNpIucaFxSFQ==",
+      "dev": true,
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
+    "node_modules/semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sortablejs": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
+      "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+      "dev": true
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "dev": true
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "dev": true,
+      "dependencies": {
+        "min-indent": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
+      "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
+      "dependencies": {
+        "copy-anything": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/svg-arc-to-cubic-bezier": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
+      "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
+    },
+    "node_modules/svg-pathdata": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-7.2.0.tgz",
+      "integrity": "sha512-qd+AxqMpfRrRQaWb2SrNFvn69cvl6piqY8TxhYl2Li1g4/LO5F9NJb5wI4vNwRryqgSgD43gYKLm/w3ag1bKvQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.11.1"
+      }
+    },
+    "node_modules/text-extensions": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/text-extensions/-/text-extensions-2.4.0.tgz",
+      "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
+    },
+    "node_modules/through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
+      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "dev": true
+    },
+    "node_modules/through2": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/through2/-/through2-3.0.2.tgz",
+      "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==",
+      "dependencies": {
+        "inherits": "^2.0.4",
+        "readable-stream": "2 || 3"
+      }
+    },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "node_modules/tinycolor2": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
+      "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
+    },
+    "node_modules/tippy.js": {
+      "version": "6.3.7",
+      "resolved": "https://registry.npmmirror.com/tippy.js/-/tippy.js-6.3.7.tgz",
+      "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
+      "dependencies": {
+        "@popperjs/core": "^2.9.0"
+      }
+    },
+    "node_modules/tmp": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/trim-newlines": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz",
+      "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true,
+      "engines": {
+        "node": ">=16.13.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
+    "node_modules/txml": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
+      "integrity": "sha512-TwMDLnXQ09enNaxybLVvKZU7rqog8LgnuAs4ZYXM0nV0eu10iLsSFwlX3AEknAXXtH1wT3CYfoiXAjyBexcmuw==",
+      "dependencies": {
+        "through2": "^3.0.1"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.3.3.tgz",
+      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "devOptional": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/utif": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
+      "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pako": "^1.0.5"
+      }
+    },
+    "node_modules/utif2": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
+      "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
+      "license": "MIT",
+      "dependencies": {
+        "pako": "^1.0.11"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "bin": {
+        "uuid": "dist-node/bin/uuid"
+      }
+    },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.3.5",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.3.5.tgz",
+      "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.39",
+        "rollup": "^4.13.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "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
+        }
+      }
+    },
+    "node_modules/vite-plugin-vue-inspector": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.4.0.tgz",
+      "integrity": "sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.23.0",
+        "@babel/plugin-proposal-decorators": "^7.23.0",
+        "@babel/plugin-syntax-import-attributes": "^7.22.5",
+        "@babel/plugin-syntax-import-meta": "^7.10.4",
+        "@babel/plugin-transform-typescript": "^7.22.15",
+        "@vue/babel-plugin-jsx": "^1.1.5",
+        "@vue/compiler-dom": "^3.3.4",
+        "kolorist": "^1.8.0",
+        "magic-string": "^0.30.4"
+      },
+      "peerDependencies": {
+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz",
+      "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+      "dev": true
+    },
+    "node_modules/vue": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.17.tgz",
+      "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-sfc": "3.5.17",
+        "@vue/runtime-dom": "3.5.17",
+        "@vue/server-renderer": "3.5.17",
+        "@vue/shared": "3.5.17"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-eslint-parser": {
+      "version": "9.3.2",
+      "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
+      "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=6.0.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "2.0.29",
+      "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.0.29.tgz",
+      "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
+      "dev": true,
+      "dependencies": {
+        "@volar/typescript": "~2.4.0-alpha.18",
+        "@vue/language-core": "2.0.29",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/vuedraggable": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
+      "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+      "dependencies": {
+        "sortablejs": "1.14.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.1"
+      }
+    },
+    "node_modules/w3c-keyname": {
+      "version": "2.2.8",
+      "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+    },
+    "node_modules/wangeditor": {
+      "version": "4.7.15",
+      "resolved": "https://registry.npmjs.org/wangeditor/-/wangeditor-4.7.15.tgz",
+      "integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@babel/runtime-corejs3": "^7.11.2",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/webcodecs-encoder": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmmirror.com/webcodecs-encoder/-/webcodecs-encoder-0.3.2.tgz",
+      "integrity": "sha512-F4XKzafAxVNhtnaQ5F8Q6xVn+bRrzLiUQs3p4PpogN3svBqGPR1y5OK66bunXEULOJ0XVJ0HqlJ/TSGGZTZ0AQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "mp4-muxer": "^2.0.2",
+        "patch-package": "^8.0.0",
+        "webm-muxer": "^1.2.0"
+      }
+    },
+    "node_modules/webm-muxer": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/webm-muxer/-/webm-muxer-1.2.1.tgz",
+      "integrity": "sha512-FaH7GVeK9tOZoWEW0gT/ryR9EoZnDe/RplJlTkG6zPWfEdrIy24PaXwEiyUnKwwO3zsjJWzFq4DLHECqGdJXIw==",
+      "deprecated": "This library is superseded by Mediabunny. Please migrate to it.",
+      "license": "MIT",
+      "dependencies": {
+        "@types/dom-webcodecs": "^0.1.4",
+        "@types/wicg-file-system-access": "^2020.9.5"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "node_modules/xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/y-protocols": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
+      "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==",
+      "dependencies": {
+        "lib0": "^0.2.85"
+      },
+      "engines": {
+        "node": ">=16.0.0",
+        "npm": ">=8.0.0"
+      },
+      "funding": {
+        "type": "GitHub Sponsors ❤",
+        "url": "https://github.com/sponsors/dmonad"
+      },
+      "peerDependencies": {
+        "yjs": "^13.0.0"
+      }
+    },
+    "node_modules/y-websocket": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz",
+      "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==",
+      "dependencies": {
+        "lib0": "^0.2.102",
+        "y-protocols": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=16.0.0",
+        "npm": ">=8.0.0"
+      },
+      "funding": {
+        "type": "GitHub Sponsors ❤",
+        "url": "https://github.com/sponsors/dmonad"
+      },
+      "peerDependencies": {
+        "yjs": "^13.5.6"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/yaml": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz",
+      "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yjs": {
+      "version": "13.6.27",
+      "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
+      "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
+      "dependencies": {
+        "lib0": "^0.2.99"
+      },
+      "engines": {
+        "node": ">=16.0.0",
+        "npm": ">=8.0.0"
+      },
+      "funding": {
+        "type": "GitHub Sponsors ❤",
+        "url": "https://github.com/sponsors/dmonad"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    }
+  },
+  "dependencies": {
+    "@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmmirror.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true
+    },
+    "@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      }
+    },
+    "@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true
+    },
+    "@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "requires": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      }
+    },
+    "@babel/helper-annotate-as-pure": {
+      "version": "7.27.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+      "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.27.3"
+      }
+    },
+    "@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "requires": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+          "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+          "dev": true,
+          "requires": {
+            "yallist": "^3.0.2"
+          }
+        },
+        "semver": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+          "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-create-class-features-plugin": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+      "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/helper-replace-supers": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/traverse": "^7.28.6",
+        "semver": "^6.3.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true
+    },
+    "@babel/helper-member-expression-to-functions": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+      "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+      "dev": true,
+      "requires": {
+        "@babel/traverse": "^7.28.5",
+        "@babel/types": "^7.28.5"
+      }
+    },
+    "@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "requires": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      }
+    },
+    "@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      }
+    },
+    "@babel/helper-optimise-call-expression": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+      "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.27.1"
+      }
+    },
+    "@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true
+    },
+    "@babel/helper-replace-supers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+      "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/traverse": "^7.28.6"
+      }
+    },
+    "@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+      "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+      "dev": true,
+      "requires": {
+        "@babel/traverse": "^7.27.1",
+        "@babel/types": "^7.27.1"
+      }
+    },
+    "@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
+    },
+    "@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true
+    },
+    "@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "requires": {
+        "@babel/types": "^7.29.0"
+      }
+    },
+    "@babel/plugin-proposal-decorators": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz",
+      "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/plugin-syntax-decorators": "^7.28.6"
+      }
+    },
+    "@babel/plugin-syntax-decorators": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
+      "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      }
+    },
+    "@babel/plugin-syntax-import-attributes": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+      "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      }
+    },
+    "@babel/plugin-syntax-import-meta": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      }
+    },
+    "@babel/plugin-syntax-jsx": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+      "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      }
+    },
+    "@babel/plugin-syntax-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+      "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      }
+    },
+    "@babel/plugin-transform-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+      "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/plugin-syntax-typescript": "^7.28.6"
+      }
+    },
+    "@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="
+    },
+    "@babel/runtime-corejs3": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
+      "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==",
+      "requires": {
+        "core-js-pure": "^3.43.0"
+      }
+    },
+    "@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      }
+    },
+    "@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "requires": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      }
+    },
+    "@commitlint/cli": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-18.4.3.tgz",
+      "integrity": "sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==",
+      "dev": true,
+      "requires": {
+        "@commitlint/format": "^18.4.3",
+        "@commitlint/lint": "^18.4.3",
+        "@commitlint/load": "^18.4.3",
+        "@commitlint/read": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "execa": "^5.0.0",
+        "lodash.isfunction": "^3.0.9",
+        "resolve-from": "5.0.0",
+        "resolve-global": "1.0.0",
+        "yargs": "^17.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+          "dev": true
+        }
+      }
+    },
+    "@commitlint/config-conventional": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-18.4.3.tgz",
+      "integrity": "sha512-729eRRaNta7JZF07qf6SAGSghoDEp9mH7yHU0m7ff0q89W97wDrWCyZ3yoV3mcQJwbhlmVmZPTkPcm7qiAu8WA==",
+      "dev": true,
+      "requires": {
+        "conventional-changelog-conventionalcommits": "^7.0.2"
+      }
+    },
+    "@commitlint/config-validator": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-18.4.3.tgz",
+      "integrity": "sha512-FPZZmTJBARPCyef9ohRC9EANiQEKSWIdatx5OlgeHKu878dWwpyeFauVkhzuBRJFcCA4Uvz/FDtlDKs008IHcA==",
+      "dev": true,
+      "requires": {
+        "@commitlint/types": "^18.4.3",
+        "ajv": "^8.11.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.12.0",
+          "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz",
+          "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        }
+      }
+    },
+    "@commitlint/ensure": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-18.4.3.tgz",
+      "integrity": "sha512-MI4fwD9TWDVn4plF5+7JUyLLbkOdzIRBmVeNlk4dcGlkrVA+/l5GLcpN66q9LkFsFv6G2X31y89ApA3hqnqIFg==",
+      "dev": true,
+      "requires": {
+        "@commitlint/types": "^18.4.3",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.kebabcase": "^4.1.1",
+        "lodash.snakecase": "^4.1.1",
+        "lodash.startcase": "^4.4.0",
+        "lodash.upperfirst": "^4.3.1"
+      }
+    },
+    "@commitlint/execute-rule": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-18.4.3.tgz",
+      "integrity": "sha512-t7FM4c+BdX9WWZCPrrbV5+0SWLgT3kCq7e7/GhHCreYifg3V8qyvO127HF796vyFql75n4TFF+5v1asOOWkV1Q==",
+      "dev": true
+    },
+    "@commitlint/format": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/format/-/format-18.4.3.tgz",
+      "integrity": "sha512-8b+ItXYHxAhRAXFfYki5PpbuMMOmXYuzLxib65z2XTqki59YDQJGpJ/wB1kEE5MQDgSTQWtKUrA8n9zS/1uIDQ==",
+      "dev": true,
+      "requires": {
+        "@commitlint/types": "^18.4.3",
+        "chalk": "^4.1.0"
+      }
+    },
+    "@commitlint/is-ignored": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-18.4.3.tgz",
+      "integrity": "sha512-ZseOY9UfuAI32h9w342Km4AIaTieeFskm2ZKdrG7r31+c6zGBzuny9KQhwI9puc0J3GkUquEgKJblCl7pMnjwg==",
+      "dev": true,
+      "requires": {
+        "@commitlint/types": "^18.4.3",
+        "semver": "7.5.4"
+      }
+    },
+    "@commitlint/lint": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/lint/-/lint-18.4.3.tgz",
+      "integrity": "sha512-18u3MRgEXNbnYkMOWoncvq6QB8/90m9TbERKgdPqVvS+zQ/MsuRhdvHYCIXGXZxUb0YI4DV2PC4bPneBV/fYuA==",
+      "dev": true,
+      "requires": {
+        "@commitlint/is-ignored": "^18.4.3",
+        "@commitlint/parse": "^18.4.3",
+        "@commitlint/rules": "^18.4.3",
+        "@commitlint/types": "^18.4.3"
+      }
+    },
+    "@commitlint/load": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/load/-/load-18.4.3.tgz",
+      "integrity": "sha512-v6j2WhvRQJrcJaj5D+EyES2WKTxPpxENmNpNG3Ww8MZGik3jWRXtph0QTzia5ZJyPh2ib5aC/6BIDymkUUM58Q==",
+      "dev": true,
+      "requires": {
+        "@commitlint/config-validator": "^18.4.3",
+        "@commitlint/execute-rule": "^18.4.3",
+        "@commitlint/resolve-extends": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "@types/node": "^18.11.9",
+        "chalk": "^4.1.0",
+        "cosmiconfig": "^8.3.6",
+        "cosmiconfig-typescript-loader": "^5.0.0",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.merge": "^4.6.2",
+        "lodash.uniq": "^4.5.0",
+        "resolve-from": "^5.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+          "dev": true
+        }
+      }
+    },
+    "@commitlint/message": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/message/-/message-18.4.3.tgz",
+      "integrity": "sha512-ddJ7AztWUIoEMAXoewx45lKEYEOeOlBVWjk8hDMUGpprkuvWULpaXczqdjwVtjrKT3JhhN+gMs8pm5G3vB2how==",
+      "dev": true
+    },
+    "@commitlint/parse": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/parse/-/parse-18.4.3.tgz",
+      "integrity": "sha512-eoH7CXM9L+/Me96KVcfJ27EIIbA5P9sqw3DqjJhRYuhaULIsPHFs5S5GBDCqT0vKZQDx0DgxhMpW6AQbnKrFtA==",
+      "dev": true,
+      "requires": {
+        "@commitlint/types": "^18.4.3",
+        "conventional-changelog-angular": "^7.0.0",
+        "conventional-commits-parser": "^5.0.0"
+      }
+    },
+    "@commitlint/read": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/read/-/read-18.4.3.tgz",
+      "integrity": "sha512-H4HGxaYA6OBCimZAtghL+B+SWu8ep4X7BwgmedmqWZRHxRLcX2q0bWBtUm5FsMbluxbOfrJwOs/Z0ah4roP/GQ==",
+      "dev": true,
+      "requires": {
+        "@commitlint/top-level": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "fs-extra": "^11.0.0",
+        "git-raw-commits": "^2.0.11",
+        "minimist": "^1.2.6"
+      }
+    },
+    "@commitlint/resolve-extends": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-18.4.3.tgz",
+      "integrity": "sha512-30sk04LZWf8+SDgJrbJCjM90gTg2LxsD9cykCFeFu+JFHvBFq5ugzp2eO/DJGylAdVaqxej3c7eTSE64hR/lnw==",
+      "dev": true,
+      "requires": {
+        "@commitlint/config-validator": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "import-fresh": "^3.0.0",
+        "lodash.mergewith": "^4.6.2",
+        "resolve-from": "^5.0.0",
+        "resolve-global": "^1.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+          "dev": true
+        }
+      }
+    },
+    "@commitlint/rules": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/rules/-/rules-18.4.3.tgz",
+      "integrity": "sha512-8KIeukDf45BiY+Lul1T0imSNXF0sMrlLG6JpLLKolkmYVQ6PxxoNOriwyZ3UTFFpaVbPy0rcITaV7U9JCAfDTA==",
+      "dev": true,
+      "requires": {
+        "@commitlint/ensure": "^18.4.3",
+        "@commitlint/message": "^18.4.3",
+        "@commitlint/to-lines": "^18.4.3",
+        "@commitlint/types": "^18.4.3",
+        "execa": "^5.0.0"
+      }
+    },
+    "@commitlint/to-lines": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-18.4.3.tgz",
+      "integrity": "sha512-fy1TAleik4Zfru1RJ8ZU6cOSvgSVhUellxd3WZV1D5RwHZETt1sZdcA4mQN2y3VcIZsUNKkW0Mq8CM9/L9harQ==",
+      "dev": true
+    },
+    "@commitlint/top-level": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-18.4.3.tgz",
+      "integrity": "sha512-E6fJPBLPFL5R8+XUNSYkj4HekIOuGMyJo3mIx2PkYc3clel+pcWQ7TConqXxNWW4x1ugigiIY2RGot55qUq1hw==",
+      "dev": true,
+      "requires": {
+        "find-up": "^5.0.0"
+      }
+    },
+    "@commitlint/types": {
+      "version": "18.4.3",
+      "resolved": "https://registry.npmmirror.com/@commitlint/types/-/types-18.4.3.tgz",
+      "integrity": "sha512-cvzx+vtY/I2hVBZHCLrpoh+sA0hfuzHwDc+BAFPimYLjJkpHnghQM+z8W/KyLGkygJh3BtI3xXXq+dKjnSWEmA==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.0"
+      }
+    },
+    "@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "dev": true,
+      "optional": true
+    },
+    "@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^3.3.0"
+      }
+    },
+    "@eslint-community/regexpp": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+      "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+      "dev": true
+    },
+    "@eslint/eslintrc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+      "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "@eslint/js": {
+      "version": "8.56.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.56.0.tgz",
+      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+      "dev": true
+    },
+    "@humanwhocodes/config-array": {
+      "version": "0.11.13",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+      "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+      "dev": true,
+      "requires": {
+        "@humanwhocodes/object-schema": "^2.0.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true
+    },
+    "@humanwhocodes/object-schema": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+      "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+      "dev": true
+    },
+    "@icon-park/vue-next": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmmirror.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz",
+      "integrity": "sha512-+QklF255wkfBOabY+xw6FAI0Bwln/RhdwCunNy/9sKdKuChtaU67QZqU67KGAvZUTeeBgsL+yaHHxqfQeGZXEQ==",
+      "requires": {}
+    },
+    "@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true
+    },
+    "@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
+    },
+    "@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "@microsoft/fetch-event-source": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
+      "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@popperjs/core": {
+      "version": "2.11.8",
+      "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+    },
+    "@rollup/rollup-android-arm-eabi": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz",
+      "integrity": "sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-android-arm64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.0.tgz",
+      "integrity": "sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-arm64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz",
+      "integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-x64": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz",
+      "integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.0.tgz",
+      "integrity": "sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.0.tgz",
+      "integrity": "sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.0.tgz",
+      "integrity": "sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-musl": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.0.tgz",
+      "integrity": "sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.0.tgz",
+      "integrity": "sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.0.tgz",
+      "integrity": "sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.0.tgz",
+      "integrity": "sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-gnu": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz",
+      "integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-musl": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.0.tgz",
+      "integrity": "sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.0.tgz",
+      "integrity": "sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.0.tgz",
+      "integrity": "sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-x64-msvc": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.0.tgz",
+      "integrity": "sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==",
+      "dev": true,
+      "optional": true
+    },
+    "@rushstack/eslint-patch": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
+      "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
+      "dev": true
+    },
+    "@tsconfig/node18": {
+      "version": "18.2.2",
+      "resolved": "https://registry.npmmirror.com/@tsconfig/node18/-/node18-18.2.2.tgz",
+      "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==",
+      "dev": true
+    },
+    "@types/crypto-js": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.1.tgz",
+      "integrity": "sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==",
+      "dev": true
+    },
+    "@types/dom-webcodecs": {
+      "version": "0.1.18",
+      "resolved": "https://registry.npmmirror.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.18.tgz",
+      "integrity": "sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg=="
+    },
+    "@types/estree": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "dev": true
+    },
+    "@types/file-saver": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz",
+      "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+      "dev": true
+    },
+    "@types/html2canvas": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
+      "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
+      "dev": true,
+      "requires": {
+        "html2canvas": "*"
+      }
+    },
+    "@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true
+    },
+    "@types/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+      "dev": true
+    },
+    "@types/lodash": {
+      "version": "4.14.202",
+      "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.202.tgz",
+      "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
+      "dev": true
+    },
+    "@types/markdown-it": {
+      "version": "14.1.2",
+      "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+      "dev": true,
+      "requires": {
+        "@types/linkify-it": "^5",
+        "@types/mdurl": "^2"
+      }
+    },
+    "@types/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+      "dev": true
+    },
+    "@types/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/@types/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "18.19.4",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.4.tgz",
+      "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==",
+      "requires": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true
+    },
+    "@types/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+      "dev": true
+    },
+    "@types/semver": {
+      "version": "7.5.6",
+      "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.5.6.tgz",
+      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "dev": true
+    },
+    "@types/svg-arc-to-cubic-bezier": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmmirror.com/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.2.tgz",
+      "integrity": "sha512-XQtKy9lmkKlV+c3Jelo7kxNPw7qOqIq3GcnOhywGZHF7zw5D5m+Ssigbmf3Turbe/A8Ur+lRh8TYjuxXKvyivw==",
+      "dev": true
+    },
+    "@types/tinycolor2": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
+      "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==",
+      "dev": true
+    },
+    "@types/wicg-file-system-access": {
+      "version": "2020.9.8",
+      "resolved": "https://registry.npmmirror.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz",
+      "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A=="
+    },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
+      "integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/type-utils": "6.16.0",
+        "@typescript-eslint/utils": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.16.0.tgz",
+      "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4"
+      }
+    },
+    "@typescript-eslint/scope-manager": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
+      "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0"
+      }
+    },
+    "@typescript-eslint/type-utils": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
+      "integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "@typescript-eslint/utils": "6.16.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/types": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.16.0.tgz",
+      "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
+      "dev": true
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
+      "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/visitor-keys": "6.16.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/utils": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.16.0.tgz",
+      "integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.16.0",
+        "@typescript-eslint/types": "6.16.0",
+        "@typescript-eslint/typescript-estree": "6.16.0",
+        "semver": "^7.5.4"
+      }
+    },
+    "@typescript-eslint/visitor-keys": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
+      "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.16.0",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "@ungap/structured-clone": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+      "dev": true
+    },
+    "@vitejs/plugin-vue": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.1.0.tgz",
+      "integrity": "sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==",
+      "dev": true,
+      "requires": {}
+    },
+    "@volar/language-core": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==",
+      "dev": true,
+      "requires": {
+        "@volar/source-map": "2.4.0-alpha.18"
+      }
+    },
+    "@volar/source-map": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==",
+      "dev": true
+    },
+    "@volar/typescript": {
+      "version": "2.4.0-alpha.18",
+      "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz",
+      "integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "2.4.0-alpha.18",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "@vue/babel-helper-vue-transform-on": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
+      "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
+      "dev": true
+    },
+    "@vue/babel-plugin-jsx": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
+      "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/plugin-syntax-jsx": "^7.27.1",
+        "@babel/template": "^7.27.2",
+        "@babel/traverse": "^7.28.0",
+        "@babel/types": "^7.28.2",
+        "@vue/babel-helper-vue-transform-on": "1.5.0",
+        "@vue/babel-plugin-resolve-type": "1.5.0",
+        "@vue/shared": "^3.5.18"
+      },
+      "dependencies": {
+        "@vue/shared": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
+          "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+          "dev": true
+        }
+      }
+    },
+    "@vue/babel-plugin-resolve-type": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
+      "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/parser": "^7.28.0",
+        "@vue/compiler-sfc": "^3.5.18"
+      },
+      "dependencies": {
+        "@vue/compiler-core": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
+          "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
+          "dev": true,
+          "requires": {
+            "@babel/parser": "^7.29.2",
+            "@vue/shared": "3.5.31",
+            "entities": "^7.0.1",
+            "estree-walker": "^2.0.2",
+            "source-map-js": "^1.2.1"
+          }
+        },
+        "@vue/compiler-dom": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
+          "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
+          "dev": true,
+          "requires": {
+            "@vue/compiler-core": "3.5.31",
+            "@vue/shared": "3.5.31"
+          }
+        },
+        "@vue/compiler-sfc": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
+          "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
+          "dev": true,
+          "requires": {
+            "@babel/parser": "^7.29.2",
+            "@vue/compiler-core": "3.5.31",
+            "@vue/compiler-dom": "3.5.31",
+            "@vue/compiler-ssr": "3.5.31",
+            "@vue/shared": "3.5.31",
+            "estree-walker": "^2.0.2",
+            "magic-string": "^0.30.21",
+            "postcss": "^8.5.8",
+            "source-map-js": "^1.2.1"
+          }
+        },
+        "@vue/compiler-ssr": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
+          "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
+          "dev": true,
+          "requires": {
+            "@vue/compiler-dom": "3.5.31",
+            "@vue/shared": "3.5.31"
+          }
+        },
+        "@vue/shared": {
+          "version": "3.5.31",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
+          "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+          "dev": true
+        },
+        "entities": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+          "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+          "dev": true
+        }
+      }
+    },
+    "@vue/compiler-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
+      "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
+      "requires": {
+        "@babel/parser": "^7.27.5",
+        "@vue/shared": "3.5.17",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
+      "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
+      "requires": {
+        "@vue/compiler-core": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
+      "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
+      "requires": {
+        "@babel/parser": "^7.27.5",
+        "@vue/compiler-core": "3.5.17",
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.17",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "@vue/compiler-ssr": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
+      "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "requires": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.2.tgz",
+      "integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==",
+      "requires": {
+        "@vue/devtools-kit": "^7.7.2"
+      }
+    },
+    "@vue/devtools-kit": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz",
+      "integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==",
+      "requires": {
+        "@vue/devtools-shared": "^7.7.2",
+        "birpc": "^0.2.19",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.1"
+      }
+    },
+    "@vue/devtools-shared": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz",
+      "integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==",
+      "requires": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "@vue/eslint-config-typescript": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
+      "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/eslint-plugin": "^6.7.0",
+        "@typescript-eslint/parser": "^6.7.0",
+        "vue-eslint-parser": "^9.3.1"
+      }
+    },
+    "@vue/language-core": {
+      "version": "2.0.29",
+      "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.0.29.tgz",
+      "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "~2.4.0-alpha.18",
+        "@vue/compiler-dom": "^3.4.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.4.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "@vue/reactivity": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.17.tgz",
+      "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
+      "requires": {
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
+      "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
+      "requires": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
+      "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
+      "requires": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/runtime-core": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "csstype": "^3.1.3"
+      }
+    },
+    "@vue/server-renderer": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
+      "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
+      "requires": {
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.17.tgz",
+      "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="
+    },
+    "@vue/tsconfig": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
+      "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
+      "dev": true
+    },
+    "@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
+    },
+    "acorn": {
+      "version": "8.11.3",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.11.3.tgz",
+      "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "requires": {}
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "animate.css": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
+      "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+    },
+    "array-ify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz",
+      "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+      "dev": true
+    },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "axios": {
+      "version": "1.7.9",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz",
+      "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+      "requires": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
+    },
+    "baseline-browser-mapping": {
+      "version": "2.10.13",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
+      "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
+      "dev": true
+    },
+    "binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true
+    },
+    "birpc": {
+      "version": "0.2.19",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-0.2.19.tgz",
+      "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ=="
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "requires": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      }
+    },
+    "call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      }
+    },
+    "call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      }
+    },
+    "call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmmirror.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+      "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.3.1",
+        "map-obj": "^4.0.0",
+        "quick-lru": "^4.0.1"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001782",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz",
+      "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "requires": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      }
+    },
+    "chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "requires": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        }
+      }
+    },
+    "ci-info": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz",
+      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="
+    },
+    "clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "requires": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
+    "cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
+    },
+    "color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
+    },
+    "compare-func": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz",
+      "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+      "dev": true,
+      "requires": {
+        "array-ify": "^1.0.0",
+        "dot-prop": "^5.1.0"
+      }
+    },
+    "computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "conventional-changelog-angular": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz",
+      "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==",
+      "dev": true,
+      "requires": {
+        "compare-func": "^2.0.0"
+      }
+    },
+    "conventional-changelog-conventionalcommits": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz",
+      "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==",
+      "dev": true,
+      "requires": {
+        "compare-func": "^2.0.0"
+      }
+    },
+    "conventional-commits-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
+      "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==",
+      "dev": true,
+      "requires": {
+        "is-text-path": "^2.0.0",
+        "JSONStream": "^1.3.5",
+        "meow": "^12.0.1",
+        "split2": "^4.0.0"
+      }
+    },
+    "convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true
+    },
+    "copy-anything": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
+      "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+      "requires": {
+        "is-what": "^4.1.8"
+      }
+    },
+    "core-js-pure": {
+      "version": "3.47.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz",
+      "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw=="
+    },
+    "core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+    },
+    "cosmiconfig": {
+      "version": "8.3.6",
+      "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
+      "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+      "dev": true,
+      "requires": {
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0",
+        "path-type": "^4.0.0"
+      },
+      "dependencies": {
+        "json-parse-even-better-errors": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+          "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+          "dev": true
+        },
+        "lines-and-columns": {
+          "version": "1.2.4",
+          "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+          "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+          "dev": true
+        },
+        "parse-json": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+          "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-even-better-errors": "^2.3.0",
+            "lines-and-columns": "^1.1.6"
+          }
+        }
+      }
+    },
+    "cosmiconfig-typescript-loader": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz",
+      "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==",
+      "dev": true,
+      "requires": {
+        "jiti": "^1.19.1"
+      }
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+    },
+    "css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true
+    },
+    "csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+    },
+    "dargs": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/dargs/-/dargs-7.0.0.tgz",
+      "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==",
+      "dev": true
+    },
+    "de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true
+    },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "dev": true
+    },
+    "decamelize-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+      "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
+      "dev": true,
+      "requires": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "dependencies": {
+        "map-obj": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-1.0.1.tgz",
+          "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
+          "dev": true
+        }
+      }
+    },
+    "deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
+    "define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "requires": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    },
+    "delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
+    "dexie": {
+      "version": "4.0.11",
+      "resolved": "https://registry.npmmirror.com/dexie/-/dexie-4.0.11.tgz",
+      "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A=="
+    },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dot-prop": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz",
+      "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+      "dev": true,
+      "requires": {
+        "is-obj": "^2.0.0"
+      }
+    },
+    "dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      }
+    },
+    "echarts": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "requires": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "echarts-wordcloud": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
+      "integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
+      "requires": {}
+    },
+    "electron-to-chromium": {
+      "version": "1.5.330",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz",
+      "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==",
+      "dev": true
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+    },
+    "es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+    },
+    "es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "requires": {
+        "es-errors": "^1.3.0"
+      }
+    },
+    "esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "requires": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true
+    },
+    "eslint": {
+      "version": "8.56.0",
+      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.56.0.tgz",
+      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.56.0",
+        "@humanwhocodes/config-array": "^0.11.13",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "eslint-plugin-vue": {
+      "version": "9.19.2",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz",
+      "integrity": "sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.13",
+        "semver": "^7.5.4",
+        "vue-eslint-parser": "^9.3.1",
+        "xml-name-validator": "^4.0.0"
+      }
+    },
+    "eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true
+    },
+    "espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "requires": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.1.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.2.0"
+      }
+    },
+    "estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true
+    },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
+    "execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.0",
+        "human-signals": "^2.1.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.1",
+        "onetime": "^5.1.2",
+        "signal-exit": "^3.0.3",
+        "strip-final-newline": "^2.0.0"
+      }
+    },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        }
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.16.0.tgz",
+      "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^3.0.4"
+      }
+    },
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "find-yarn-workspace-root": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+      "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+      "requires": {
+        "micromatch": "^4.0.2"
+      }
+    },
+    "flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "requires": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      }
+    },
+    "flatted": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.9.tgz",
+      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "dev": true
+    },
+    "follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
+    },
+    "form-data": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "fs-extra": {
+      "version": "11.2.0",
+      "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz",
+      "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true
+    },
+    "get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      }
+    },
+    "get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "requires": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      }
+    },
+    "get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true
+    },
+    "git-raw-commits": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz",
+      "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==",
+      "dev": true,
+      "requires": {
+        "dargs": "^7.0.0",
+        "lodash": "^4.17.15",
+        "meow": "^8.0.0",
+        "split2": "^3.0.0",
+        "through2": "^4.0.0"
+      },
+      "dependencies": {
+        "hosted-git-info": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+          "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "meow": {
+          "version": "8.1.2",
+          "resolved": "https://registry.npmmirror.com/meow/-/meow-8.1.2.tgz",
+          "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==",
+          "dev": true,
+          "requires": {
+            "@types/minimist": "^1.2.0",
+            "camelcase-keys": "^6.2.2",
+            "decamelize-keys": "^1.1.0",
+            "hard-rejection": "^2.1.0",
+            "minimist-options": "4.1.0",
+            "normalize-package-data": "^3.0.0",
+            "read-pkg-up": "^7.0.1",
+            "redent": "^3.0.0",
+            "trim-newlines": "^3.0.0",
+            "type-fest": "^0.18.0",
+            "yargs-parser": "^20.2.3"
+          }
+        },
+        "normalize-package-data": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+          "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+          "dev": true,
+          "requires": {
+            "hosted-git-info": "^4.0.1",
+            "is-core-module": "^2.5.0",
+            "semver": "^7.3.4",
+            "validate-npm-package-license": "^3.0.1"
+          }
+        },
+        "readable-stream": {
+          "version": "3.6.2",
+          "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+          "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "split2": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmmirror.com/split2/-/split2-3.2.2.tgz",
+          "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
+          "dev": true,
+          "requires": {
+            "readable-stream": "^3.0.0"
+          }
+        },
+        "through2": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmmirror.com/through2/-/through2-4.0.2.tgz",
+          "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+          "dev": true,
+          "requires": {
+            "readable-stream": "3"
+          }
+        },
+        "type-fest": {
+          "version": "0.18.1",
+          "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.18.1.tgz",
+          "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "20.2.9",
+          "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz",
+          "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+          "dev": true
+        }
+      }
+    },
+    "glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.3"
+      }
+    },
+    "global-dirs": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmmirror.com/global-dirs/-/global-dirs-0.1.1.tgz",
+      "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==",
+      "dev": true,
+      "requires": {
+        "ini": "^1.3.4"
+      }
+    },
+    "globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.20.2"
+      }
+    },
+    "globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "requires": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      }
+    },
+    "good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "requires": {
+        "delegate": "^3.1.2"
+      }
+    },
+    "gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+    },
+    "graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+    },
+    "graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true
+    },
+    "hard-rejection": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/hard-rejection/-/hard-rejection-2.1.0.tgz",
+      "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+    },
+    "has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "requires": {
+        "es-define-property": "^1.0.0"
+      }
+    },
+    "has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+    },
+    "hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
+    "hfmath": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmmirror.com/hfmath/-/hfmath-0.0.2.tgz",
+      "integrity": "sha512-cKUi0yiQLGfLgs8+3Iw5nAiqSH13Knp7vCf0G1vlF5nfiKKO1XmxNagMvyp0F4ZvUNaHpRGTkmc7nowCy1S58g=="
+    },
+    "hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+    },
+    "hosted-git-info": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+      "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "^10.0.1"
+      }
+    },
+    "html-to-image": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.13.tgz",
+      "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="
+    },
+    "html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "requires": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      }
+    },
+    "https": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/https/-/https-1.0.0.tgz",
+      "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="
+    },
+    "human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true
+    },
+    "husky": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz",
+      "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
+      "dev": true
+    },
+    "ignore": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.0.tgz",
+      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "dev": true
+    },
+    "image-size": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/image-size/-/image-size-1.1.0.tgz",
+      "integrity": "sha512-asnTHw2K8OlqT5kVnQwX+AGKQqpvLo95LbNzQ/C0ln3yzentZmAdd0ygoD004VC4Kkd4PV7J2iaPQkqwp9yuTw==",
+      "requires": {
+        "queue": "6.0.2"
+      }
+    },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+    },
+    "immutable": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz",
+      "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "dev": true
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-core-module": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "dev": true,
+      "requires": {
+        "hasown": "^2.0.0"
+      }
+    },
+    "is-docker": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz",
+      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+    },
+    "is-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz",
+      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+      "dev": true
+    },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true
+    },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
+      "dev": true
+    },
+    "is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "dev": true
+    },
+    "is-text-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-text-path/-/is-text-path-2.0.0.tgz",
+      "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==",
+      "dev": true,
+      "requires": {
+        "text-extensions": "^2.0.0"
+      }
+    },
+    "is-what": {
+      "version": "4.1.16",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
+      "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="
+    },
+    "is-wsl": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz",
+      "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+      "requires": {
+        "is-docker": "^2.0.0"
+      }
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "isomorphic.js": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
+      "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="
+    },
+    "jiti": {
+      "version": "1.21.0",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "requires": {
+        "argparse": "^2.0.1"
+      }
+    },
+    "jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true
+    },
+    "json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true
+    },
+    "json-parse-even-better-errors": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz",
+      "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+      "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
+      "requires": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "isarray": "^2.0.5",
+        "jsonify": "^0.0.1",
+        "object-keys": "^1.1.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+          "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+        }
+      }
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true
+    },
+    "jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "requires": {
+        "graceful-fs": "^4.1.6",
+        "universalify": "^2.0.0"
+      }
+    },
+    "jsonify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/jsonify/-/jsonify-0.0.1.tgz",
+      "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="
+    },
+    "jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+      "dev": true
+    },
+    "JSONStream": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmmirror.com/JSONStream/-/JSONStream-1.3.5.tgz",
+      "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+      "dev": true,
+      "requires": {
+        "jsonparse": "^1.2.0",
+        "through": ">=2.2.7 <3"
+      }
+    },
+    "jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "requires": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "katex": {
+      "version": "0.16.22",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
+      "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
+      "requires": {
+        "commander": "^8.3.0"
+      }
+    },
+    "keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "requires": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true
+    },
+    "klaw-sync": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/klaw-sync/-/klaw-sync-6.0.0.tgz",
+      "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+      "requires": {
+        "graceful-fs": "^4.1.11"
+      }
+    },
+    "kolorist": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+      "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+      "dev": true
+    },
+    "levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      }
+    },
+    "lib0": {
+      "version": "0.2.114",
+      "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
+      "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
+      "requires": {
+        "isomorphic.js": "^0.2.4"
+      }
+    },
+    "lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "lines-and-columns": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz",
+      "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==",
+      "dev": true
+    },
+    "linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "requires": {
+        "uc.micro": "^2.0.0"
+      }
+    },
+    "locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^5.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "dev": true
+    },
+    "lodash.isfunction": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+      "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
+      "dev": true
+    },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "dev": true
+    },
+    "lodash.kebabcase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+      "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+      "dev": true
+    },
+    "lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "lodash.mergewith": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+      "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+      "dev": true
+    },
+    "lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+      "dev": true
+    },
+    "lodash.startcase": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+      "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+      "dev": true
+    },
+    "lodash.uniq": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+      "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+      "dev": true
+    },
+    "lodash.upperfirst": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmmirror.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+      "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.1.0.tgz",
+      "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
+      "dev": true
+    },
+    "magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "requires": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "map-obj": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-4.3.0.tgz",
+      "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+      "dev": true
+    },
+    "markdown-it": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
+      "requires": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      }
+    },
+    "math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+    },
+    "mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
+    },
+    "memorystream": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+      "dev": true
+    },
+    "meow": {
+      "version": "12.1.1",
+      "resolved": "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz",
+      "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+      "dev": true
+    },
+    "merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true
+    },
+    "merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz",
+      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "requires": {
+        "braces": "^3.0.2",
+        "picomatch": "^2.3.1"
+      }
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true
+    },
+    "min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^2.0.1"
+      }
+    },
+    "minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
+    },
+    "minimist-options": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/minimist-options/-/minimist-options-4.1.0.tgz",
+      "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0",
+        "kind-of": "^6.0.3"
+      }
+    },
+    "mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+    },
+    "mp4-muxer": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/mp4-muxer/-/mp4-muxer-2.3.0.tgz",
+      "integrity": "sha512-eupg1QMfcVJy/nZ2smUbC4OcEKdKBaLxKe+oeoDlklB5Dkb+4mv4CdPVYtzo1ZnzBKuVCclDk/znQwFQJIC32g==",
+      "requires": {
+        "@types/dom-webcodecs": "^0.1.6",
+        "@types/wicg-file-system-access": "^2020.9.5"
+      }
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true
+    },
+    "nanoid": {
+      "version": "5.0.7",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
+      "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
+    "node-releases": {
+      "version": "2.0.36",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+      "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+      "dev": true
+    },
+    "normalize-package-data": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
+      "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^7.0.0",
+        "is-core-module": "^2.8.1",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      }
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
+    "npm-run-all2": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-6.1.1.tgz",
+      "integrity": "sha512-lWLbkPZ5BSdXtN8lR+0rc8caKoPdymycpZksyDEC9MOBvfdwTXZ0uVhb7bMcGeXv2/BKtfQuo6Zn3zfc8rxNXA==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^6.2.1",
+        "cross-spawn": "^7.0.3",
+        "memorystream": "^0.3.1",
+        "minimatch": "^9.0.0",
+        "pidtree": "^0.6.0",
+        "read-pkg": "^8.0.0",
+        "shell-quote": "^1.7.3"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "6.2.1",
+          "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+          "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+          "dev": true
+        }
+      }
+    },
+    "npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "requires": {
+        "path-key": "^3.0.0"
+      }
+    },
+    "nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "requires": {
+        "boolbase": "^1.0.0"
+      }
+    },
+    "number-precision": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz",
+      "integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ=="
+    },
+    "object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "open": {
+      "version": "7.4.2",
+      "resolved": "https://registry.npmmirror.com/open/-/open-7.4.2.tgz",
+      "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+      "requires": {
+        "is-docker": "^2.0.0",
+        "is-wsl": "^2.1.1"
+      }
+    },
+    "optionator": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "dev": true,
+      "requires": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0"
+      }
+    },
+    "orderedmap": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz",
+      "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
+    },
+    "p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "requires": {
+        "yocto-queue": "^0.1.0"
+      }
+    },
+    "p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^3.0.2"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-7.1.1.tgz",
+      "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.21.4",
+        "error-ex": "^1.3.2",
+        "json-parse-even-better-errors": "^3.0.0",
+        "lines-and-columns": "^2.0.3",
+        "type-fest": "^3.8.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "3.13.1",
+          "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-3.13.1.tgz",
+          "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+          "dev": true
+        }
+      }
+    },
+    "patch-package": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/patch-package/-/patch-package-8.0.1.tgz",
+      "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
+      "requires": {
+        "@yarnpkg/lockfile": "^1.1.0",
+        "chalk": "^4.1.2",
+        "ci-info": "^3.7.0",
+        "cross-spawn": "^7.0.3",
+        "find-yarn-workspace-root": "^2.0.0",
+        "fs-extra": "^10.0.0",
+        "json-stable-stringify": "^1.0.2",
+        "klaw-sync": "^6.0.0",
+        "minimist": "^1.2.6",
+        "open": "^7.4.2",
+        "semver": "^7.5.3",
+        "slash": "^2.0.0",
+        "tmp": "^0.2.4",
+        "yaml": "^2.2.2"
+      },
+      "dependencies": {
+        "fs-extra": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+          "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+          "requires": {
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^6.0.1",
+            "universalify": "^2.0.0"
+          }
+        },
+        "slash": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmmirror.com/slash/-/slash-2.0.0.tgz",
+          "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
+        }
+      }
+    },
+    "path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+    },
+    "path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true
+    },
+    "perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
+    },
+    "picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+    },
+    "pidtree": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz",
+      "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+      "dev": true
+    },
+    "pinia": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.2.tgz",
+      "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==",
+      "requires": {
+        "@vue/devtools-api": "^7.7.2"
+      }
+    },
+    "postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "requires": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "dependencies": {
+        "nanoid": {
+          "version": "3.3.11",
+          "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+          "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
+        }
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "6.0.15",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
+      "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      }
+    },
+    "pptxgenjs": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmmirror.com/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
+      "integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
+      "requires": {
+        "@types/node": "^18.7.3",
+        "https": "^1.0.0",
+        "image-size": "^1.0.0",
+        "jszip": "^3.7.1"
+      }
+    },
+    "pptxtojson": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmmirror.com/pptxtojson/-/pptxtojson-1.12.0.tgz",
+      "integrity": "sha512-kXdCqXlCLdiGpM3womaQIpGYrQEzl8/jdxSpWWtYNj3Pxvr9n39tGpuBxeZSdenttwzM7Fb/yBKUPzxMFXLIJQ==",
+      "requires": {
+        "jszip": "^3.10.1",
+        "tinycolor2": "1.6.0",
+        "txml": "^5.1.1"
+      }
+    },
+    "prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
+    "prosemirror-commands": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
+      "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
+      "requires": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.0.0"
+      }
+    },
+    "prosemirror-dropcursor": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
+      "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
+      "requires": {
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.1.0",
+        "prosemirror-view": "^1.1.0"
+      }
+    },
+    "prosemirror-gapcursor": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
+      "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
+      "requires": {
+        "prosemirror-keymap": "^1.0.0",
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-view": "^1.0.0"
+      }
+    },
+    "prosemirror-history": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz",
+      "integrity": "sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==",
+      "requires": {
+        "prosemirror-state": "^1.2.2",
+        "prosemirror-transform": "^1.0.0",
+        "prosemirror-view": "^1.31.0",
+        "rope-sequence": "^1.3.0"
+      }
+    },
+    "prosemirror-inputrules": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
+      "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
+      "requires": {
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.0.0"
+      }
+    },
+    "prosemirror-keymap": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
+      "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
+      "requires": {
+        "prosemirror-state": "^1.0.0",
+        "w3c-keyname": "^2.2.0"
+      }
+    },
+    "prosemirror-model": {
+      "version": "1.22.2",
+      "resolved": "https://registry.npmmirror.com/prosemirror-model/-/prosemirror-model-1.22.2.tgz",
+      "integrity": "sha512-I4lS7HHIW47D0Xv/gWmi4iUWcQIDYaJKd8Hk4+lcSps+553FlQrhmxtItpEvTr75iAruhzVShVp6WUwsT6Boww==",
+      "requires": {
+        "orderedmap": "^2.0.0"
+      }
+    },
+    "prosemirror-schema-basic": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz",
+      "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==",
+      "requires": {
+        "prosemirror-model": "^1.19.0"
+      }
+    },
+    "prosemirror-schema-list": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
+      "integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
+      "requires": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.7.3"
+      }
+    },
+    "prosemirror-state": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmmirror.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
+      "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
+      "requires": {
+        "prosemirror-model": "^1.0.0",
+        "prosemirror-transform": "^1.0.0",
+        "prosemirror-view": "^1.27.0"
+      }
+    },
+    "prosemirror-transform": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz",
+      "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==",
+      "requires": {
+        "prosemirror-model": "^1.0.0"
+      }
+    },
+    "prosemirror-view": {
+      "version": "1.33.9",
+      "resolved": "https://registry.npmmirror.com/prosemirror-view/-/prosemirror-view-1.33.9.tgz",
+      "integrity": "sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==",
+      "requires": {
+        "prosemirror-model": "^1.20.0",
+        "prosemirror-state": "^1.0.0",
+        "prosemirror-transform": "^1.1.0"
+      }
+    },
+    "proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true
+    },
+    "punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
+    },
+    "qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "requires": {
+        "side-channel": "^1.1.0"
+      }
+    },
+    "queue": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/queue/-/queue-6.0.2.tgz",
+      "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+      "requires": {
+        "inherits": "~2.0.3"
+      }
+    },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
+    "quick-lru": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz",
+      "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+      "dev": true
+    },
+    "read-pkg": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-8.1.0.tgz",
+      "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==",
+      "dev": true,
+      "requires": {
+        "@types/normalize-package-data": "^2.4.1",
+        "normalize-package-data": "^6.0.0",
+        "parse-json": "^7.0.0",
+        "type-fest": "^4.2.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "4.9.0",
+          "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.9.0.tgz",
+          "integrity": "sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "hosted-git-info": {
+          "version": "2.8.9",
+          "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+          "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+          "dev": true
+        },
+        "json-parse-even-better-errors": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+          "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+          "dev": true
+        },
+        "lines-and-columns": {
+          "version": "1.2.4",
+          "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+          "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+          "dev": true
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^4.1.0"
+          }
+        },
+        "normalize-package-data": {
+          "version": "2.5.0",
+          "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+          "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+          "dev": true,
+          "requires": {
+            "hosted-git-info": "^2.1.4",
+            "resolve": "^1.10.0",
+            "semver": "2 || 3 || 4 || 5",
+            "validate-npm-package-license": "^3.0.1"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "parse-json": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+          "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-even-better-errors": "^2.3.0",
+            "lines-and-columns": "^1.1.6"
+          }
+        },
+        "read-pkg": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz",
+          "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+          "dev": true,
+          "requires": {
+            "@types/normalize-package-data": "^2.4.0",
+            "normalize-package-data": "^2.5.0",
+            "parse-json": "^5.0.0",
+            "type-fest": "^0.6.0"
+          },
+          "dependencies": {
+            "type-fest": {
+              "version": "0.6.0",
+              "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz",
+              "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+              "dev": true
+            }
+          }
+        },
+        "semver": {
+          "version": "5.7.2",
+          "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
+          "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+          "dev": true
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "requires": {
+        "picomatch": "^2.2.1"
+      }
+    },
+    "redent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+      "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+      "dev": true,
+      "requires": {
+        "indent-string": "^4.0.0",
+        "strip-indent": "^3.0.0"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true
+    },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "requires": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "resolve-global": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-global/-/resolve-global-1.0.0.tgz",
+      "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==",
+      "dev": true,
+      "requires": {
+        "global-dirs": "^0.1.1"
+      }
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "rollup": {
+      "version": "4.19.0",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.19.0.tgz",
+      "integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==",
+      "dev": true,
+      "requires": {
+        "@rollup/rollup-android-arm-eabi": "4.19.0",
+        "@rollup/rollup-android-arm64": "4.19.0",
+        "@rollup/rollup-darwin-arm64": "4.19.0",
+        "@rollup/rollup-darwin-x64": "4.19.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.19.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.19.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.19.0",
+        "@rollup/rollup-linux-arm64-musl": "4.19.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.19.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.19.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.19.0",
+        "@rollup/rollup-linux-x64-gnu": "4.19.0",
+        "@rollup/rollup-linux-x64-musl": "4.19.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.19.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.19.0",
+        "@rollup/rollup-win32-x64-msvc": "4.19.0",
+        "@types/estree": "1.0.5",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "rope-sequence": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz",
+      "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
+    },
+    "rtf.js": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/rtf.js/-/rtf.js-3.0.9.tgz",
+      "integrity": "sha512-I1GpDat4i548WzmeZXv27f/743984fvEeeBS8BC01/Sop17pMlUl3M7DYcdcB3PUvOZTrFIMxGZx8qw7cSMAKQ==",
+      "requires": {
+        "codepage": "^1.15.0"
+      }
+    },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "sass": {
+      "version": "1.69.6",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.6.tgz",
+      "integrity": "sha512-qbRr3k9JGHWXCvZU77SD2OTwUlC+gNT+61JOLcmLm+XqH4h/5D+p4IIsxvpkB89S9AwJOyb5+rWNpIucaFxSFQ==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      }
+    },
+    "select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
+    "semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "requires": {
+        "lru-cache": "^6.0.0"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        }
+      }
+    },
+    "set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "requires": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      }
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+    },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+    },
+    "shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true
+    },
+    "side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      }
+    },
+    "side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      }
+    },
+    "side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "requires": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      }
+    },
+    "side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "requires": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      }
+    },
+    "signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true
+    },
+    "slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true
+    },
+    "sortablejs": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
+      "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
+    },
+    "source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
+    },
+    "spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "dev": true
+    },
+    "speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
+    },
+    "split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "dev": true
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true
+    },
+    "strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "dev": true,
+      "requires": {
+        "min-indent": "^1.0.0"
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true
+    },
+    "superjson": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
+      "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
+      "requires": {
+        "copy-anything": "^3.0.2"
+      }
+    },
+    "supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true
+    },
+    "svg-arc-to-cubic-bezier": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
+      "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
+    },
+    "svg-pathdata": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-7.2.0.tgz",
+      "integrity": "sha512-qd+AxqMpfRrRQaWb2SrNFvn69cvl6piqY8TxhYl2Li1g4/LO5F9NJb5wI4vNwRryqgSgD43gYKLm/w3ag1bKvQ=="
+    },
+    "text-extensions": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/text-extensions/-/text-extensions-2.4.0.tgz",
+      "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==",
+      "dev": true
+    },
+    "text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
+      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "dev": true
+    },
+    "through2": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/through2/-/through2-3.0.2.tgz",
+      "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==",
+      "requires": {
+        "inherits": "^2.0.4",
+        "readable-stream": "2 || 3"
+      }
+    },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "tinycolor2": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
+      "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
+    },
+    "tippy.js": {
+      "version": "6.3.7",
+      "resolved": "https://registry.npmmirror.com/tippy.js/-/tippy.js-6.3.7.tgz",
+      "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
+      "requires": {
+        "@popperjs/core": "^2.9.0"
+      }
+    },
+    "tmp": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "trim-newlines": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz",
+      "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+      "dev": true
+    },
+    "ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true,
+      "requires": {}
+    },
+    "tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
+    "txml": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
+      "integrity": "sha512-TwMDLnXQ09enNaxybLVvKZU7rqog8LgnuAs4ZYXM0nV0eu10iLsSFwlX3AEknAXXtH1wT3CYfoiXAjyBexcmuw==",
+      "requires": {
+        "through2": "^3.0.1"
+      }
+    },
+    "type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1"
+      }
+    },
+    "type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true
+    },
+    "typescript": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.3.3.tgz",
+      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "devOptional": true
+    },
+    "uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
+    },
+    "undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
+    "universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="
+    },
+    "update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "requires": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      }
+    },
+    "uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "utif": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
+      "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+      "requires": {
+        "pako": "^1.0.5"
+      }
+    },
+    "utif2": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
+      "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
+      "requires": {
+        "pako": "^1.0.11"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "requires": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
+    "uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vite": {
+      "version": "5.3.5",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.3.5.tgz",
+      "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+      "dev": true,
+      "requires": {
+        "esbuild": "^0.21.3",
+        "fsevents": "~2.3.3",
+        "postcss": "^8.4.39",
+        "rollup": "^4.13.0"
+      }
+    },
+    "vite-plugin-vue-inspector": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.4.0.tgz",
+      "integrity": "sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==",
+      "dev": true,
+      "requires": {
+        "@babel/core": "^7.23.0",
+        "@babel/plugin-proposal-decorators": "^7.23.0",
+        "@babel/plugin-syntax-import-attributes": "^7.22.5",
+        "@babel/plugin-syntax-import-meta": "^7.10.4",
+        "@babel/plugin-transform-typescript": "^7.22.15",
+        "@vue/babel-plugin-jsx": "^1.1.5",
+        "@vue/compiler-dom": "^3.3.4",
+        "kolorist": "^1.8.0",
+        "magic-string": "^0.30.4"
+      }
+    },
+    "vscode-uri": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz",
+      "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+      "dev": true
+    },
+    "vue": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.17.tgz",
+      "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-sfc": "3.5.17",
+        "@vue/runtime-dom": "3.5.17",
+        "@vue/server-renderer": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "vue-eslint-parser": {
+      "version": "9.3.2",
+      "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
+      "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      }
+    },
+    "vue-tsc": {
+      "version": "2.0.29",
+      "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.0.29.tgz",
+      "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
+      "dev": true,
+      "requires": {
+        "@volar/typescript": "~2.4.0-alpha.18",
+        "@vue/language-core": "2.0.29",
+        "semver": "^7.5.4"
+      }
+    },
+    "vuedraggable": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
+      "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+      "requires": {
+        "sortablejs": "1.14.0"
+      }
+    },
+    "w3c-keyname": {
+      "version": "2.2.8",
+      "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+    },
+    "wangeditor": {
+      "version": "4.7.15",
+      "resolved": "https://registry.npmjs.org/wangeditor/-/wangeditor-4.7.15.tgz",
+      "integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
+      "requires": {
+        "@babel/runtime": "^7.11.2",
+        "@babel/runtime-corejs3": "^7.11.2",
+        "tslib": "^2.1.0"
+      }
+    },
+    "webcodecs-encoder": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmmirror.com/webcodecs-encoder/-/webcodecs-encoder-0.3.2.tgz",
+      "integrity": "sha512-F4XKzafAxVNhtnaQ5F8Q6xVn+bRrzLiUQs3p4PpogN3svBqGPR1y5OK66bunXEULOJ0XVJ0HqlJ/TSGGZTZ0AQ==",
+      "requires": {
+        "mp4-muxer": "^2.0.2",
+        "patch-package": "^8.0.0",
+        "webm-muxer": "^1.2.0"
+      }
+    },
+    "webm-muxer": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/webm-muxer/-/webm-muxer-1.2.1.tgz",
+      "integrity": "sha512-FaH7GVeK9tOZoWEW0gT/ryR9EoZnDe/RplJlTkG6zPWfEdrIy24PaXwEiyUnKwwO3zsjJWzFq4DLHECqGdJXIw==",
+      "requires": {
+        "@types/dom-webcodecs": "^0.1.4",
+        "@types/wicg-file-system-access": "^2020.9.5"
+      }
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true
+    },
+    "y-protocols": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
+      "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==",
+      "requires": {
+        "lib0": "^0.2.85"
+      }
+    },
+    "y-websocket": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz",
+      "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==",
+      "requires": {
+        "lib0": "^0.2.102",
+        "y-protocols": "^1.0.5"
+      }
+    },
+    "y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true
+    },
+    "yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "yaml": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz",
+      "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="
+    },
+    "yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "requires": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      }
+    },
+    "yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true
+    },
+    "yjs": {
+      "version": "13.6.27",
+      "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
+      "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
+      "requires": {
+        "lib0": "^0.2.99"
+      }
+    },
+    "yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true
+    },
+    "zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "requires": {
+        "tslib": "2.3.0"
+      }
+    }
+  }
+}

+ 3 - 0
package.json

@@ -7,6 +7,8 @@
     "dev": "vite",
     "build": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
+    "test:speaking-api-config": "node scripts/test-speaking-api-config.mjs",
+    "test:dialogue-stream-fallback": "node scripts/test-dialogue-stream-fallback.mjs",
     "build-only": "vite build",
     "type-check": "vue-tsc --build --force",
     "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -88,6 +90,7 @@
     "sass": "1.69.6",
     "typescript": "~5.3.0",
     "vite": "^5.3.5",
+    "vite-plugin-vue-inspector": "^5.4.0",
     "vue-tsc": "^2.0.29"
   }
 }

+ 26 - 0
public/pcm-recorder-worklet.js

@@ -0,0 +1,26 @@
+/**
+ * AudioWorklet processor: 收集麦克风 PCM 采样数据
+ * 运行在音频线程,通过 MessagePort 将 Float32Array 发回主线程
+ */
+class PCMRecorderProcessor extends AudioWorkletProcessor {
+  constructor() {
+    super()
+    this._stopped = false
+    this.port.onmessage = (e) => {
+      if (e.data === 'stop') this._stopped = true
+    }
+  }
+
+  process(inputs) {
+    if (this._stopped) return false
+
+    const input = inputs[0]
+    if (input && input[0] && input[0].length > 0) {
+      // 取第一个声道(mono),拷贝一份发给主线程
+      this.port.postMessage(new Float32Array(input[0]))
+    }
+    return true
+  }
+}
+
+registerProcessor('pcm-recorder-processor', PCMRecorderProcessor)

+ 126 - 0
scripts/test-dialogue-stream-fallback.mjs

@@ -0,0 +1,126 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts', import.meta.url)
+let source = await readFile(sourceUrl, 'utf8')
+
+source = source
+  .replace(
+    "import { ref, reactive, computed, onUnmounted } from 'vue'",
+    `
+const ref = value => ({ value })
+const reactive = value => value
+const computed = getter => ({ get value() { return getter() } })
+const onUnmounted = () => {}
+`,
+  )
+  .replace(
+    "import { createDialogueApi, DialogueApiError } from '../services/llmService'",
+    `
+const createDialogueApi = () => globalThis.__dialogueApi
+class DialogueApiError extends Error {
+  constructor(message, status) {
+    super(message)
+    this.status = status
+  }
+}
+`,
+  )
+  .replace(
+    "import { buildSpeakingWsUrl } from '../services/speakingApiConfig'",
+    "const buildSpeakingWsUrl = () => 'wss://example.test/api/speaking/dialogue/speak-stream'",
+  )
+
+const compiled = ts.transpileModule(source, {
+  compilerOptions: {
+    module: ts.ModuleKind.ESNext,
+    target: ts.ScriptTarget.ES2022,
+  },
+}).outputText
+
+class FakeWebSocket {
+  static CONNECTING = 0
+  static OPEN = 1
+  static CLOSING = 2
+  static CLOSED = 3
+  static instances = []
+
+  readyState = FakeWebSocket.CONNECTING
+  binaryType = ''
+  sent = []
+  onopen = null
+  onmessage = null
+  onerror = null
+  onclose = null
+
+  constructor(url) {
+    this.url = url
+    FakeWebSocket.instances.push(this)
+  }
+
+  send(payload) {
+    this.sent.push(payload)
+  }
+
+  close() {
+    this.readyState = FakeWebSocket.CLOSED
+    this.onclose?.({})
+  }
+}
+
+globalThis.WebSocket = FakeWebSocket
+
+const speakCalls = []
+globalThis.__dialogueApi = {
+  async *speak(sessionId, audioBlob, signal, turnId) {
+    speakCalls.push({ sessionId, audioBlob, signal, turnId })
+    yield { type: 'transcript', text: 'hello' }
+    yield { type: 'token', text: 'Hi.' }
+    yield { type: 'done', isComplete: false }
+  },
+  generateGreeting() {
+    throw new Error('not used')
+  },
+  getReport() {
+    throw new Error('not used')
+  },
+  completeSession() {
+    throw new Error('not used')
+  },
+}
+
+const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+const engine = mod.useDialogueEngine()
+engine.attachSession({ sessionId: 'session-1', totalRounds: 3 })
+
+const ctl = engine.beginStudentStream({ sampleRate: 16000, bits: 16, channels: 1 })
+assert.ok(ctl)
+
+const ws = FakeWebSocket.instances[0]
+ws.readyState = FakeWebSocket.CLOSED
+ws.onerror?.({})
+ws.onclose?.({})
+
+const audioBlob = new Blob([new Uint8Array([1, 2, 3, 4])], { type: 'audio/webm' })
+ctl.commit(audioBlob)
+await new Promise(resolve => setTimeout(resolve, 0))
+
+// WS 失败时不再自动降级 HTTP——api.speak 不应被自动调用。
+assert.equal(speakCalls.length, 0, 'auto fallback to /speak should not happen')
+
+// 应当只 push 一条 student 消息(已 splice 掉 ai 占位),处于 error 状态等待用户点重试。
+assert.equal(engine.messages.value.length, 1)
+const studentMsg = engine.messages.value[0]
+assert.equal(studentMsg.role, 'student')
+assert.equal(studentMsg.status, 'error')
+assert.equal(studentMsg.recovery, 'retry')
+assert.equal(studentMsg.audioBlob, audioBlob, 'audioBlob preserved for retry')
+assert.equal(studentMsg.turnId, ctl.turnId)
+
+// 用户点重试 → engine.retryMessage 走 sendStudentMessage → /speak(HTTP)。
+await engine.retryMessage(studentMsg.id)
+assert.equal(speakCalls.length, 1, 'retry should invoke /speak once')
+assert.equal(speakCalls[0].sessionId, 'session-1')
+assert.equal(speakCalls[0].turnId, ctl.turnId, 'retry reuses original turnId for idempotency')
+assert.equal(speakCalls[0].audioBlob, audioBlob)

+ 64 - 0
scripts/test-speaking-api-config.mjs

@@ -0,0 +1,64 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/services/speakingApiConfig.ts', import.meta.url)
+const source = await readFile(sourceUrl, 'utf8')
+
+async function loadConfig(env = {}) {
+  const compiled = ts.transpileModule(source.replaceAll('import.meta.env', JSON.stringify(env)), {
+    compilerOptions: {
+      module: ts.ModuleKind.ESNext,
+      target: ts.ScriptTarget.ES2020,
+    },
+  }).outputText
+
+  return import(`data:text/javascript,${encodeURIComponent(compiled)}#${Math.random()}`)
+}
+
+const defaults = await loadConfig()
+
+assert.equal(
+  defaults.FALLBACK_SPEAKING_API_HOST,
+  'https://ppt-english-speaking-api.cocorobo.cn',
+)
+
+assert.equal(
+  defaults.SPEAKING_DIALOGUE_API_BASE_URL,
+  'https://ppt-english-speaking-api.cocorobo.cn/api/speaking/dialogue',
+)
+
+assert.equal(
+  defaults.SPEAKING_CONFIG_API_BASE_URL,
+  'https://ppt-english-speaking-api.cocorobo.cn/api/speaking/config',
+)
+
+assert.equal(
+  defaults.SPEAKING_TRANSPORT,
+  'websocket',
+)
+
+assert.equal(
+  defaults.buildSpeakingWsUrl('/speak-stream'),
+  'wss://ppt-english-speaking-api.cocorobo.cn/api/speaking/dialogue/speak-stream',
+)
+
+const custom = await loadConfig({
+  VITE_SPEAKING_API_HOST: 'https://example.com/',
+  VITE_SPEAKING_TRANSPORT: 'http',
+})
+
+assert.equal(
+  custom.SPEAKING_DIALOGUE_API_BASE_URL,
+  'https://example.com/api/speaking/dialogue',
+)
+
+assert.equal(
+  custom.SPEAKING_CONFIG_API_BASE_URL,
+  'https://example.com/api/speaking/config',
+)
+
+assert.equal(
+  custom.SPEAKING_TRANSPORT,
+  'http',
+)

+ 30 - 1
src/components/CollapsibleToolbar/index2.vue

@@ -78,6 +78,13 @@
           </svg>
           <span class="item-label">{{ lang.ssMultimedia }}</span>
         </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }"
+          @click="toggleSubmenu('english')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path><path d="M19 10v2a7 7 0 01-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>
+          </svg>
+          <span class="item-label">{{ lang.ssEnglish }}</span>
+        </div>
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'cocoai' }">
@@ -498,6 +505,9 @@
         </div> -->
       </div>
     </div>
+    <div class="submenu submenu-english" :class="{ visible: activeSubmenu === 'english' }">
+      <SpeakingPanel />
+    </div>
 
     <div v-if="exporting" class="parsing-modal">
       <div class="parsing-content">
@@ -523,13 +533,15 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue'
+import { ref, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import { useSlidesStore } from '@/store'
+import { useSpeakingStore } from '@/store/speaking'
 import FileInput from '@/components/FileInput.vue'
 import AiChat from './componets/aiChat.vue'
+import SpeakingPanel from '@/views/Editor/EnglishSpeaking/SpeakingPanel.vue'
 import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
@@ -772,6 +784,18 @@ const toggleSubmenu = (menu: string) => {
   }
 }
 
+// 监听"打开口语配置面板"信号 → 自动展开 english 子菜单
+const speakingStore = useSpeakingStore()
+watch(() => speakingStore.openConfigSignal, (val, old) => {
+  if (val !== old) {
+    if (isCollapsed.value) {
+      isCollapsed.value = false
+      emit('toggle', false)
+    }
+    activeSubmenu.value = 'english'
+  }
+})
+
 import titlePage from './page/TitlePage.json'
 import ImagePage from './page/ImagePage.json'
 import ContentPage from './page/ContentPage.json'
@@ -1100,6 +1124,11 @@ const handleParsingClose = () => {
       width: 320px;
     }
   }
+
+  &.submenu-english {
+    display: flex;
+    flex-direction: column;
+  }
 }
 
 .submenu-title {

+ 104 - 0
src/services/speaking.ts

@@ -0,0 +1,104 @@
+import type { TopicDiscussionConfig } from '@/types/englishSpeaking'
+import { SPEAKING_CONFIG_API_BASE_URL, SPEAKING_DIALOGUE_API_BASE_URL } from '@/views/Editor/EnglishSpeaking/services/speakingApiConfig'
+
+const API_BASE = SPEAKING_CONFIG_API_BASE_URL
+
+export interface SpeakingConfigRecord {
+  id: string
+  config: TopicDiscussionConfig
+  ownerUid?: string | null
+}
+
+async function parse<T>(res: Response): Promise<T> {
+  if (!res.ok) {
+    const detail = await res.text().catch(() => '')
+    throw new Error(`[${res.status}] ${detail || res.statusText}`)
+  }
+  return res.json() as Promise<T>
+}
+
+/** 新建话题讨论配置 */
+export async function createSpeakingConfig(
+  config: TopicDiscussionConfig,
+  ownerUid?: string | null,
+): Promise<SpeakingConfigRecord> {
+  const res = await fetch(API_BASE, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ config, ownerUid: ownerUid ?? null }),
+  })
+  return parse<SpeakingConfigRecord>(res)
+}
+
+/** 读取话题讨论配置 */
+export async function getSpeakingConfig(id: string): Promise<SpeakingConfigRecord> {
+  const res = await fetch(`${API_BASE}/${encodeURIComponent(id)}`)
+  return parse<SpeakingConfigRecord>(res)
+}
+
+/** 更新话题讨论配置 */
+export async function updateSpeakingConfig(
+  id: string,
+  config: TopicDiscussionConfig,
+): Promise<SpeakingConfigRecord> {
+  const res = await fetch(`${API_BASE}/${encodeURIComponent(id)}`, {
+    method: 'PUT',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ config }),
+  })
+  return parse<SpeakingConfigRecord>(res)
+}
+
+const DIALOGUE_BASE = SPEAKING_DIALOGUE_API_BASE_URL
+
+export interface ClassSessionSummary {
+  userId: string
+  sessionId: string
+  status: 'active' | 'completed' | 'abandoned'
+  overallStatus: 'ready' | 'generating' | 'failed' | null
+  currentRound: number
+  totalRounds: number
+  overallScore: number | null
+  dimensions: Record<string, number>
+  topHighlights: string[]
+  topImprovements: string[]
+  createdAt: string | null
+  completedAt: string | null
+}
+
+export interface ListSessionsByConfigResponse {
+  summaries: ClassSessionSummary[]
+}
+
+export async function listSpeakingSessionsByConfig(
+  configId: string,
+  userIds: string[],
+): Promise<ListSessionsByConfigResponse> {
+  const params = new URLSearchParams({ configId, userIds: userIds.join(',') })
+  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config?${params}`, {
+    method: 'GET',
+    credentials: 'include',
+  })
+  return parse<ListSessionsByConfigResponse>(res)
+}
+
+export interface ClassSummaryResponse {
+  bullets: [string, string, string]
+  generatedAt: string
+  fromCache: boolean
+  llmStatus: 'ok' | 'fallback'
+}
+
+export async function generateClassSummary(
+  configId: string,
+  userIds: string[],
+  locale: 'zh' | 'en' | 'hk',
+): Promise<ClassSummaryResponse> {
+  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config/summary`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    credentials: 'include',
+    body: JSON.stringify({ configId, userIds, locale }),
+  })
+  return parse<ClassSummaryResponse>(res)
+}

+ 168 - 0
src/store/speaking.ts

@@ -0,0 +1,168 @@
+import { defineStore } from 'pinia'
+import type {
+  TopicDiscussionConfig,
+  PracticeMode,
+  ScoreMode,
+  EvalDimensions,
+  PreviewDialogueState,
+} from '@/types/englishSpeaking'
+
+const DEFAULT_CONFIG: TopicDiscussionConfig = {
+  topic: 'How I get to school',
+  grade: 'grade5-1',
+  selectedRole: 'tom',
+  learningGoals: {
+    vocabulary: ['on foot', 'by bus', 'by bike', 'by car', 'by underground', 'bus stop', 'near', 'far from'],
+    sentences: [
+      'How do you come to school?',
+      'I come to school by bus.',
+      'I come to school on foot.',
+      'Do you live near school?',
+      'I live far from school.',
+      'How long does it take?',
+    ],
+  },
+  practice: {
+    mode: 'time',
+    rounds: 2,
+    duration: 5,
+    showEnglish: true,
+    taskHint: true,
+    stutterHint: true,
+  },
+  evaluation: {
+    showReport: true,
+    dimensions: {
+      accuracy: true,
+      fluency: true,
+      completeness: true,
+      rhythm: true,
+    },
+    scoreMode: 'letter',
+  },
+}
+
+// 手动创建用:结构与 DEFAULT_CONFIG 完全一致,按需手动调整为空值
+const EMPTY_CONFIG: TopicDiscussionConfig = {
+  topic: '',
+  grade: 'grade5-1',
+  selectedRole: 'tom',
+  learningGoals: {
+    vocabulary: [],
+    sentences: [],
+  },
+  practice: {
+    mode: 'time',
+    rounds: 60,
+    duration: 1,
+    showEnglish: true,
+    taskHint: true,
+    stutterHint: true,
+  },
+  evaluation: {
+    showReport: true,
+    dimensions: {
+      accuracy: true,
+      fluency: true,
+      completeness: true,
+      rhythm: true,
+    },
+    scoreMode: 'letter',
+  },
+}
+
+export const useSpeakingStore = defineStore('speaking', {
+  state: () => ({
+    config: { ...DEFAULT_CONFIG } as TopicDiscussionConfig,
+    // 话题讨论预览当前阶段(由 TopicDiscussionPreview 同步)
+    previewState: 'ready' as PreviewDialogueState,
+    // 请求重置预览的递增信号(由 CanvasTool 触发 → TopicDiscussionPreview 监听)
+    resetSignal: 0,
+    // 请求打开配置面板的递增信号(由画布点击 77 型 frame 触发 → CollapsibleToolbar + SpeakingPanel 监听)
+    openConfigSignal: 0,
+  }),
+
+  actions: {
+    // 重置为默认配置
+    resetConfig() {
+      this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG))
+    },
+
+    // 重置为空白配置(手动创建用)
+    resetConfigEmpty() {
+      this.config = JSON.parse(JSON.stringify(EMPTY_CONFIG))
+    },
+
+    setPreviewState(state: PreviewDialogueState) {
+      this.previewState = state
+    },
+    requestResetPreview() {
+      this.resetSignal += 1
+    },
+    openConfigPanel() {
+      this.openConfigSignal += 1
+    },
+
+    // 用推荐卡片数据预填充
+    prefillFromTask(topic: string, vocabulary: string[], sentences: string[]) {
+      this.resetConfig()
+      this.config.topic = topic
+      this.config.learningGoals.vocabulary = [...vocabulary]
+      this.config.learningGoals.sentences = [...sentences]
+    },
+
+    // 话题
+    updateTopic(topic: string) {
+      this.config.topic = topic
+    },
+
+    // 年级
+    updateGrade(grade: string) {
+      this.config.grade = grade
+    },
+
+    // 学习目标 - 词汇
+    addVocabulary(word: string) {
+      this.config.learningGoals.vocabulary.push(word)
+    },
+    removeVocabulary(index: number) {
+      this.config.learningGoals.vocabulary.splice(index, 1)
+    },
+
+    // 学习目标 - 句型
+    addSentence(sentence: string) {
+      this.config.learningGoals.sentences.push(sentence)
+    },
+    removeSentence(index: number) {
+      this.config.learningGoals.sentences.splice(index, 1)
+    },
+    updateSentence(index: number, sentence: string) {
+      this.config.learningGoals.sentences[index] = sentence
+    },
+
+    // 练习设置
+    updatePracticeMode(mode: PracticeMode) {
+      this.config.practice.mode = mode
+    },
+    updateRounds(rounds: number) {
+      this.config.practice.rounds = rounds
+    },
+    updateDuration(duration: number) {
+      this.config.practice.duration = duration
+    },
+    togglePracticeSetting(key: 'showEnglish' | 'taskHint' | 'stutterHint') {
+      this.config.practice[key] = !this.config.practice[key]
+    },
+
+    // 评估设置
+    toggleShowReport() {
+      this.config.evaluation.showReport = !this.config.evaluation.showReport
+    },
+    toggleEvalDimension(dimension: keyof EvalDimensions) {
+      this.config.evaluation.dimensions[dimension] = !this.config.evaluation.dimensions[dimension]
+    },
+    updateScoreMode(scoreMode: ScoreMode) {
+      this.config.evaluation.scoreMode = scoreMode
+    },
+  },
+})

+ 342 - 0
src/types/englishSpeaking.ts

@@ -0,0 +1,342 @@
+// 练习方式
+export type PracticeMode = 'rounds' | 'time' | 'free'
+
+// 评分方式
+export type ScoreMode = 'numeric' | 'letter' | 'comment_only'
+
+// AI 角色
+export interface Role {
+  id: string
+  name: string
+  avatar: string
+  avatarUrl?: string
+  identity: string
+  personality: string
+  difficulty: number
+  isRecommended?: boolean
+}
+
+// 评估维度
+export interface EvalDimensions {
+  accuracy: boolean
+  fluency: boolean
+  completeness: boolean
+  rhythm: boolean
+}
+
+// 练习设置
+export interface PracticeSettings {
+  mode: PracticeMode
+  rounds: number
+  duration: number // 时长(分钟)
+  showEnglish: boolean
+  taskHint: boolean
+  stutterHint: boolean
+}
+
+// 评估设置
+export interface EvaluationSettings {
+  showReport: boolean
+  dimensions: EvalDimensions
+  scoreMode: ScoreMode
+}
+
+// 学习目标
+export interface LearningGoals {
+  vocabulary: string[]
+  sentences: string[]
+}
+
+// 话题讨论配置
+export interface TopicDiscussionConfig {
+  topic: string
+  grade: string
+  selectedRole: string
+  learningGoals: LearningGoals
+  practice: PracticeSettings
+  evaluation: EvaluationSettings
+}
+
+// 推荐任务卡片
+export interface TopicDiscussionTask {
+  id: string
+  titleZh: string
+  titleEn: string
+  subtitle: string
+  difficulty: 1 | 2 | 3
+  rounds: number
+  durationMinutes: number
+  vocabulary: string[]
+  sentences: string[]
+  note: string
+}
+
+// 教材单元
+export interface CurriculumUnit {
+  id: string
+  number: string
+  title: string
+  description: string
+  vocabulary: string[]
+  sentences: string[]
+}
+
+// 教材年级
+export interface CurriculumGrade {
+  id: string
+  name: string
+  semester: string
+  units: CurriculumUnit[]
+}
+
+// 教材
+export interface CurriculumTextbook {
+  id: string
+  name: string
+  grades: CurriculumGrade[]
+}
+
+// 教材数据根
+export interface CurriculumData {
+  textbooks: CurriculumTextbook[]
+}
+
+// 创建方式
+export type CreationMode = 'smart' | 'ai' | 'manual'
+
+// 页面层级
+export type PageMode = 'layer1' | 'layer2' | 'config'
+
+// 练习类型
+export type ExerciseType = 'speaking' | 'listening' | 'reading' | 'writing'
+
+// ==================== 预览组件类型定义 ====================
+
+// 评分等级
+export type ScoreLevel = 'excellent' | 'good' | 'fair' | 'needsWork'
+
+// 评分展示方式(复用配置侧的 ScoreMode)
+export type ScoreDisplayMode = ScoreMode
+
+// 对话统计数据
+export interface DialogueStatistics {
+  totalRounds: number
+  averageScore: number
+  highestScore: number
+  highestRound: number
+  grammarErrors: number
+  excellentExpressions: number
+  totalDuration: number // 秒
+}
+
+// 下次挑战推荐
+export interface NextChallenge {
+  difficulty?: string
+  unlockedTopic?: string
+  suggestedMode?: string
+}
+
+// 单句评价
+export interface SentenceEvaluation {
+  id: string
+  round: number
+  role: 'ai' | 'student'
+  content: string
+  contentZh?: string
+  audioUrl?: string
+  audioDuration?: number
+  score?: number
+  pronunciation?: {
+    accuracy: number
+    intonation: number
+    stress: number
+    fluency: number
+  }
+  feedback?: {
+    comment: string
+    betterExpression: string
+  }
+}
+
+// 整体评价报告
+export interface OverallEvaluation {
+  overallScore: number
+  scoreLevel: ScoreLevel
+  percentile: number
+  dimensions: {
+    fluency: number
+    interaction: number
+    vocabulary: number
+    grammar: number
+  }
+  aiComment: string
+  highlights: string[]
+  improvements: string[]
+  nextChallenge: NextChallenge
+  statistics: DialogueStatistics
+  sentenceEvaluations: SentenceEvaluation[]
+}
+
+// 预览用 AI 角色(扩展配置侧的 Role)
+export interface PreviewAIRole {
+  id: string
+  name: string
+  avatar: string
+  identity: string
+  personality: string
+  speakingStyle: 'formal' | 'casual' | 'playful'
+  speed: 'slow' | 'normal' | 'fast'
+  isCustom: boolean
+  isRecommended?: boolean
+}
+
+// 消息状态
+export type MessageStatus = 'loading' | 'done' | 'error'
+
+// 对话消息
+export interface PreviewChatMessage {
+  id: string
+  role: 'ai' | 'student'
+  content: string
+  timestamp: Date
+  status?: MessageStatus
+  error?: string
+  turnId?: string                                     // set when message is created via begin/commit
+  recovery?: 'retry' | 'rerecord' | 'restart'        // set on error; drives the button matrix
+  audioBlob?: Blob
+  audioUrl?: string
+  audioDuration?: number   // NEW: 秒,由后端 transcript 事件 / sessions-latest 写入
+  evaluation?: {
+    dimensions: {
+      accuracy: 'excellent' | 'good' | 'improve'
+      fluency: 'excellent' | 'good' | 'improve'
+      completeness: 'excellent' | 'good' | 'improve'
+      rhythm: 'excellent' | 'good' | 'improve'
+    }
+    suggestion: string
+    betterExpression?: string
+    suggestedWords?: string[]
+    wordAnalysis?: {
+      word: string
+      status: 'correct' | 'improvable'
+      userPronunciation?: string
+      standardPronunciation?: string
+      tip?: string
+    }[]
+  }
+}
+
+// ==================== 对话引擎类型定义 ====================
+
+// SSE 事件类型
+export type SSEEvent =
+  | { type: 'transcript'; text: string; audioDuration?: number | null }
+  | { type: 'token'; text: string }
+  | { type: 'done'; isComplete: boolean }
+  | { type: 'error'; message: string }
+
+// 对话会话配置
+export interface SessionConfig {
+  topic: string
+  grade: string
+  roleId: string
+  totalRounds: number
+  /** 时长(分钟)。与 totalRounds 是并存的两个结束条件,后端据此计算 expiresAt */
+  durationMinutes: number
+  vocabulary?: string[]
+  sentences?: string[]
+  configId?: string | null
+  userId?: string | null
+}
+
+// 对话会话信息 (createSession 返回)
+export interface SessionInfo {
+  sessionId: string
+  totalRounds: number
+  currentRound: number
+  expiresAt: string | null
+}
+
+// 开场白生成结果 (generateGreeting 返回)
+export interface GreetingInfo {
+  aiMessage: string
+}
+
+export interface HistoricalDialogueMessage {
+  id: string
+  round: number
+  role: 'ai' | 'student'
+  content: string
+  audioUrl?: string | null
+  audioDuration?: number | null    // NEW: 秒,来自后端 sessions/latest
+  clientTurnId?: string | null
+}
+
+// 用于启动 chat view 的最小 session 信息(父组件 createSession 后传给 DialogueChatView)
+// expiresAt 由后端基于 createSession 时传入的 durationMinutes 计算下发 —— 后端权威,
+// 关页面/刷新/换设备重进都接续同一个截止时刻,确保倒计时不会因任何原因暂停。
+export interface SessionStartInfo {
+  sessionId: string
+  expiresAt: string | null
+  currentRound?: number
+  messages?: HistoricalDialogueMessage[]
+}
+
+export interface LatestSessionInfo extends SessionStartInfo {
+  status: 'active' | 'completed' | 'abandoned'
+  totalRounds: number
+  currentRound: number
+  createdAt: string | null
+  completedAt: string | null
+  messages: HistoricalDialogueMessage[]
+}
+
+export interface LatestSessionResponse {
+  session: LatestSessionInfo | null
+}
+
+// 对话报告
+export type DialogueReportStatus = 'evaluating' | 'ready' | 'failed' | 'incomplete'
+
+export interface DialogueReport {
+  status: DialogueReportStatus
+  evaluation: OverallEvaluation
+}
+
+export interface TaskHint {
+  practice_level: string
+  conversation_topic: string
+  current_question: string
+  example_sentences: {
+    english: string
+    chinese: string
+  }[]
+  key_vocabulary: {
+    word: string
+    meaning: string
+  }[]
+}
+
+// 对话 API 接口
+export interface DialogueAPI {
+  createSession(config: SessionConfig): Promise<SessionInfo>
+  getLatestSession(configId: string, userId: string): Promise<LatestSessionResponse>
+  completeSession(sessionId: string): Promise<void>
+  /** Throws DOMException('AbortError') on signal abort; throws DialogueApiError on non-OK HTTP. */
+  generateGreeting(sessionId: string, turnId: string, signal?: AbortSignal): Promise<GreetingInfo>
+  generateTaskHint(sessionId: string): Promise<TaskHint>
+  speak(sessionId: string, audioBlob: Blob, signal: AbortSignal, turnId: string): AsyncGenerator<SSEEvent>
+  getReport(sessionId: string): Promise<DialogueReport>
+}
+
+// 徽章成就
+export interface BadgeAchievement {
+  id: string
+  name: string
+  nameEn: string
+  icon: string
+  description: string
+}
+
+// 预览对话状态
+export type PreviewDialogueState = 'checking-history' | 'ready' | 'chatting' | 'completed'

+ 8 - 1
src/views/Editor/Canvas/Operate/index.vue

@@ -10,7 +10,7 @@
     }"
   >
     <component
-      v-if="isSelected"
+      v-if="isSelected && !isLockedInteractiveWidget"
       :is="currentOperateComponent"
       :elementInfo="elementInfo"
       :handlerVisible="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)"
@@ -103,6 +103,13 @@ const elementIndexListInAnimation = computed(() => {
 
 const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
 const height = computed(() => 'height' in props.elementInfo ? props.elementInfo.height : 0)
+
+// 英语口语预览(FRAME + toolType 77)产品设计上占满整页不可变形,
+// 强制隐藏 resize / rotate 把手,用户仍可选中+删除+开配置面板,但看不到任何"拖动感"。
+const isLockedInteractiveWidget = computed(() => (
+  props.elementInfo.type === ElementTypes.FRAME &&
+  Number((props.elementInfo as any).toolType) === 77
+))
 </script>
 
 <style lang="scss" scoped>

+ 6 - 0
src/views/Editor/Canvas/index.vue

@@ -384,6 +384,12 @@ const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing
 const throttleUpdateSlideIndex = throttle(updateSlideIndex, 300, { leading: true, trailing: false })
 
 const handleMousewheelCanvas = (e: WheelEvent) => {
+  // type 77(英语口语预览)这类可交互 widget:如果 wheel 来自它内部的某个 scroll 容器(比如
+  // 报告的 .report-scroll、对话的 .chat-area),让内部 scroll 接管,canvas 不介入。
+  // 如果不这样做,下面的 preventDefault 会把浏览器默认滚动行为一并抑制,内部永远滚不动。
+  const targetEl = e.target instanceof Element ? e.target : null
+  if (targetEl?.closest('.topic-discussion-preview')) return
+
   e.preventDefault()
 
   // 按住Ctrl键时:缩放画布

+ 46 - 0
src/views/Editor/CanvasTool/index2.vue

@@ -104,6 +104,20 @@
           <span>{{ lang.ssShape }}</span>
         </div>
       </Popover>
+
+      <!-- 英语口语工具:重置预览 -->
+      <div
+        v-if="canResetSpeakingPreview"
+        class="handler-item reset-preview-btn"
+        @click="handleResetSpeakingPreview"
+      >
+        <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+          stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <polyline points="23 4 23 10 17 10" />
+          <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
+        </svg>
+        <span>重置预览</span>
+      </div>
     </div>
 
 
@@ -133,6 +147,7 @@
 import { ref, computed } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
+import { useSpeakingStore } from '@/store/speaking'
 import { getImageDataURL } from '@/utils/image'
 import useImport from '@/hooks/useImport'
 import type { ShapePoolItem } from '@/configs/shapes'
@@ -158,9 +173,26 @@ import Button from '@/components/Button.vue'
 
 const mainStore = useMainStore()
 const slidesStore = useSlidesStore()
+const speakingStore = useSpeakingStore()
 const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
 const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
 const { currentSlide } = storeToRefs(slidesStore)
+const { previewState } = storeToRefs(speakingStore)
+
+// 当前 slide 是否包含英语口语工具(toolType=77)
+const hasEnglishSpeakingTool = computed(() => {
+  const elements = currentSlide.value?.elements || []
+  return elements.some((el: any) => el.type === 'frame' && Number(el.toolType) === 77)
+})
+
+// 仅当存在 toolType=77 且预览不在 ready 阶段时,显示"重置预览"按钮
+const canResetSpeakingPreview = computed(
+  () => hasEnglishSpeakingTool.value && previewState.value !== 'ready'
+)
+
+const handleResetSpeakingPreview = () => {
+  speakingStore.requestResetPreview()
+}
 
 const getInitialViewMode = () => {
   const urlParams = new URLSearchParams(window.location.search)
@@ -481,6 +513,20 @@ const editContent = (toolType: number) => {
   }
 }
 
+.reset-preview-btn {
+  gap: 5px;
+  color: #6b7280;
+
+  svg {
+    width: 14px;
+    height: 14px;
+  }
+
+  &:hover {
+    color: #f97316;
+  }
+}
+
 .left-handler {
   display: flex;
   align-items: center;

+ 172 - 0
src/views/Editor/EnglishSpeaking/SpeakingPanel.vue

@@ -0,0 +1,172 @@
+<!--
+  SpeakingPanel — 英语口语练习的主面板容器组件
+
+  职责:
+  1. 管理三层页面导航状态(layer1 → layer2 → config)
+  2. 渲染面包屑导航,支持任意层级回退
+  3. 根据 pageMode 切换展示不同的子页面组件
+
+  页面流转:
+  layer1 (首页:选课本/年级/单元) → layer2 (口语练习列表) → config (话题讨论配置)
+-->
+<template>
+  <div class="speaking-panel">
+    <!-- 面包屑导航:根据当前 pageMode 展示不同层级的路径 -->
+    <div class="panel-header">
+      <div class="breadcrumb">
+        <!-- layer1: 仅显示标题,无回退链接 -->
+        <template v-if="pageMode === 'layer1'">
+          <span class="breadcrumb-title">{{ lang.ssEnglishSubject }}</span>
+        </template>
+        <template v-else>
+          <button class="breadcrumb-link" @click="goHome">{{ lang.ssEnglishSubject }}</button>
+          <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M9 18l6-6-6-6" />
+          </svg>
+          <!-- config 层:显示完整三级面包屑 英语 > 口语 > 话题讨论 -->
+          <template v-if="pageMode === 'config'">
+            <button class="breadcrumb-link" @click="goLayer2">{{ lang.ssSpeaking }}</button>
+            <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M9 18l6-6-6-6" />
+            </svg>
+            <span class="breadcrumb-current">{{ lang.ssTopicDiscussion }}</span>
+          </template>
+          <!-- layer2:显示两级面包屑 英语 > 口语 -->
+          <template v-else>
+            <span class="breadcrumb-current">{{ lang.ssSpeaking }}</span>
+          </template>
+        </template>
+      </div>
+    </div>
+
+    <!-- 内容区域:根据 pageMode 切换子页面 -->
+    <div class="panel-body">
+      <Layer1Home
+        v-if="pageMode === 'layer1'"
+        :selectedTextbook="selectedTextbook"
+        :selectedGrade="selectedGrade"
+        :selectedUnit="selectedUnit"
+        @update:selectedTextbook="selectedTextbook = $event"
+        @update:selectedGrade="selectedGrade = $event"
+        @update:selectedUnit="selectedUnit = $event"
+        @selectExerciseType="handleSelectExerciseType"
+      />
+      <Layer2Speaking
+        v-else-if="pageMode === 'layer2'"
+        :unitId="selectedUnit"
+        @openConfig="handleOpenConfig"
+      />
+      <TopicDiscussionConfig
+        v-else-if="pageMode === 'config'"
+        @back="goLayer2"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+import { lang } from '@/main'
+import type { PageMode, ExerciseType } from '@/types/englishSpeaking'
+import { useSpeakingStore } from '@/store/speaking'
+import Layer1Home from './layers/Layer1Home.vue'
+import Layer2Speaking from './layers/Layer2Speaking.vue'
+import TopicDiscussionConfig from './configs/TopicDiscussionConfig.vue'
+
+/** 当前页面层级:layer1(首页) → layer2(口语列表) → config(配置页) */
+const pageMode = ref<PageMode>('layer1')
+
+const speakingStore = useSpeakingStore()
+
+// 课本筛选条件的默认值(上海英语·五年级上·Unit2),供开发/演示用
+const selectedTextbook = ref('shep')
+const selectedUnit = ref('SHEP-5-上-Unit2')
+// selectedGrade 持久化到 store(用于发送到后端 createSession 渲染 system_prompt)
+const selectedGrade = computed({
+  get: () => speakingStore.config.grade,
+  set: (val: string) => speakingStore.updateGrade(val),
+})
+
+/** Layer1 选择练习类型后的回调,目前仅 speaking 类型会触发页面跳转 */
+const handleSelectExerciseType = (type: ExerciseType) => {
+  if (type === 'speaking') {
+    pageMode.value = 'layer2'
+  }
+}
+
+/** Layer2 点击某个口语话题后,进入配置页 */
+const handleOpenConfig = () => {
+  pageMode.value = 'config'
+}
+
+// ---- 面包屑导航回退 ----
+const goHome = () => {
+  pageMode.value = 'layer1'
+}
+
+const goLayer2 = () => {
+  pageMode.value = 'layer2'
+}
+
+// 点击画布上的 77 型 frame 会递增 openConfigSignal → 强制切到配置页
+watch(() => speakingStore.openConfigSignal, (val, old) => {
+  if (val !== old) pageMode.value = 'config'
+})
+</script>
+
+<style lang="scss" scoped>
+.speaking-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #fff;
+}
+
+.panel-header {
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+  flex-shrink: 0;
+}
+
+.breadcrumb {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+}
+
+.breadcrumb-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #111;
+}
+
+.breadcrumb-link {
+  background: none;
+  border: none;
+  padding: 0;
+  cursor: pointer;
+  color: #9ca3af;
+  font-size: 13px;
+  transition: color 0.2s;
+
+  &:hover {
+    color: #6b7280;
+  }
+}
+
+.breadcrumb-arrow {
+  color: #d1d5db;
+  flex-shrink: 0;
+}
+
+.breadcrumb-current {
+  font-weight: 600;
+  color: #111;
+}
+
+.panel-body {
+  flex: 1;
+  overflow-y: auto;
+}
+</style>

+ 239 - 0
src/views/Editor/EnglishSpeaking/components/AIGenerationForm.vue

@@ -0,0 +1,239 @@
+<template>
+  <div class="ai-generation-form">
+    <div class="form-section">
+      <label class="form-label">描述你想要的练习</label>
+      <textarea
+        v-model="description"
+        class="form-textarea"
+        placeholder="例如:我想让学生练习用英语描述上学路线..."
+        rows="4"
+      />
+    </div>
+
+    <div class="form-section">
+      <label class="form-label">任务类型</label>
+      <div class="task-type-selector">
+        <select v-model="taskType" class="form-select">
+          <option value="topic-discussion">话题讨论</option>
+          <option value="scenario-task">情境任务</option>
+          <option value="casual-chat">轻松闲聊</option>
+          <option value="mock-interview">模拟访谈</option>
+        </select>
+        <svg class="select-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <path d="M6 9l6 6 6-6" />
+        </svg>
+      </div>
+    </div>
+
+    <div class="form-section">
+      <label class="form-label">参考资料(可选)</label>
+      <div class="knowledge-upload">
+        <div class="upload-placeholder">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
+            <polyline points="17 8 12 3 7 8" />
+            <line x1="12" y1="3" x2="12" y2="15" />
+          </svg>
+          <span>上传文件或粘贴链接</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="form-actions">
+      <button class="btn-generate" :disabled="!description.trim()" @click="handleGenerate">
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
+        </svg>
+        <span>生成练习</span>
+      </button>
+    </div>
+
+    <!-- 功能开发中提示 -->
+    <div v-if="showDevelopingTip" class="developing-tip">
+      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <circle cx="12" cy="12" r="10" />
+        <line x1="12" y1="8" x2="12" y2="12" />
+        <line x1="12" y1="16" x2="12.01" y2="16" />
+      </svg>
+      <span>{{ lang.ssFunctionDeveloping }}</span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { lang } from '@/main'
+
+const description = ref('')
+const taskType = ref('topic-discussion')
+const showDevelopingTip = ref(false)
+
+const handleGenerate = () => {
+  // 仅 UI 壳子,显示"功能开发中"提示
+  showDevelopingTip.value = true
+  setTimeout(() => {
+    showDevelopingTip.value = false
+  }, 3000)
+}
+</script>
+
+<style lang="scss" scoped>
+.ai-generation-form {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.form-label {
+  font-size: 13px;
+  font-weight: 500;
+  color: #374151;
+}
+
+.form-textarea {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  border-radius: 12px;
+  background: #f9fafb;
+  font-size: 13px;
+  color: #111827;
+  line-height: 1.5;
+  resize: vertical;
+  transition: all 0.2s;
+  font-family: inherit;
+  box-sizing: border-box;
+
+  &:focus {
+    outline: none;
+    border-color: #f97316;
+    background: #fff;
+    box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.1);
+  }
+
+  &::placeholder {
+    color: #9ca3af;
+  }
+}
+
+.task-type-selector {
+  position: relative;
+}
+
+.form-select {
+  width: 100%;
+  appearance: none;
+  padding: 8px 32px 8px 12px;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  border-radius: 12px;
+  background: #f9fafb;
+  font-size: 13px;
+  color: #111827;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:focus {
+    outline: none;
+    border-color: #f97316;
+    background: #fff;
+    box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.1);
+  }
+}
+
+.select-arrow {
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #9ca3af;
+  pointer-events: none;
+}
+
+.knowledge-upload {
+  border: 1px dashed rgba(0, 0, 0, 0.12);
+  border-radius: 12px;
+  padding: 16px;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: #f97316;
+    background: #fff7ed;
+  }
+}
+
+.upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  color: #9ca3af;
+  font-size: 12px;
+
+  svg {
+    color: #d1d5db;
+  }
+}
+
+.form-actions {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.btn-generate {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 20px;
+  border: none;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #f97316, #ef4444);
+  color: #fff;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover:not(:disabled) {
+    opacity: 0.9;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3);
+  }
+
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+}
+
+.developing-tip {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 10px 16px;
+  border-radius: 12px;
+  background: #fffbeb;
+  border: 1px solid #fde68a;
+  color: #92400e;
+  font-size: 13px;
+  animation: fadeIn 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-4px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+</style>

+ 123 - 0
src/views/Editor/EnglishSpeaking/components/CreationModeSwitch.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="creation-mode-switch">
+    <span class="mode-title">
+      {{ currentModeLabel }}
+    </span>
+    <div class="mode-actions">
+      <!-- 当前是 smart 模式时,显示 AI生成 和 手动创建 按钮 -->
+      <template v-if="modelValue === 'smart'">
+        <!-- AI生成按钮(暂时隐藏,保留以便后续启用)
+        <button class="mode-btn" @click="$emit('update:modelValue', 'ai')">
+          <svg class="mode-btn-icon sparkle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
+          </svg>
+          <span>AI生成</span>
+        </button>
+        -->
+        <button class="mode-btn" @click="$emit('update:modelValue', 'manual')">
+          <svg class="mode-btn-icon clipboard" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" />
+            <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
+          </svg>
+          <span>手动创建</span>
+        </button>
+      </template>
+      <!-- 当前是 ai 或 manual 模式时,显示 返回推荐 按钮 -->
+      <template v-else>
+        <button class="mode-btn mode-btn--active" @click="$emit('update:modelValue', 'smart')">
+          <svg class="mode-btn-icon sparkle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
+          </svg>
+          <span>{{ lang.ssBackToRecommend }}</span>
+        </button>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { lang } from '@/main'
+import type { CreationMode } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  modelValue: CreationMode
+}>()
+
+defineEmits<{
+  (e: 'update:modelValue', value: CreationMode): void
+}>()
+
+const currentModeLabel = computed(() => {
+  switch (props.modelValue) {
+    case 'ai': return 'AI 生成'
+    case 'manual': return lang.ssManualCreate
+    default: return lang.ssSmartRecommend
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.creation-mode-switch {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.mode-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.mode-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.mode-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  border-radius: 12px;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  background: transparent;
+  font-size: 13px;
+  color: #4b5563;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: #d1d5db;
+    background: #f9fafb;
+  }
+
+  &--active {
+    border-color: #fb923c;
+    background: #fff7ed;
+    color: #ea580c;
+
+    &:hover {
+      opacity: 0.8;
+    }
+
+    .sparkle {
+      color: #ea580c;
+    }
+  }
+}
+
+.mode-btn-icon {
+  flex-shrink: 0;
+
+  &.sparkle {
+    color: #f97316;
+  }
+
+  &.clipboard {
+    color: #f97316;
+  }
+}
+</style>

+ 116 - 0
src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="curriculum-selector">
+    <!-- 教材 -->
+    <div class="select-wrapper">
+      <select
+        :value="selectedTextbook"
+        @change="$emit('update:selectedTextbook', ($event.target as HTMLSelectElement).value)"
+      >
+        <option v-for="tb in textbooks" :key="tb.id" :value="tb.id">{{ tb.name }}</option>
+      </select>
+      <svg class="select-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
+    </div>
+
+    <!-- 年级 -->
+    <div class="select-wrapper">
+      <select
+        :value="selectedGrade"
+        @change="$emit('update:selectedGrade', ($event.target as HTMLSelectElement).value)"
+      >
+        <option v-for="g in grades" :key="g.id" :value="g.id">{{ g.name }}{{ g.semester }}</option>
+      </select>
+      <svg class="select-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
+    </div>
+
+    <!-- 单元 -->
+    <div class="select-wrapper">
+      <select
+        :value="selectedUnit"
+        @change="$emit('update:selectedUnit', ($event.target as HTMLSelectElement).value)"
+      >
+        <option v-for="u in units" :key="u.id" :value="u.id">{{ u.number }}</option>
+      </select>
+      <svg class="select-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import curriculumData from '../data/curriculum.json'
+import type { CurriculumData } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  selectedTextbook: string
+  selectedGrade: string
+  selectedUnit: string
+}>()
+
+defineEmits<{
+  (e: 'update:selectedTextbook', value: string): void
+  (e: 'update:selectedGrade', value: string): void
+  (e: 'update:selectedUnit', value: string): void
+}>()
+
+const data = curriculumData as CurriculumData
+
+const textbooks = computed(() => data.textbooks)
+
+const grades = computed(() => {
+  const tb = data.textbooks.find(t => t.id === props.selectedTextbook)
+  return tb?.grades || []
+})
+
+const units = computed(() => {
+  const tb = data.textbooks.find(t => t.id === props.selectedTextbook)
+  const grade = tb?.grades.find(g => g.id === props.selectedGrade)
+  return grade?.units || []
+})
+</script>
+
+<style lang="scss" scoped>
+.curriculum-selector {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.select-wrapper {
+  position: relative;
+  flex: 1;
+
+  select {
+    width: 100%;
+    appearance: none;
+    background: rgba(249, 250, 251, 0.8);
+    border: 1px solid rgba(229, 231, 235, 0.8);
+    border-radius: 12px;
+    padding: 8px 28px 8px 12px;
+    font-size: 13px;
+    color: #1f2937;
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &:hover {
+      background: #fff;
+      border-color: #d1d5db;
+    }
+
+    &:focus {
+      outline: none;
+      border-color: #fb923c;
+      box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.1);
+      background: #fff;
+    }
+  }
+}
+
+.select-arrow {
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #9ca3af;
+  pointer-events: none;
+}
+</style>

+ 145 - 0
src/views/Editor/EnglishSpeaking/components/ExerciseTypeGrid.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="exercise-type-section">
+    <label class="section-label">{{ lang.ssAddExercise }}</label>
+    <div class="type-grid">
+      <button
+        v-for="item in exerciseTypes"
+        :key="item.id"
+        class="type-card"
+        :class="[`type-card--${item.id}`, { disabled: item.id !== 'speaking' }]"
+        @click="handleClick(item.id)"
+      >
+        <div class="type-icon" v-html="item.icon"></div>
+        <span class="type-name">{{ item.label }}</span>
+        <span v-if="item.id !== 'speaking'" class="coming-soon">{{ lang.ssComingSoon }}</span>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { lang } from '@/main'
+import type { ExerciseType } from '@/types/englishSpeaking'
+
+const emit = defineEmits<{
+  (e: 'select', type: ExerciseType): void
+}>()
+
+const exerciseTypes: { id: ExerciseType; label: string; icon: string }[] = [
+  {
+    id: 'speaking',
+    label: lang.ssSpeaking as string,
+    icon: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',
+  },
+  {
+    id: 'listening',
+    label: lang.ssListening as string,
+    icon: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0118 0v6"/><path d="M21 19a2 2 0 01-2 2h-1a2 2 0 01-2-2v-3a2 2 0 012-2h3zM3 19a2 2 0 002 2h1a2 2 0 002-2v-3a2 2 0 00-2-2H3z"/></svg>',
+  },
+  {
+    id: 'reading',
+    label: lang.ssReading as string,
+    icon: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
+  },
+  {
+    id: 'writing',
+    label: lang.ssWriting as string,
+    icon: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
+  },
+]
+
+const handleClick = (type: ExerciseType) => {
+  if (type === 'speaking') {
+    emit('select', type)
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.section-label {
+  display: block;
+  font-size: 14px;
+  font-weight: 500;
+  color: #4b5563;
+  margin-bottom: 16px;
+}
+
+.type-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 14px;
+}
+
+.type-card {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  padding: 16px 12px;
+  border-radius: 16px;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  background: #fff;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &:hover:not(.disabled) {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
+  }
+
+  &.disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  &--speaking {
+    border-color: rgba(251, 146, 60, 0.15);
+    background: linear-gradient(135deg, rgba(255, 237, 213, 0.3), #fff);
+    &:hover { border-color: #fb923c; box-shadow: 0 8px 24px rgba(251, 146, 60, 0.15); }
+    .type-icon { color: #f97316; }
+  }
+
+  &--listening {
+    border-color: rgba(59, 130, 246, 0.15);
+    background: linear-gradient(135deg, rgba(219, 234, 254, 0.3), #fff);
+    .type-icon { color: #3b82f6; }
+  }
+
+  &--reading {
+    border-color: rgba(34, 197, 94, 0.15);
+    background: linear-gradient(135deg, rgba(220, 252, 231, 0.3), #fff);
+    .type-icon { color: #22c55e; }
+  }
+
+  &--writing {
+    border-color: rgba(168, 85, 247, 0.15);
+    background: linear-gradient(135deg, rgba(243, 232, 255, 0.3), #fff);
+    .type-icon { color: #a855f7; }
+  }
+}
+
+.type-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.type-name {
+  font-size: 13px;
+  font-weight: 600;
+  color: #374151;
+}
+
+.coming-soon {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  font-size: 10px;
+  color: #9ca3af;
+  background: #f3f4f6;
+  padding: 2px 6px;
+  border-radius: 6px;
+}
+</style>

+ 123 - 0
src/views/Editor/EnglishSpeaking/components/RecommendCard.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="recommend-card" @click="$emit('select')">
+    <div class="card-content">
+      <h4 class="card-title">{{ task.titleZh }}</h4>
+      <p class="card-subtitle">{{ task.subtitle }}</p>
+      <p class="card-meta">
+        <span>{{ lang.ssTopicDiscussion }}</span>
+        <span class="meta-dot">·</span>
+        <span>{{ task.rounds }}{{ lang.ssRoundsUnit }}</span>
+        <span class="meta-dot">·</span>
+        <span>{{ difficultyLabel }}</span>
+        <span class="meta-dot">·</span>
+        <span>~{{ task.durationMinutes }}{{ lang.ssMinutesUnit }}</span>
+      </p>
+    </div>
+    <svg class="card-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+      <path d="M9 18l6-6-6-6" />
+    </svg>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { lang } from '@/main'
+import type { TopicDiscussionTask } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  task: TopicDiscussionTask
+}>()
+
+defineEmits<{
+  (e: 'select'): void
+}>()
+
+const difficultyLabel = computed(() => {
+  const labels: Record<number, string> = {
+    1: lang.ssDifficultyBeginner as string,
+    2: lang.ssDifficultyIntermediate as string,
+    3: lang.ssDifficultyAdvanced as string,
+  }
+  return labels[props.task.difficulty] || ''
+})
+</script>
+
+<style lang="scss" scoped>
+.recommend-card {
+  position: relative;
+  display: flex;
+  align-items: flex-start;
+  background: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  border-radius: 16px;
+  padding: 14px;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: rgba(0, 0, 0, 0.12);
+    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
+
+    .card-arrow {
+      color: #f97316;
+    }
+
+    .card-title {
+      color: #ea580c;
+    }
+  }
+}
+
+.card-content {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.card-title {
+  font-size: 13px;
+  font-weight: 500;
+  color: #111827;
+  line-height: 1.4;
+  transition: color 0.2s;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-subtitle {
+  font-size: 12px;
+  color: #6b7280;
+  line-height: 1.5;
+  margin: 0;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.card-meta {
+  font-size: 11px;
+  color: #9ca3af;
+  margin: 0;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 0;
+}
+
+.meta-dot {
+  margin: 0 4px;
+}
+
+.card-arrow {
+  position: absolute;
+  top: 14px;
+  right: 14px;
+  color: #d1d5db;
+  flex-shrink: 0;
+  transition: color 0.2s;
+}
+</style>

+ 62 - 0
src/views/Editor/EnglishSpeaking/components/UnitInfoCard.vue

@@ -0,0 +1,62 @@
+<template>
+  <div v-if="unit" class="unit-info-card">
+    <div class="unit-icon">📚</div>
+    <div class="unit-content">
+      <h3 class="unit-title">{{ unit.title }}</h3>
+      <p class="unit-desc">{{ unit.description }}</p>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { CurriculumUnit } from '@/types/englishSpeaking'
+
+defineProps<{
+  unit: CurriculumUnit | undefined
+}>()
+</script>
+
+<style lang="scss" scoped>
+.unit-info-card {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  background: linear-gradient(135deg, rgba(251, 191, 36, 0.06), #fff);
+  border-radius: 16px;
+  padding: 14px;
+  border: 1px solid rgba(251, 191, 36, 0.15);
+  box-shadow: 0 1px 3px rgba(251, 191, 36, 0.08);
+}
+
+.unit-icon {
+  width: 36px;
+  height: 36px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #fbbf24, #f97316);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  font-size: 16px;
+  box-shadow: 0 2px 6px rgba(251, 191, 36, 0.3);
+}
+
+.unit-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.unit-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #111827;
+  margin: 0 0 4px 0;
+}
+
+.unit-desc {
+  font-size: 12px;
+  color: #4b5563;
+  line-height: 1.5;
+  margin: 0;
+}
+</style>

+ 170 - 0
src/views/Editor/EnglishSpeaking/composables/useAudioPlayer.ts

@@ -0,0 +1,170 @@
+import { ref, onUnmounted, type Ref } from 'vue'
+import { synthesize } from '../services/speechService'
+
+export type PlaySource =
+  | { kind: 'tts'; text: string }
+  | { kind: 'blob'; blob: Blob }
+  | { kind: 'url'; url: string }
+
+export interface AudioPlayer {
+  /** Id of the message currently playing (audio element fired `playing`). */
+  playingId: Readonly<Ref<string | null>>
+  /** Id of the message whose synthesis or play() is in flight. */
+  loadingId: Readonly<Ref<string | null>>
+  /** Id of the message whose last playback attempt failed. Sticky until next play(). */
+  errorId: Readonly<Ref<string | null>>
+
+  play(id: string, source: PlaySource): Promise<void>
+  stop(): void
+}
+
+/**
+ * Single audio playback owner for the dialogue view.
+ *
+ * Structural guarantees:
+ *  - At most one HTMLAudioElement / one in-flight synthesis at a time.
+ *    Every play() begins by aborting & pausing the prior session.
+ *  - Cached MP3 blobs (one per AI messageId) live in-memory; URLs are
+ *    revoked on view unmount.
+ *  - Errors collapse to a single per-id state. The view renders a retry
+ *    affordance; clicking the same play button re-enters play().
+ */
+export function useAudioPlayer(): AudioPlayer {
+  const playingId = ref<string | null>(null)
+  const loadingId = ref<string | null>(null)
+  const errorId = ref<string | null>(null)
+
+  // Closure-private state. Not reactive on purpose.
+  let currentAudio: HTMLAudioElement | null = null
+  let synthAbort: AbortController | null = null
+  const ttsCache = new Map<string, Blob>()
+  const cachedUrls: string[] = []
+
+  function clearCurrentAudio() {
+    if (currentAudio) {
+      currentAudio.onplaying = null
+      currentAudio.onended = null
+      currentAudio.onerror = null
+      try { currentAudio.pause() } catch { /* ignore */ }
+      currentAudio = null
+    }
+  }
+
+  function failPlayback(id: string, reason: unknown) {
+    if (loadingId.value === id) loadingId.value = null
+    if (playingId.value === id) playingId.value = null
+    errorId.value = id
+    console.warn('[audio-player] playback failed:', id, reason)
+  }
+
+  async function play(id: string, source: PlaySource): Promise<void> {
+    // A fresh attempt — drop stale error.
+    errorId.value = null
+
+    // Abort any in-flight synthesis and pause any current audio.
+    stop()
+
+    loadingId.value = id
+
+    try {
+      let blob: Blob
+      if (source.kind === 'blob') {
+        blob = source.blob
+      }
+      else if (source.kind === 'url') {
+        synthAbort = new AbortController()
+        const res = await fetch(source.url, { signal: synthAbort.signal })
+        synthAbort = null
+        if (loadingId.value !== id) return
+        if (!res.ok) throw new Error(`fetch audio failed: ${res.status}`)
+        blob = await res.blob()
+      }
+      else {
+        const cached = ttsCache.get(id)
+        if (cached) {
+          blob = cached
+        }
+        else {
+          synthAbort = new AbortController()
+          blob = await synthesize(source.text, synthAbort.signal)
+          synthAbort = null
+          // We may have been interrupted while awaiting (loadingId changed).
+          if (loadingId.value !== id) return
+          ttsCache.set(id, blob)
+        }
+      }
+
+      const url = URL.createObjectURL(blob)
+      cachedUrls.push(url)
+
+      const audio = new Audio(url)
+      currentAudio = audio
+
+      audio.onplaying = () => {
+        if (loadingId.value === id) loadingId.value = null
+        playingId.value = id
+      }
+      audio.onended = () => {
+        if (currentAudio === audio) currentAudio = null
+        if (playingId.value === id) playingId.value = null
+      }
+      audio.onerror = () => {
+        if (currentAudio === audio) currentAudio = null
+        failPlayback(id, audio.error)
+      }
+
+      try {
+        await audio.play()
+      }
+      catch (err) {
+        if (currentAudio === audio) currentAudio = null
+        // pause() during a pending play() rejects with AbortError —
+        // that is an intentional interrupt (from stop()), not a failure.
+        if (err instanceof DOMException && err.name === 'AbortError') return
+        // Most often: autoplay policy blocked the call.
+        failPlayback(id, err)
+      }
+    }
+    catch (err) {
+      // Synthesis path errors. AbortError fires when stop() was called
+      // mid-synthesis — that is a normal interrupt, not a failure.
+      synthAbort = null
+      if (err instanceof Error && err.name === 'AbortError') {
+        // loadingId was already cleared synchronously by stop(); if a newer
+        // play() has since set it again, that value belongs to that call,
+        // not this aborted one. Do nothing here.
+        return
+      }
+      failPlayback(id, err)
+    }
+  }
+
+  function stop(): void {
+    if (synthAbort) {
+      synthAbort.abort()
+      synthAbort = null
+    }
+    clearCurrentAudio()
+    loadingId.value = null
+    playingId.value = null
+    // Note: errorId is intentionally left alone here. It is only
+    // cleared at the start of a new play() attempt.
+  }
+
+  onUnmounted(() => {
+    stop()
+    for (const url of cachedUrls) {
+      try { URL.revokeObjectURL(url) } catch { /* ignore */ }
+    }
+    cachedUrls.length = 0
+    ttsCache.clear()
+  })
+
+  return {
+    playingId,
+    loadingId,
+    errorId,
+    play,
+    stop,
+  }
+}

+ 300 - 0
src/views/Editor/EnglishSpeaking/composables/useAudioRecorder.ts

@@ -0,0 +1,300 @@
+import { ref, onUnmounted } from 'vue'
+
+/**
+ * 诊断日志说明:本文件的 `console.debug('[recorder] ...')` 用于排查"录不到声音"类问题
+ * (mic track 状态 / AudioContext 状态 / 每 50 个 chunk 的 Float32 峰值 / 结束时整体统计)。
+ * 默认在 Chrome DevTools 不显示——浏览器 Console 的 log level filter 切到 "Verbose"
+ * (或 "All levels")即可看到。Safari / Firefox 同理。
+ */
+
+export function useAudioRecorder() {
+  const isRecording = ref(false)
+  const permissionState = ref<PermissionState>('prompt')
+  const recordingDuration = ref(0)
+  const silenceDetected = ref(false)
+  /** 实际硬件采样率,startRecording 后可读;用于 WebSocket 上报给后端 */
+  const sampleRate = ref<number>(0)
+  /** 每收到一块 PCM chunk 时触发(外部可订阅做流式上传) */
+  const onChunk = ref<((pcm16: ArrayBuffer) => void) | null>(null)
+
+  let audioContext: AudioContext | null = null
+  let mediaStream: MediaStream | null = null
+  let workletNode: AudioWorkletNode | null = null
+  let pcmChunks: Float32Array[] = []
+  let durationTimer: ReturnType<typeof setInterval> | null = null
+  let silenceCheckTimer: ReturnType<typeof setInterval> | null = null
+  let analyser: AnalyserNode | null = null
+
+  // [DEBUG] 跨 start/stop 共享的录音诊断计数器
+  let debugChunkCount = 0
+  let debugPeakFloat = 0
+
+  // Check permission state
+  async function checkPermission() {
+    try {
+      const status = await navigator.permissions.query({ name: 'microphone' as PermissionName })
+      permissionState.value = status.state
+      status.onchange = () => { permissionState.value = status.state }
+    } catch {
+      // permissions API may not support microphone query in all browsers
+    }
+  }
+
+  // Silence detection using AnalyserNode
+  function startSilenceDetection(source: MediaStreamAudioSourceNode) {
+    analyser = audioContext!.createAnalyser()
+    analyser.fftSize = 512
+    source.connect(analyser)
+
+    const dataArray = new Uint8Array(analyser.frequencyBinCount)
+    let silentFrames = 0
+    const SILENCE_THRESHOLD = 10
+    const FRAMES_FOR_5S = Math.ceil(5000 / 200)
+
+    silenceCheckTimer = setInterval(() => {
+      if (!analyser) return
+      analyser.getByteFrequencyData(dataArray)
+      const average = dataArray.reduce((sum, v) => sum + v, 0) / dataArray.length
+
+      if (average < SILENCE_THRESHOLD) {
+        silentFrames++
+        if (silentFrames >= FRAMES_FOR_5S) {
+          silenceDetected.value = true
+        }
+      } else {
+        silentFrames = 0
+        silenceDetected.value = false
+      }
+    }, 200)
+  }
+
+  function stopSilenceDetection() {
+    silenceDetected.value = false
+    if (silenceCheckTimer) { clearInterval(silenceCheckTimer); silenceCheckTimer = null }
+    analyser = null
+  }
+
+  async function startRecording(signal?: AbortSignal): Promise<void> {
+    pcmChunks = []
+    silenceDetected.value = false
+
+    if (signal?.aborted) {
+      throw new DOMException('Aborted', 'AbortError')
+    }
+
+    // 获取麦克风
+    try {
+      mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
+      permissionState.value = 'granted'
+    } catch (err: any) {
+      if (err.name === 'NotAllowedError') {
+        permissionState.value = 'denied'
+      }
+      throw err
+    }
+
+    if (signal?.aborted) {
+      // User cancelled while the permission prompt was open. Release the track.
+      mediaStream.getTracks().forEach(t => t.stop())
+      mediaStream = null
+      throw new DOMException('Aborted', 'AbortError')
+    }
+
+    // 创建 AudioContext(不指定 sampleRate,用硬件默认 — iOS Safari 不支持自定义采样率)
+    audioContext = new AudioContext()
+    sampleRate.value = audioContext.sampleRate
+    const source = audioContext.createMediaStreamSource(mediaStream)
+
+    // 诊断录音管线是否健康:mic track 状态 + AudioContext 状态
+    console.debug('[recorder] startRecording', {
+      contextState: audioContext.state,
+      contextSampleRate: audioContext.sampleRate,
+      tracks: mediaStream.getAudioTracks().map(t => ({
+        label: t.label,
+        enabled: t.enabled,
+        muted: t.muted,
+        readyState: t.readyState,
+        settings: t.getSettings?.(),
+      })),
+    })
+
+    // 有些浏览器 AudioContext 默认 suspended,必须 resume 才会 tick worklet
+    if (audioContext.state === 'suspended') {
+      await audioContext.resume()
+      console.debug('[recorder] AudioContext resumed →', audioContext.state)
+    }
+
+    // 加载 AudioWorklet processor
+    await audioContext.audioWorklet.addModule('/pcm-recorder-worklet.js')
+    workletNode = new AudioWorkletNode(audioContext, 'pcm-recorder-processor')
+
+    // 重置诊断计数器(很便宜,一直追踪;只有打印受 DevTools Verbose filter 控制)
+    debugChunkCount = 0
+    debugPeakFloat = 0
+
+    // 收集 PCM 数据;同时(可选)通过 onChunk 把 int16 字节推给外部(WebSocket 流式上传)
+    workletNode.port.onmessage = (e: MessageEvent<Float32Array>) => {
+      pcmChunks.push(e.data)
+      debugChunkCount++
+      for (let i = 0; i < e.data.length; i++) {
+        const a = Math.abs(e.data[i])
+        if (a > debugPeakFloat) debugPeakFloat = a
+      }
+      // 每 50 个 chunk 打一次(≈ 128*50/48000 ≈ 133ms 一条,避免刷屏)
+      if (debugChunkCount % 50 === 0) {
+        console.debug(`[recorder] chunks=${debugChunkCount} peak_float=${debugPeakFloat.toFixed(4)}`)
+      }
+      const cb = onChunk.value
+      if (cb) {
+        const int16 = float32ToInt16(e.data)
+        cb(int16.buffer as ArrayBuffer)
+      }
+    }
+
+    source.connect(workletNode)
+    workletNode.connect(audioContext.destination) // 需要连接才能驱动处理
+
+    // 静音检测
+    startSilenceDetection(source)
+
+    isRecording.value = true
+    recordingDuration.value = 0
+    durationTimer = setInterval(() => { recordingDuration.value++ }, 1000)
+  }
+
+  async function stopRecording(): Promise<Blob> {
+    if (!workletNode || !audioContext) {
+      throw new Error('No active recording')
+    }
+
+    // 通知 worklet 停止
+    workletNode.port.postMessage('stop')
+    workletNode.disconnect()
+    workletNode = null
+
+    // 合并 PCM chunks 并按实际硬件采样率编码 WAV(后端按 WAV header 解析)
+    const sampleRate = audioContext.sampleRate
+
+    // 合并前打一条"整段录音的最终统计"—— peak_float ≈ 0 就是录音管线产零
+    let totalSamples = 0
+    for (const c of pcmChunks) totalSamples += c.length
+    console.debug('[recorder] stopRecording final', {
+      chunks: debugChunkCount,
+      totalSamples,
+      duration: (totalSamples / sampleRate).toFixed(2) + 's',
+      peakFloat: debugPeakFloat.toFixed(6),
+      contextState: audioContext.state,
+      trackMuted: mediaStream?.getAudioTracks()[0]?.muted,
+      trackEnabled: mediaStream?.getAudioTracks()[0]?.enabled,
+    })
+
+    const wavBlob = encodeWAV(pcmChunks, sampleRate)
+    pcmChunks = []
+
+    cleanup()
+    return wavBlob
+  }
+
+  function cleanup() {
+    isRecording.value = false
+    recordingDuration.value = 0
+
+    if (durationTimer) { clearInterval(durationTimer); durationTimer = null }
+    stopSilenceDetection()
+
+    if (audioContext) { audioContext.close().catch(() => {}); audioContext = null }
+    if (mediaStream) {
+      mediaStream.getTracks().forEach(t => t.stop())
+      mediaStream = null
+    }
+  }
+
+  checkPermission()
+
+  onUnmounted(() => {
+    cleanup()
+  })
+
+  return {
+    isRecording,
+    permissionState,
+    recordingDuration,
+    silenceDetected,
+    sampleRate,
+    onChunk,
+    startRecording,
+    stopRecording,
+    cleanup,
+  }
+}
+
+// ==================== Helpers ====================
+
+function float32ToInt16(float32: Float32Array): Int16Array {
+  const int16 = new Int16Array(float32.length)
+  for (let i = 0; i < float32.length; i++) {
+    const s = Math.max(-1, Math.min(1, float32[i]))
+    int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF
+  }
+  return int16
+}
+
+// ==================== WAV 编码 ====================
+
+function encodeWAV(chunks: Float32Array[], sampleRate: number): Blob {
+  // 计算总长度
+  let totalLength = 0
+  for (const chunk of chunks) totalLength += chunk.length
+
+  // 合并为单个 Float32Array
+  const pcm = new Float32Array(totalLength)
+  let offset = 0
+  for (const chunk of chunks) {
+    pcm.set(chunk, offset)
+    offset += chunk.length
+  }
+
+  // Float32 → Int16
+  const int16 = float32ToInt16(pcm)
+
+  // 写 WAV header + data
+  const numChannels = 1
+  const bitsPerSample = 16
+  const byteRate = sampleRate * numChannels * (bitsPerSample / 8)
+  const blockAlign = numChannels * (bitsPerSample / 8)
+  const dataSize = int16.length * (bitsPerSample / 8)
+
+  const buffer = new ArrayBuffer(44 + dataSize)
+  const view = new DataView(buffer)
+
+  // RIFF header
+  writeString(view, 0, 'RIFF')
+  view.setUint32(4, 36 + dataSize, true)
+  writeString(view, 8, 'WAVE')
+
+  // fmt chunk
+  writeString(view, 12, 'fmt ')
+  view.setUint32(16, 16, true)           // chunk size
+  view.setUint16(20, 1, true)            // PCM format
+  view.setUint16(22, numChannels, true)
+  view.setUint32(24, sampleRate, true)
+  view.setUint32(28, byteRate, true)
+  view.setUint16(32, blockAlign, true)
+  view.setUint16(34, bitsPerSample, true)
+
+  // data chunk
+  writeString(view, 36, 'data')
+  view.setUint32(40, dataSize, true)
+
+  // PCM data
+  const int16View = new Int16Array(buffer, 44)
+  int16View.set(int16)
+
+  return new Blob([buffer], { type: 'audio/wav' })
+}
+
+function writeString(view: DataView, offset: number, str: string) {
+  for (let i = 0; i < str.length; i++) {
+    view.setUint8(offset + i, str.charCodeAt(i))
+  }
+}

+ 615 - 0
src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts

@@ -0,0 +1,615 @@
+import { ref, reactive, computed, onUnmounted } from 'vue'
+import type { PreviewChatMessage, DialogueAPI, DialogueReport, HistoricalDialogueMessage } from '@/types/englishSpeaking'
+import { createDialogueApi, DialogueApiError } from '../services/llmService'
+import { buildSpeakingWsUrl } from '../services/speakingApiConfig'
+
+export function useDialogueEngine() {
+  const messages = ref<PreviewChatMessage[]>([])
+  const sessionId = ref<string | null>(null)
+  const expiresAt = ref<string | null>(null)
+  const currentRound = ref(1)
+  const totalRounds = ref<number>(3)
+  const isComplete = ref(false)
+  const countdownSeconds = ref<number | null>(null)
+
+  const api: DialogueAPI = createDialogueApi()
+  let currentAbortController: AbortController | null = null
+  let countdownTimer: ReturnType<typeof setInterval> | null = null
+
+  const isProcessing = computed(() => messages.value.some(m => m.status === 'loading'))
+  const canRecord = computed(() => !isProcessing.value && !isComplete.value)
+  const isFinalRound = computed(() => currentRound.value >= totalRounds.value)
+
+  // ==================== Session Attach ====================
+
+  const greetingInflight = ref(false)
+  let greetingAbortController: AbortController | null = null
+
+  /** 仅附加已建好的 session 到 engine,不发任何网络请求。
+   *  时间和轮次是两个并存的结束条件,先满足谁就结束。
+   *  expiresAt 由后端在 createSession 时基于 durationMinutes 计算下发,前端只读 ——
+   *  保证关页面/刷新/换设备重进都能接续同一个截止时刻,倒计时不暂停。
+   */
+  function toPreviewMessage(message: HistoricalDialogueMessage): PreviewChatMessage {
+    return {
+      id: message.id,
+      role: message.role,
+      content: message.content,
+      timestamp: new Date(),
+      status: 'done',
+      turnId: message.clientTurnId ?? undefined,
+      audioUrl: message.audioUrl ?? undefined,
+      audioDuration: message.audioDuration ?? undefined,
+    }
+  }
+
+  function attachSession(info: {
+    sessionId: string
+    expiresAt?: string | null
+    totalRounds: number
+    currentRound?: number
+    messages?: HistoricalDialogueMessage[]
+  }) {
+    sessionId.value = info.sessionId
+    expiresAt.value = info.expiresAt ?? null
+    totalRounds.value = info.totalRounds
+    currentRound.value = info.currentRound ?? 1
+    messages.value = (info.messages ?? []).map(toPreviewMessage)
+    if (info.expiresAt) startCountdown(info.expiresAt)
+  }
+
+  /** 执行开场白生成的私有帮手:push loading 占位 AI 消息 → 请求 /greeting → 成功填内容,失败标 error。
+   *  @param turnId 本次生成的轮次 ID,用于后端幂等性查找。
+   */
+  async function _runGreeting(turnId: string) {
+    if (!sessionId.value || greetingInflight.value) return
+    greetingInflight.value = true
+    greetingAbortController = new AbortController()
+
+    const aiMsg = reactive<PreviewChatMessage>({
+      id: crypto.randomUUID(),
+      role: 'ai',
+      content: '',
+      timestamp: new Date(),
+      status: 'loading',
+      turnId,
+    })
+    messages.value.push(aiMsg)
+
+    try {
+      const { aiMessage } = await api.generateGreeting(sessionId.value, turnId, greetingAbortController.signal)
+      aiMsg.content = aiMessage
+      aiMsg.status = 'done'
+    } catch (err: unknown) {
+      if (err instanceof Error && err.name === 'AbortError') return  // 组件卸载:不改 UI
+      aiMsg.status = 'error'
+      const status = err instanceof DialogueApiError ? err.status : undefined
+      const raw = err instanceof Error ? err.message : undefined
+      aiMsg.error = friendlyErrorMessage(raw)
+      aiMsg.recovery = classifyError(raw, status, 'ai')
+    } finally {
+      greetingInflight.value = false
+      greetingAbortController = null
+    }
+  }
+
+  /** 触发开场白生成:分配新的 turnId,调用 _runGreeting。 */
+  async function generateGreeting() {
+    await _runGreeting(crypto.randomUUID())
+  }
+
+  /** 重试失败的开场白:移除 error 消息,复用失败消息的 turnId 再走一遍(后端幂等性查找)。
+   *  如果老消息没有 turnId(不应该发生,但防守性),分配新的。 */
+  async function retryGreeting() {
+    if (greetingInflight.value) return
+    const firstAi = messages.value.find(m => m.role === 'ai')
+    if (firstAi?.status !== 'error' || firstAi.recovery === 'restart') return
+    const reuseTurnId = firstAi.turnId ?? crypto.randomUUID()
+    messages.value = messages.value.filter(m => m.id !== firstAi.id)
+    await _runGreeting(reuseTurnId)
+  }
+
+  // ==================== Send Message ====================
+
+  async function sendStudentMessage(audioBlob: Blob, turnId: string) {
+    if (!sessionId.value || isProcessing.value) return
+
+    // 只 push student 占位。aiMsg 留到 transcript 事件到达再 push——避免 AI typing-bubble
+    // 抢在 student 转录文本前出现。学生 loading 状态下由 student 自己的 typing-bubble 占位。
+    const studentMsg = reactive<PreviewChatMessage>({
+      id: crypto.randomUUID(),
+      role: 'student',
+      content: '',
+      timestamp: new Date(),
+      status: 'loading',
+      audioBlob,
+      turnId,
+    })
+    messages.value.push(studentMsg)
+
+    const aiMsg = reactive<PreviewChatMessage>({
+      id: crypto.randomUUID(),
+      role: 'ai',
+      content: '',
+      timestamp: new Date(),
+      status: 'loading',
+      turnId,
+    })
+
+    currentAbortController = new AbortController()
+
+    try {
+      const generator = api.speak(sessionId.value, audioBlob, currentAbortController.signal, turnId)
+
+      for await (const event of generator) {
+        if (event.type === 'transcript') {
+          studentMsg.content = event.text
+          studentMsg.status = 'done'
+          if (event.audioDuration != null) {
+            studentMsg.audioDuration = event.audioDuration
+          }
+          if (!isFinalRound.value) {
+            messages.value.push(aiMsg)
+          }
+        } else if (event.type === 'token') {
+          aiMsg.content += event.text
+        } else if (event.type === 'done') {
+          aiMsg.status = 'done'
+          isComplete.value = event.isComplete
+
+          if (!event.isComplete) {
+            currentRound.value++
+          }
+        } else if (event.type === 'error') {
+          studentMsg.status = 'error'
+          studentMsg.error = friendlyErrorMessage(event.message)
+          studentMsg.recovery = classifyError(event.message, undefined, 'student')
+          const aiIdx = messages.value.indexOf(aiMsg)
+          if (aiIdx !== -1) messages.value.splice(aiIdx, 1)
+          return
+        }
+      }
+
+      // If student message never got transcript, mark done with fallback
+      if (studentMsg.status === 'loading') {
+        studentMsg.status = 'done'
+      }
+      if (aiMsg.status === 'loading') {
+        aiMsg.status = 'done'
+      }
+    } catch (err: any) {
+      if (err.name === 'AbortError') return
+      const status: number | undefined = err instanceof DialogueApiError ? err.status : undefined
+      if (studentMsg.status === 'loading') {
+        studentMsg.status = 'error'
+        studentMsg.error = friendlyErrorMessage(err.message)
+        studentMsg.recovery = classifyError(err.message, status, 'student')
+      } else if (aiMsg.status === 'loading') {
+        aiMsg.status = 'error'
+        aiMsg.error = friendlyErrorMessage(err.message)
+        aiMsg.recovery = classifyError(err.message, status, 'ai')
+      }
+    } finally {
+      currentAbortController = null
+    }
+  }
+
+  // ==================== Retry / Regenerate ====================
+
+  async function retryMessage(messageId: string) {
+    const msg = messages.value.find(m => m.id === messageId)
+    if (!msg || msg.status !== 'error') return
+
+    if (msg.role === 'student' && msg.audioBlob && msg.turnId) {
+      // Remove the failed student message and any subsequent AI message
+      const idx = messages.value.indexOf(msg)
+      messages.value.splice(idx)
+      await sendStudentMessage(msg.audioBlob, msg.turnId)
+    }
+  }
+
+  async function regenerateAiMessage(messageId: string) {
+    const msg = messages.value.find(m => m.id === messageId)
+    if (!msg || msg.role !== 'ai' || msg.status !== 'error') return
+
+    // Find the student message before this AI message
+    const idx = messages.value.indexOf(msg)
+    const prevStudent = messages.value.slice(0, idx).reverse().find(m => m.role === 'student')
+    if (!prevStudent?.audioBlob || !sessionId.value || !prevStudent.turnId) return
+
+    // Remove the failed AI message
+    messages.value.splice(idx, 1)
+
+    // Re-add AI placeholder and stream
+    const aiMsg = reactive<PreviewChatMessage>({
+      id: crypto.randomUUID(),
+      role: 'ai',
+      content: '',
+      timestamp: new Date(),
+      status: 'loading',
+      turnId: prevStudent.turnId,
+    })
+    messages.value.push(aiMsg)
+
+    currentAbortController = new AbortController()
+
+    try {
+      const generator = api.speak(
+        sessionId.value, prevStudent.audioBlob,
+        currentAbortController.signal, prevStudent.turnId,
+      )
+
+      for await (const event of generator) {
+        if (event.type === 'transcript') {
+          // Skip transcript on regenerate, student message already exists
+        } else if (event.type === 'token') {
+          aiMsg.content += event.text
+        } else if (event.type === 'done') {
+          aiMsg.status = 'done'
+          isComplete.value = event.isComplete
+          if (!event.isComplete) currentRound.value++
+        }
+      }
+
+      if (aiMsg.status === 'loading') aiMsg.status = 'done'
+    } catch (err: any) {
+      if (err.name === 'AbortError') return
+      aiMsg.status = 'error'
+      aiMsg.error = friendlyErrorMessage(err.message)
+      const status = err instanceof DialogueApiError ? err.status : undefined
+      aiMsg.recovery = classifyError(err.message, status, 'ai')
+    } finally {
+      currentAbortController = null
+    }
+  }
+
+  // ==================== Report ====================
+
+  async function waitForReport(sessionId: string): Promise<DialogueReport> {
+    const maxAttempts = 30
+    const intervalMs = 2000
+    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+      const report = await api.getReport(sessionId)
+      if (report.status === 'ready' || report.status === 'failed' || report.status === 'incomplete') {
+        return report
+      }
+      await new Promise(resolve => setTimeout(resolve, intervalMs))
+    }
+    throw new Error('Report generation timed out')
+  }
+
+  async function getReport(): Promise<DialogueReport> {
+    if (!sessionId.value) return Promise.reject(new Error('No session'))
+
+    const report = await waitForReport(sessionId.value!)
+    return report
+  }
+
+  async function completeSession(): Promise<void> {
+    if (!sessionId.value) return
+    await api.completeSession(sessionId.value)
+  }
+
+  // ==================== Countdown ====================
+
+  function startCountdown(expiresAtStr: string) {
+    stopCountdown()
+
+    const update = () => {
+      const remaining = Math.max(0, Math.floor((new Date(expiresAtStr).getTime() - Date.now()) / 1000))
+      countdownSeconds.value = remaining
+      if (remaining <= 0) {
+        stopCountdown()
+        isComplete.value = true
+      }
+    }
+
+    update()
+    countdownTimer = setInterval(update, 1000)
+  }
+
+  function stopCountdown() {
+    if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
+    countdownSeconds.value = null
+  }
+
+  // ==================== Abort ====================
+
+  function abort() {
+    currentAbortController?.abort()
+    currentAbortController = null
+  }
+
+  // ==================== Streaming Speak (WebSocket) ====================
+
+  /** 流式开始:打开 WS,返回 { pushChunk, commit, abort };占位消息在 commit() 时才 push */
+  function beginStudentStream(opts: {
+    sampleRate: number
+    bits?: number
+    channels?: number
+  }) {
+    if (!sessionId.value || isProcessing.value) return null
+
+    const turnId = crypto.randomUUID()
+
+    // No messages pushed yet. They are pushed in commit().
+    let studentMsg: PreviewChatMessage | null = null
+    let aiMsg: PreviewChatMessage | null = null
+
+    const wsUrl = buildSpeakingWsUrl('/speak-stream')
+    const ws = new WebSocket(wsUrl)
+    ws.binaryType = 'arraybuffer'
+
+    let aborted = false
+    let committed = false
+    let pendingCommitBlob: Blob | null = null
+    let openWaitTimer: ReturnType<typeof setTimeout> | null = null
+    let chunkQueue: ArrayBuffer[] = []
+    let open = false
+
+    const clearOpenWaitTimer = () => {
+      if (openWaitTimer) {
+        clearTimeout(openWaitTimer)
+        openWaitTimer = null
+      }
+    }
+
+    const pushPlaceholders = (blob: Blob) => {
+      studentMsg = reactive<PreviewChatMessage>({
+        id: crypto.randomUUID(),
+        role: 'student',
+        content: '',
+        timestamp: new Date(),
+        status: 'loading',
+        audioBlob: blob,
+        turnId,
+      })
+      messages.value.push(studentMsg)
+
+      // aiMsg 创建但暂不 push——等 transcript 事件到达后再插入聊天列表。
+      // 这样 student loading 阶段不会有 AI typing-bubble 抢先显示。
+      if (!isFinalRound.value) {
+        aiMsg = reactive<PreviewChatMessage>({
+          id: crypto.randomUUID(),
+          role: 'ai',
+          content: '',
+          timestamp: new Date(),
+          status: 'loading',
+          turnId,
+        })
+      }
+    }
+
+    const finalizeError = (raw: string) => {
+      const text = friendlyErrorMessage(raw)
+      if (studentMsg && studentMsg.status === 'loading') {
+        studentMsg.status = 'error'
+        studentMsg.error = text
+        studentMsg.recovery = classifyError(raw, undefined, 'student')
+        // 学生侧出错时把 ai 占位移除,避免孤立 typing-bubble。
+        if (aiMsg && aiMsg.status === 'loading') {
+          const idx = messages.value.indexOf(aiMsg)
+          if (idx !== -1) messages.value.splice(idx, 1)
+        }
+      } else if (aiMsg && aiMsg.status === 'loading') {
+        aiMsg.status = 'error'
+        aiMsg.error = text
+        aiMsg.recovery = classifyError(raw, undefined, 'ai')
+      }
+    }
+
+    ws.onopen = () => {
+      if (aborted) {
+        try { ws.close() } catch { /* ignore */ }
+        return
+      }
+      open = true
+      clearOpenWaitTimer()
+      ws.send(JSON.stringify({
+        type: 'start',
+        sessionId: sessionId.value,
+        turnId,
+        sampleRate: opts.sampleRate,
+        bits: opts.bits ?? 16,
+        channels: opts.channels ?? 1,
+      }))
+      for (const c of chunkQueue) ws.send(c)
+      chunkQueue = []
+      if (pendingCommitBlob) {
+        const blob = pendingCommitBlob
+        pendingCommitBlob = null
+        pushPlaceholders(blob)
+        ws.send(JSON.stringify({ type: 'stop' }))
+      }
+    }
+
+    ws.onmessage = (e: MessageEvent) => {
+      if (!committed) return  // tokens shouldn't arrive before commit; defensive
+      try {
+        const data = JSON.parse(e.data)
+        if (data.type === 'transcript' && studentMsg) {
+          studentMsg.content = data.text
+          studentMsg.status = 'done'
+          if (data.audioDuration != null) {
+            studentMsg.audioDuration = data.audioDuration
+          }
+          if (aiMsg && !messages.value.includes(aiMsg)) {
+            messages.value.push(aiMsg)
+          }
+        }
+        else if (data.type === 'token' && aiMsg) {
+          aiMsg.content += data.content
+        }
+        else if (data.type === 'done') {
+          if (aiMsg) aiMsg.status = 'done'
+          else if (studentMsg && studentMsg.status === 'loading') studentMsg.status = 'done'
+          isComplete.value = !!data.isComplete
+          if (!data.isComplete) currentRound.value++
+          ws.close()
+        }
+        else if (data.type === 'error') {
+          finalizeError(data.message)
+          ws.close()
+        }
+      } catch { /* ignore */ }
+    }
+
+    ws.onerror = () => {
+      if (aborted) return
+      if (pendingCommitBlob) {
+        // commit 已发但 ws 仍 CONNECTING → 直接出错给用户,不再自动降级 HTTP。
+        const blob = pendingCommitBlob
+        pendingCommitBlob = null
+        clearOpenWaitTimer()
+        pushPlaceholders(blob)
+        finalizeError('WebSocket error')
+        try { ws.close() } catch { /* ignore */ }
+        return
+      }
+      if (committed) finalizeError('WebSocket error')
+      // commit 之前断开:什么都不做,等用户按完成时由 commit 处理 readyState。
+    }
+    ws.onclose = () => {
+      if (aborted) return
+      if (pendingCommitBlob) {
+        const blob = pendingCommitBlob
+        pendingCommitBlob = null
+        clearOpenWaitTimer()
+        pushPlaceholders(blob)
+        finalizeError('Connection closed')
+        return
+      }
+      if (!committed) return
+      if (studentMsg?.status === 'loading') finalizeError('Connection closed')
+      else if (aiMsg?.status === 'loading') finalizeError('Connection closed')
+    }
+
+    const pushChunk = (chunk: ArrayBuffer) => {
+      if (aborted) return
+      if (open && ws.readyState === WebSocket.OPEN) ws.send(chunk)
+      else chunkQueue.push(chunk)
+    }
+
+    const commit = (blob: Blob) => {
+      if (committed) return
+      committed = true
+
+      if (open && ws.readyState === WebSocket.OPEN) {
+        pushPlaceholders(blob)
+        ws.send(JSON.stringify({ type: 'stop' }))
+        return
+      }
+
+      if (ws.readyState === WebSocket.CONNECTING) {
+        // 连接中按完成:等 onopen 至多 2.5s。超时还没来 = 直接报错给用户重试。
+        pendingCommitBlob = blob
+        openWaitTimer = setTimeout(() => {
+          if (!pendingCommitBlob) return
+          const b = pendingCommitBlob
+          pendingCommitBlob = null
+          try { ws.close() } catch { /* ignore */ }
+          pushPlaceholders(b)
+          finalizeError('Connection closed')
+        }, 2500)
+        return
+      }
+
+      // CLOSING / CLOSED:commit 时 ws 已断开 → 直接报错。
+      pushPlaceholders(blob)
+      finalizeError('Connection closed')
+    }
+
+    const abort = () => {
+      aborted = true
+      pendingCommitBlob = null
+      clearOpenWaitTimer()
+      try { ws.close() } catch { /* ignore */ }
+      // No messages were pushed (commit not called) → nothing to clean up.
+    }
+
+    return { turnId, pushChunk, commit, abort }
+  }
+
+  /**
+   * 丢弃当前轮次的所有消息(student + ai),用于"重录"按钮。
+   * 通过 turnId 精确定位同一轮的两条消息并一并移除。
+   */
+  function discardCurrentTurn(messageId: string) {
+    const msg = messages.value.find(m => m.id === messageId)
+    if (!msg) return
+    const turnId = msg.turnId
+    if (!turnId) {
+      // Defensive: messages without turnId (legacy / greeting) can't be turn-grouped.
+      messages.value = messages.value.filter(m => m.id !== messageId)
+      return
+    }
+    // Splice out both rows of the turn (student + ai) sharing this turnId.
+    messages.value = messages.value.filter(m => m.turnId !== turnId)
+  }
+
+  // ==================== Cleanup ====================
+
+  onUnmounted(() => {
+    abort()
+    greetingAbortController?.abort()
+    stopCountdown()
+  })
+
+  return {
+    messages,
+    sessionId,
+    currentRound,
+    totalRounds,
+    isComplete,
+    isProcessing,
+    canRecord,
+    countdownSeconds,
+    greetingInflight,
+    isFinalRound,
+
+    attachSession,
+    generateGreeting,
+    retryGreeting,
+    sendStudentMessage,
+    beginStudentStream,
+    retryMessage,
+    regenerateAiMessage,
+    discardCurrentTurn,
+    getReport,
+    completeSession,
+    abort,
+  }
+}
+
+// ==================== Helpers ====================
+
+/** 把后端英文错误转成面向用户的中文友好文案 */
+function friendlyErrorMessage(raw: string | undefined): string {
+  const map: Record<string, string> = {
+    'No speech detected': '没听清,请再说一次',
+    'Session not found': '会话已失效,请刷新页面重新开始',
+    'Session is not active': '会话已结束',
+    'sessionId required': '会话参数缺失',
+    'First message must be start': '协议异常,请刷新重试',
+    'Invalid start payload': '协议异常,请刷新重试',
+    'Internal error': '服务暂时不可用,请稍后重试',
+    'WebSocket error': '网络连接异常',
+    'Connection closed': '连接中断,请重试',
+    'Aborted': '已取消',
+  }
+  if (!raw) return '请求失败,请重试'
+  return map[raw] || raw
+}
+
+export type Recovery = 'retry' | 'rerecord' | 'restart'
+
+export function classifyError(
+  raw: string | undefined,
+  status: number | undefined,
+  role: 'student' | 'ai',
+): Recovery {
+  if (status === 404 || status === 409) return 'restart'
+  if (raw === 'Session not found' || raw === 'Session is not active') return 'restart'
+  if (raw === 'No speech detected' && role === 'student') return 'rerecord'
+  return 'retry'
+}

+ 1082 - 0
src/views/Editor/EnglishSpeaking/configs/TopicDiscussionConfig.vue

@@ -0,0 +1,1082 @@
+<template>
+  <div class="topic-discussion-config">
+    <!-- 滚动内容区域 -->
+    <div class="config-scroll">
+
+      <!-- 主配置卡片 -->
+      <div class="config-card">
+
+        <!-- 1. 讨论话题 -->
+        <div class="form-group">
+          <label class="form-label">{{ lang.ssDiscussionTopic }}</label>
+          <input
+            type="text"
+            :value="store.config.topic"
+            @input="store.updateTopic(($event.target as HTMLInputElement).value)"
+            class="form-input"
+            :placeholder="lang.ssEnterTopic as string"
+          />
+        </div>
+
+        <!-- 2. 学习目标 -->
+        <div class="form-group">
+          <button class="section-toggle" @click="learningGoalsExpanded = !learningGoalsExpanded">
+            <span class="section-toggle-label">{{ lang.ssLearningGoals }}</span>
+            <svg
+              class="section-toggle-arrow"
+              :class="{ collapsed: !learningGoalsExpanded }"
+              width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+            >
+              <path d="M6 9l6 6 6-6" />
+            </svg>
+          </button>
+
+          <div v-if="learningGoalsExpanded" class="learning-goals-body">
+            <!-- 练习词汇 -->
+            <div class="sub-section">
+              <div class="sub-section-header">
+                <span class="sub-label">{{ lang.ssPracticeVocabulary }}</span>
+                <div v-if="!isAddingVocab" class="sub-actions">
+                  <button class="icon-btn" @click="isAddingVocab = true" title="添加词汇">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
+                    </svg>
+                  </button>
+                  <button class="icon-btn" @click="showBatchPaste = true" :title="lang.ssBatchPaste as string">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" />
+                      <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
+                    </svg>
+                  </button>
+                </div>
+              </div>
+              <div class="vocab-tags">
+                <span
+                  v-for="(word, index) in store.config.learningGoals.vocabulary"
+                  :key="index"
+                  class="vocab-tag"
+                >
+                  {{ word }}
+                  <button class="tag-remove" @click="store.removeVocabulary(index)">
+                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+                    </svg>
+                  </button>
+                </span>
+                <input
+                  v-if="isAddingVocab"
+                  ref="vocabInputRef"
+                  v-model="newVocab"
+                  class="vocab-inline-input"
+                  :placeholder="lang.ssAddWord as string"
+                  @keydown.enter="handleAddVocab"
+                  @keydown.escape="cancelAddVocab"
+                  @blur="handleAddVocabBlur"
+                />
+              </div>
+            </div>
+
+            <!-- 练习句型 -->
+            <div class="sub-section">
+              <div class="sub-section-header">
+                <span class="sub-label">{{ lang.ssPracticeSentences }}</span>
+                <div v-if="!isAddingSentence" class="sub-actions">
+                  <button class="icon-btn" @click="isAddingSentence = true" title="添加句型">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
+                    </svg>
+                  </button>
+                  <button class="icon-btn" @click="showBatchPaste = true" :title="lang.ssBatchPaste as string">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" />
+                      <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
+                    </svg>
+                  </button>
+                </div>
+              </div>
+              <div class="sentence-list">
+                <div
+                  v-for="(sentence, index) in store.config.learningGoals.sentences"
+                  :key="index"
+                >
+                  <!-- 编辑模式 -->
+                  <input
+                    v-if="editingSentenceIndex === index"
+                    v-model="editingSentenceValue"
+                    class="sentence-edit-input"
+                    @keydown.enter="handleSaveEditSentence"
+                    @keydown.escape="cancelEditSentence"
+                    @blur="handleSaveEditSentenceBlur"
+                  />
+                  <!-- 展示模式 -->
+                  <div v-else class="sentence-item" @dblclick="handleEditSentence(index)">
+                    <span class="sentence-text">{{ sentence }}</span>
+                    <button class="sentence-remove" @click="store.removeSentence(index)">
+                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                        <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+                      </svg>
+                    </button>
+                  </div>
+                </div>
+                <input
+                  v-if="isAddingSentence"
+                  ref="sentenceInputRef"
+                  v-model="newSentence"
+                  class="sentence-edit-input"
+                  :placeholder="lang.ssAddSentence as string"
+                  @keydown.enter="handleAddSentence"
+                  @keydown.escape="cancelAddSentence"
+                  @blur="handleAddSentenceBlur"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 分割线 -->
+        <div class="config-divider"></div>
+
+        <!-- 3. 练习方式 -->
+        <div class="form-group">
+          <label class="sub-label">{{ lang.ssPracticeMode }}</label>
+          <div class="mode-tabs">
+            <button
+              class="mode-tab"
+              :class="{ active: store.config.practice.mode === 'time' }"
+              @click="store.updatePracticeMode('time')"
+            >
+              {{ lang.ssTimeLimit }}
+            </button>
+            <button
+              class="mode-tab"
+              :class="{ active: store.config.practice.mode === 'rounds' }"
+              @click="store.updatePracticeMode('rounds')"
+            >
+              {{ lang.ssRoundLimit }}
+            </button>
+          </div>
+        </div>
+
+        <!-- 滑块 -->
+        <div class="form-group slider-group">
+          <template v-if="store.config.practice.mode === 'time'">
+            <input
+              type="range"
+              :value="store.config.practice.duration"
+              min="1" max="60" step="1"
+              class="config-slider"
+              @input="store.updateDuration(Number(($event.target as HTMLInputElement).value))"
+            />
+            <div class="slider-labels">
+              <span class="slider-min">1 {{ lang.ssMinute }}</span>
+              <span class="slider-value">{{ store.config.practice.duration }} {{ lang.ssMinute }}</span>
+              <span class="slider-max">60 {{ lang.ssMinute }}</span>
+            </div>
+          </template>
+          <template v-else>
+            <input
+              type="range"
+              :value="store.config.practice.rounds"
+              min="1" max="60" step="1"
+              class="config-slider"
+              @input="store.updateRounds(Number(($event.target as HTMLInputElement).value))"
+            />
+            <div class="slider-labels">
+              <span class="slider-min">1 {{ lang.ssRound }}</span>
+              <span class="slider-value">{{ store.config.practice.rounds }} {{ lang.ssRound }}</span>
+              <span class="slider-max">60 {{ lang.ssRound }}</span>
+            </div>
+          </template>
+        </div>
+
+        <!-- 展示学习报告 -->
+        <div class="toggle-row">
+          <span class="toggle-label">{{ lang.ssShowReport }}</span>
+          <button
+            class="toggle-switch"
+            :class="{ on: store.config.evaluation.showReport }"
+            @click="store.toggleShowReport()"
+          >
+            <span class="toggle-knob"></span>
+          </button>
+        </div>
+      </div>
+
+      <!-- 分割线 -->
+      <div class="section-spacer"></div>
+
+      <!-- 4. 高级配置 -->
+      <div class="advanced-section">
+        <button class="section-toggle" @click="advancedExpanded = !advancedExpanded">
+          <span class="section-toggle-label">{{ lang.ssAdvancedConfig }}</span>
+          <svg
+            class="section-toggle-arrow"
+            :class="{ collapsed: !advancedExpanded }"
+            width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+          >
+            <path d="M6 9l6 6 6-6" />
+          </svg>
+        </button>
+
+        <div v-if="advancedExpanded" class="advanced-body">
+          <!-- 辅助功能 -->
+          <div class="config-card sub-card">
+            <div class="sub-card-title">{{ lang.ssAssistance }}</div>
+            <div class="checkbox-group">
+              <label class="checkbox-item">
+                <input type="checkbox" :checked="store.config.practice.showEnglish" @change="store.togglePracticeSetting('showEnglish')" />
+                <span>{{ lang.ssShowEnglish }}</span>
+              </label>
+              <label class="checkbox-item">
+                <input type="checkbox" :checked="store.config.practice.taskHint" @change="store.togglePracticeSetting('taskHint')" />
+                <span>{{ lang.ssTaskHint }}</span>
+              </label>
+              <label class="checkbox-item">
+                <input type="checkbox" :checked="store.config.practice.stutterHint" @change="store.togglePracticeSetting('stutterHint')" />
+                <span>{{ lang.ssStutterHint }}</span>
+              </label>
+            </div>
+          </div>
+
+          <!-- 评估详情 -->
+          <div class="config-card sub-card">
+            <div class="sub-card-title">{{ lang.ssEvalDetails }}</div>
+
+            <!-- 评估维度 -->
+            <div class="eval-group">
+              <div class="sub-label">{{ lang.ssEvalDimensions }}</div>
+              <div class="checkbox-group">
+                <label class="checkbox-item">
+                  <input type="checkbox" :checked="store.config.evaluation.dimensions.accuracy" @change="store.toggleEvalDimension('accuracy')" />
+                  <span>{{ lang.ssAccuracy }}</span>
+                </label>
+                <label class="checkbox-item">
+                  <input type="checkbox" :checked="store.config.evaluation.dimensions.fluency" @change="store.toggleEvalDimension('fluency')" />
+                  <span>{{ lang.ssFluency }}</span>
+                </label>
+                <label class="checkbox-item">
+                  <input type="checkbox" :checked="store.config.evaluation.dimensions.completeness" @change="store.toggleEvalDimension('completeness')" />
+                  <span>{{ lang.ssCompleteness }}</span>
+                </label>
+                <label class="checkbox-item">
+                  <input type="checkbox" :checked="store.config.evaluation.dimensions.rhythm" @change="store.toggleEvalDimension('rhythm')" />
+                  <span>{{ lang.ssRhythm }}</span>
+                </label>
+              </div>
+            </div>
+
+            <!-- 评分方式 -->
+            <div class="eval-group">
+              <div class="sub-label">{{ lang.ssScoreMode }}</div>
+              <div class="radio-group">
+                <label class="radio-item" @click="store.updateScoreMode('letter')">
+                  <span class="radio-circle" :class="{ active: store.config.evaluation.scoreMode === 'letter' }">
+                    <span v-if="store.config.evaluation.scoreMode === 'letter'" class="radio-dot"></span>
+                  </span>
+                  <span>{{ lang.ssLetterScore }}</span>
+                </label>
+                <label class="radio-item" @click="store.updateScoreMode('numeric')">
+                  <span class="radio-circle" :class="{ active: store.config.evaluation.scoreMode === 'numeric' }">
+                    <span v-if="store.config.evaluation.scoreMode === 'numeric'" class="radio-dot"></span>
+                  </span>
+                  <span>{{ lang.ssNumericScore }}</span>
+                </label>
+                <label class="radio-item" @click="store.updateScoreMode('comment_only')">
+                  <span class="radio-circle" :class="{ active: store.config.evaluation.scoreMode === 'comment_only' }">
+                    <span v-if="store.config.evaluation.scoreMode === 'comment_only'" class="radio-dot"></span>
+                  </span>
+                  <span>{{ lang.ssCommentOnly }}</span>
+                </label>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部留白 -->
+      <div class="bottom-spacer"></div>
+    </div>
+
+    <!-- 底部操作栏 -->
+    <div class="config-footer">
+      <button class="btn-apply" @click="handleApply">
+        {{ lang.ssApplyConfig }}
+      </button>
+    </div>
+
+    <!-- 批量粘贴弹窗 -->
+    <div v-if="showBatchPaste" class="batch-paste-overlay" @click.self="showBatchPaste = false">
+      <div class="batch-paste-dialog">
+        <h3 class="dialog-title">{{ lang.ssBatchPasteTitle }}</h3>
+        <div class="dialog-body">
+          <div class="dialog-field">
+            <label class="dialog-label">{{ lang.ssBatchPasteVocab }}</label>
+            <textarea v-model="batchVocab" class="dialog-textarea" rows="4"></textarea>
+          </div>
+          <div class="dialog-field">
+            <label class="dialog-label">{{ lang.ssBatchPasteSentences }}</label>
+            <textarea v-model="batchSentences" class="dialog-textarea" rows="4"></textarea>
+          </div>
+        </div>
+        <div class="dialog-footer">
+          <button class="btn-cancel" @click="showBatchPaste = false">{{ lang.ssCancel }}</button>
+          <button class="btn-confirm" @click="handleBatchPasteConfirm">{{ lang.ssConfirm }}</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, nextTick } from 'vue'
+import { storeToRefs } from 'pinia'
+import { lang } from '@/main'
+import { useSpeakingStore } from '@/store/speaking'
+import { useSlidesStore } from '@/store'
+import useCreateElement from '@/hooks/useCreateElement'
+import { createSpeakingConfig, updateSpeakingConfig } from '@/services/speaking'
+import message from '@/utils/message'
+
+const emit = defineEmits<{
+  (e: 'back'): void
+}>()
+
+const store = useSpeakingStore()
+
+// UI 状态
+const learningGoalsExpanded = ref(true)
+const advancedExpanded = ref(false)
+
+// 词汇新增
+const isAddingVocab = ref(false)
+const newVocab = ref('')
+const vocabInputRef = ref<HTMLInputElement | null>(null)
+
+// 句型新增 / 编辑
+const isAddingSentence = ref(false)
+const newSentence = ref('')
+const sentenceInputRef = ref<HTMLInputElement | null>(null)
+const editingSentenceIndex = ref<number | null>(null)
+const editingSentenceValue = ref('')
+
+// 批量粘贴
+const showBatchPaste = ref(false)
+const batchVocab = ref('')
+const batchSentences = ref('')
+
+// 词汇操作
+const handleAddVocab = () => {
+  if (newVocab.value.trim()) {
+    store.addVocabulary(newVocab.value.trim())
+    newVocab.value = ''
+  }
+}
+
+const handleAddVocabBlur = () => {
+  if (newVocab.value.trim()) {
+    handleAddVocab()
+  }
+  isAddingVocab.value = false
+}
+
+const cancelAddVocab = () => {
+  isAddingVocab.value = false
+  newVocab.value = ''
+}
+
+// 句型操作
+const handleAddSentence = () => {
+  if (newSentence.value.trim()) {
+    store.addSentence(newSentence.value.trim())
+    newSentence.value = ''
+  }
+}
+
+const handleAddSentenceBlur = () => {
+  if (newSentence.value.trim()) {
+    handleAddSentence()
+  }
+  isAddingSentence.value = false
+}
+
+const cancelAddSentence = () => {
+  isAddingSentence.value = false
+  newSentence.value = ''
+}
+
+const handleEditSentence = (index: number) => {
+  editingSentenceIndex.value = index
+  editingSentenceValue.value = store.config.learningGoals.sentences[index]
+}
+
+const handleSaveEditSentence = () => {
+  if (editingSentenceIndex.value !== null && editingSentenceValue.value.trim()) {
+    store.updateSentence(editingSentenceIndex.value, editingSentenceValue.value.trim())
+    editingSentenceIndex.value = null
+    editingSentenceValue.value = ''
+  }
+}
+
+const handleSaveEditSentenceBlur = () => {
+  if (editingSentenceValue.value.trim()) {
+    handleSaveEditSentence()
+  } else {
+    cancelEditSentence()
+  }
+}
+
+const cancelEditSentence = () => {
+  editingSentenceIndex.value = null
+  editingSentenceValue.value = ''
+}
+
+// 批量粘贴
+const handleBatchPasteConfirm = () => {
+  const vocabItems = batchVocab.value.split('\n').map(s => s.trim()).filter(Boolean)
+  const sentenceItems = batchSentences.value.split('\n').map(s => s.trim()).filter(Boolean)
+  vocabItems.forEach(item => store.addVocabulary(item))
+  sentenceItems.forEach(item => store.addSentence(item))
+  batchVocab.value = ''
+  batchSentences.value = ''
+  showBatchPaste.value = false
+}
+
+// 应用配置 → 更新画布上已有 77 型 frame 关联的配置
+// 新交互流程下,元素由 Layer2 点卡片即创建;此处"应用"只负责 PUT 更新到后端
+const { createFrameElement } = useCreateElement()
+const { currentSlide } = storeToRefs(useSlidesStore())
+const applying = ref(false)
+
+const handleApply = async () => {
+  if (applying.value) return
+  applying.value = true
+  try {
+    const existing = currentSlide.value?.elements?.find(
+      (el: any) => el.type === 'frame' && Number(el.toolType) === 77
+    ) as { url?: string } | undefined
+
+    if (existing?.url) {
+      await updateSpeakingConfig(existing.url, store.config)
+      message.success('配置已更新')
+    } else {
+      // 兜底:画布上没有 77 型 frame(用户删了或从未创建),此时先 POST + 插入
+      const { id } = await createSpeakingConfig(store.config)
+      createFrameElement(id, 77)
+      message.success('已创建口语工具')
+    }
+  } catch (err: any) {
+    console.error('[speaking] apply failed:', err)
+    message.error(err?.message || '保存配置失败')
+  } finally {
+    applying.value = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.topic-discussion-config {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.config-scroll {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+// 配置卡片
+.config-card {
+  background: #fff;
+  border: 1px solid #e5e7eb;
+  border-radius: 12px;
+  padding: 16px;
+}
+
+.config-divider {
+  height: 1px;
+  background: #f3f4f6;
+  margin: 16px 0;
+}
+
+.section-spacer {
+  height: 1px;
+  background: #f3f4f6;
+  margin: 16px 0;
+}
+
+.bottom-spacer {
+  height: 24px;
+}
+
+// 表单元素
+.form-group {
+  margin-bottom: 12px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.form-label {
+  display: block;
+  font-size: 12px;
+  font-weight: 600;
+  color: #374151;
+  margin-bottom: 6px;
+}
+
+.form-input {
+  width: 100%;
+  height: 36px;
+  padding: 0 12px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #374151;
+  background: #fff;
+  transition: all 0.2s;
+  box-sizing: border-box;
+
+  &:focus {
+    outline: none;
+    border-color: #f97316;
+  }
+
+  &::placeholder {
+    color: #9ca3af;
+  }
+}
+
+// 折叠切换
+.section-toggle {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 4px 0;
+  background: none;
+  border: none;
+  cursor: pointer;
+  transition: color 0.2s;
+
+  &:hover {
+    .section-toggle-label {
+      color: #111827;
+    }
+  }
+}
+
+.section-toggle-label {
+  font-size: 12px;
+  font-weight: 600;
+  color: #374151;
+}
+
+.section-toggle-arrow {
+  color: #9ca3af;
+  transition: transform 0.2s;
+
+  &.collapsed {
+    transform: rotate(-90deg);
+  }
+}
+
+// 学习目标
+.learning-goals-body {
+  margin-top: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.sub-section {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.sub-section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.sub-label {
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.sub-actions {
+  display: flex;
+  gap: 6px;
+}
+
+.icon-btn {
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px dashed #d1d5db;
+  border-radius: 4px;
+  background: none;
+  color: #9ca3af;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    border-color: #f97316;
+    color: #f97316;
+    background: #fff7ed;
+  }
+}
+
+// 词汇标签
+.vocab-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.vocab-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 10px;
+  background: #f9fafb;
+  border-radius: 6px;
+  font-size: 12px;
+  color: #374151;
+  transition: background 0.2s;
+
+  &:hover {
+    background: #f3f4f6;
+
+    .tag-remove {
+      opacity: 1;
+    }
+  }
+}
+
+.tag-remove {
+  display: flex;
+  background: none;
+  border: none;
+  padding: 0;
+  color: #9ca3af;
+  cursor: pointer;
+  opacity: 0;
+  transition: all 0.2s;
+
+  &:hover {
+    color: #6b7280;
+  }
+}
+
+.vocab-inline-input {
+  padding: 4px 10px;
+  border: 1px solid #d1d5db;
+  border-radius: 6px;
+  font-size: 12px;
+  color: #374151;
+  width: 96px;
+
+  &:focus {
+    outline: none;
+    border-color: #9ca3af;
+  }
+}
+
+// 句型列表
+.sentence-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.sentence-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 6px 10px;
+  background: #f9fafb;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s;
+
+  &:hover {
+    background: #f3f4f6;
+
+    .sentence-remove {
+      opacity: 1;
+    }
+  }
+}
+
+.sentence-text {
+  font-size: 12px;
+  color: #374151;
+}
+
+.sentence-remove {
+  display: flex;
+  background: none;
+  border: none;
+  padding: 0;
+  color: #9ca3af;
+  cursor: pointer;
+  opacity: 0;
+  transition: all 0.2s;
+
+  &:hover {
+    color: #6b7280;
+  }
+}
+
+.sentence-edit-input {
+  width: 100%;
+  padding: 6px 10px;
+  border: 1px solid #d1d5db;
+  border-radius: 8px;
+  font-size: 12px;
+  color: #374151;
+  box-sizing: border-box;
+
+  &:focus {
+    outline: none;
+    border-color: #9ca3af;
+  }
+}
+
+// 练习方式 Tab
+.mode-tabs {
+  display: flex;
+  gap: 8px;
+}
+
+.mode-tab {
+  flex: 1;
+  height: 32px;
+  border-radius: 8px;
+  border: none;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  background: #f9fafb;
+  color: #4b5563;
+
+  &:hover:not(.active) {
+    background: #f3f4f6;
+  }
+
+  &.active {
+    background: #f97316;
+    color: #fff;
+  }
+}
+
+// 滑块
+.slider-group {
+  margin-top: 4px;
+}
+
+.config-slider {
+  width: 100%;
+  height: 6px;
+  border-radius: 3px;
+  appearance: none;
+  background: #e5e7eb;
+  cursor: pointer;
+  accent-color: #f97316;
+
+  &::-webkit-slider-thumb {
+    appearance: none;
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    background: #f97316;
+    cursor: pointer;
+    border: 2px solid #fff;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+  }
+}
+
+.slider-labels {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 6px;
+  font-size: 12px;
+}
+
+.slider-min, .slider-max {
+  color: #9ca3af;
+}
+
+.slider-value {
+  font-weight: 500;
+  color: #374151;
+}
+
+// Toggle 开关
+.toggle-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 12px;
+}
+
+.toggle-label {
+  font-size: 12px;
+  color: #4b5563;
+}
+
+.toggle-switch {
+  position: relative;
+  width: 44px;
+  height: 24px;
+  border-radius: 12px;
+  border: none;
+  cursor: pointer;
+  transition: background 0.2s;
+  background: #d1d5db;
+
+  &.on {
+    background: #f97316;
+
+    .toggle-knob {
+      left: 22px;
+    }
+  }
+}
+
+.toggle-knob {
+  position: absolute;
+  top: 4px;
+  left: 4px;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+  transition: left 0.2s;
+}
+
+// 高级配置
+.advanced-section {
+  // 外层无需额外样式
+}
+
+.advanced-body {
+  margin-top: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.sub-card {
+  padding: 12px;
+}
+
+.sub-card-title {
+  font-size: 12px;
+  font-weight: 600;
+  color: #374151;
+  margin-bottom: 10px;
+}
+
+.checkbox-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px 16px;
+}
+
+.checkbox-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  cursor: pointer;
+  font-size: 12px;
+  color: #4b5563;
+
+  input[type="checkbox"] {
+    width: 14px;
+    height: 14px;
+    accent-color: #f97316;
+  }
+}
+
+.eval-group {
+  margin-top: 10px;
+}
+
+.radio-group {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-top: 4px;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  font-size: 12px;
+  color: #374151;
+  padding: 2px 0;
+  user-select: none;
+}
+
+.radio-circle {
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  border: 2px solid #d1d5db;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: border-color 0.2s;
+
+  &.active {
+    border-color: #f97316;
+  }
+}
+
+.radio-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #f97316;
+}
+
+// 底部操作栏
+.config-footer {
+  border-top: 1px solid #f3f4f6;
+  padding: 12px 16px;
+  background: #fff;
+  flex-shrink: 0;
+}
+
+.btn-apply {
+  width: 100%;
+  height: 36px;
+  border: none;
+  border-radius: 8px;
+  background: #f97316;
+  color: #fff;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: background 0.2s;
+
+  &:hover {
+    background: #ea580c;
+  }
+}
+
+// 批量粘贴弹窗
+.batch-paste-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.batch-paste-dialog {
+  width: 360px;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+}
+
+.dialog-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #111827;
+  padding: 16px 20px;
+  border-bottom: 1px solid #f3f4f6;
+  margin: 0;
+}
+
+.dialog-body {
+  padding: 16px 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.dialog-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.dialog-label {
+  font-size: 12px;
+  font-weight: 500;
+  color: #374151;
+}
+
+.dialog-textarea {
+  width: 100%;
+  padding: 8px 12px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #374151;
+  resize: vertical;
+  font-family: inherit;
+  box-sizing: border-box;
+
+  &:focus {
+    outline: none;
+    border-color: #f97316;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  padding: 12px 20px;
+  border-top: 1px solid #f3f4f6;
+}
+
+.btn-cancel {
+  padding: 6px 16px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  background: #fff;
+  font-size: 13px;
+  color: #4b5563;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    background: #f9fafb;
+  }
+}
+
+.btn-confirm {
+  padding: 6px 16px;
+  border: none;
+  border-radius: 8px;
+  background: #f97316;
+  font-size: 13px;
+  color: #fff;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    background: #ea580c;
+  }
+}
+</style>

+ 113 - 0
src/views/Editor/EnglishSpeaking/data/curriculum.json

@@ -0,0 +1,113 @@
+{
+  "textbooks": [
+    {
+      "id": "shep",
+      "name": "沪教版(上海教育出版社)",
+      "grades": [
+        {
+          "id": "grade5-1",
+          "name": "五年级",
+          "semester": "上",
+          "units": [
+            {
+              "id": "SHEP-5-上-Unit1",
+              "number": "Unit 1",
+              "title": "My future",
+              "description": "学习描述职业和未来理想。掌握词汇 want, pilot, teach, cook, taxi driver, job, become 等;能用 \"What do you want to be?\" 询问,用 \"I want to be a pilot.\" 回答",
+              "vocabulary": ["want", "pilot", "teach", "cook", "taxi driver", "job", "become", "singer", "fall", "lifeguard", "save", "(be) good at"],
+              "sentences": ["What do you want to be?", "I want to be a pilot."]
+            },
+            {
+              "id": "SHEP-5-上-Unit2",
+              "number": "Unit 2",
+              "title": "Going to school",
+              "description": "学习描述上学方式和交通工具。掌握核心词汇 by, walk, Ms, underground, take, after, hour(目录 Key words);认读词汇包括 journey, primary school, station, bus stop, by bus, on foot, by bike, by car, get off 等;语音重点:辅音组合 sp(wasp, crisp)",
+              "vocabulary": ["by", "walk", "Ms", "underground", "take", "after", "hour", "journey", "primary school", "station", "bus stop", "by bus", "far from", "on foot", "by bike", "by car", "get off"],
+              "sentences": ["How do you come to school?", "I come to school by bus.", "I come to school on foot.", "Do you live near school?", "I live far from school.", "How long does it take?", "My father goes to work by...", "In my class, ... students come to school on foot."]
+            },
+            {
+              "id": "SHEP-5-上-Unit3",
+              "number": "Unit 3",
+              "title": "My birthday",
+              "description": "学习描述生日和日期。掌握词汇 party, when, begin, bring, thing, favourite, interesting, hat 等;能用 \"When's your birthday?\" 询问,用 \"It's on 26th September.\" 回答",
+              "vocabulary": ["party", "when", "begin", "bring", "thing", "favourite", "interesting", "hat", "have fun"],
+              "sentences": ["When's your birthday?", "It's on 26th September."]
+            },
+            {
+              "id": "SHEP-5-上-Unit4",
+              "number": "Unit 4",
+              "title": "Grandparents",
+              "description": "学习描述与祖父母的日常活动及频率。掌握词汇 usually, often, visit, sometimes, always, never 等;能用 \"What do you usually do with your grandparents?\" 询问,用 \"I usually play chess with my grandpa.\" 回答",
+              "vocabulary": ["usually", "often", "visit", "sometimes", "always", "never", "play sport", "go shopping"],
+              "sentences": ["What do you usually do with your grandparents?", "I usually play chess with my grandpa."]
+            },
+            {
+              "id": "SHEP-5-上-Unit5",
+              "number": "Unit 5",
+              "title": "Friends",
+              "description": "学习描述朋友特点和共同爱好。掌握词汇 clever, same, class, both, heavy, different, word, easy, say, ask, answer 等;能用 \"We both like sport.\" 描述共同爱好",
+              "vocabulary": ["clever", "same", "class", "both", "heavy", "different", "word", "easy", "say", "ask", "answer", "cross", "carry", "bored", "soon", "each other", "make phone calls", "tell a story"],
+              "sentences": ["We both like sport."]
+            },
+            {
+              "id": "SHEP-5-上-Unit6",
+              "number": "Unit 6",
+              "title": "Family life",
+              "description": "学习描述家庭日常生活和家居场所。掌握词汇 living room, bedroom, kitchen, bathroom, their, light, watch, TV, before 等;能用 \"I'm doing my homework.\" 描述正在进行的动作",
+              "vocabulary": ["living room", "bedroom", "kitchen", "bathroom", "their", "light", "watch", "TV", "before", "bedtime", "do homework", "turn off", "watch TV", "model plane", "life"],
+              "sentences": ["I'm doing my homework."]
+            },
+            {
+              "id": "SHEP-5-上-Unit7",
+              "number": "Unit 7",
+              "title": "At the beach",
+              "description": "学习描述海滩活动和现在进行时。掌握词汇 beach, enjoy, collect, sea, letter, put, know, year 等;能用 \"Is Paul collecting shells? Yes, he is.\" 描述他人正在进行的活动",
+              "vocabulary": ["beach", "enjoy", "collect", "sea", "letter", "put", "know", "year", "shell", "sunshine", "on holiday", "have a good time"],
+              "sentences": ["Is Paul collecting shells?", "Yes, he is."]
+            },
+            {
+              "id": "SHEP-5-上-Unit8",
+              "number": "Unit 8",
+              "title": "An outing",
+              "description": "学习描述外出活动和询问他人活动。掌握词汇 map, hill, find, another, lake, key, think 等;能用 \"What are you doing? I'm reading the map.\" 进行问答",
+              "vocabulary": ["map", "hill", "find", "another", "lake", "key", "think", "outing", "diamond", "funny", "hole", "at the top of", "get through"],
+              "sentences": ["What are you doing?", "I'm reading the map."]
+            },
+            {
+              "id": "SHEP-5-上-Unit9",
+              "number": "Unit 9",
+              "title": "Around the city",
+              "description": "学习描述城市方位和问路指路。掌握词汇 post office, quite, along, turn, left, right, between, flower shop, hospital, toy shop 等;能用 \"Walk along Winter Street. Turn left at Spring Street.\" 描述路线",
+              "vocabulary": ["post office", "quite", "along", "turn", "left", "right", "between", "flower shop", "hospital", "toy shop", "road", "get to", "straight"],
+              "sentences": ["Walk along Winter Street.", "Turn left at Spring Street."]
+            },
+            {
+              "id": "SHEP-5-上-Unit10",
+              "number": "Unit 10",
+              "title": "Wind",
+              "description": "学习描述风的特征和副词用法。掌握词汇 blow, gently, softly, strongly, happily, move, slowly, quickly, sound, paper, quiet 等;能用 \"The children are flying their kites happily.\" 描述动作方式",
+              "vocabulary": ["blow", "gently", "softly", "strongly", "happily", "move", "slowly", "quickly", "sound", "paper", "quiet", "windmill", "wind-bell", "cut"],
+              "sentences": ["The children are flying their kites happily."]
+            },
+            {
+              "id": "SHEP-5-上-Unit11",
+              "number": "Unit 11",
+              "title": "Water",
+              "description": "学习描述水的用途和自然现象。掌握词汇 use, clothes, farmer, useful, up, shine, over, tree, ground 等;能用 \"We use water to wash our hands.\" 表达用途",
+              "vocabulary": ["use", "clothes", "farmer", "useful", "up", "shine", "over", "tree", "ground", "tap", "vegetable", "drop", "mountain", "inside", "grow crops", "put out fires"],
+              "sentences": ["We use water to wash our hands."]
+            },
+            {
+              "id": "SHEP-5-上-Unit12",
+              "number": "Unit 12",
+              "title": "Fire",
+              "description": "学习描述火灾安全和情态动词 must/mustn't。掌握词汇 fire, hurt, must, careful, smoke, hate 等;能用 \"We must be careful with fire. We mustn't play with matches.\" 表达规则",
+              "vocabulary": ["fire", "hurt", "must", "careful", "smoke", "hate", "burn", "safety", "match", "heat", "burn down", "(be) careful with", "not at all"],
+              "sentences": ["We must be careful with fire.", "We mustn't play with matches."]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 52 - 0
src/views/Editor/EnglishSpeaking/data/topicDiscussionTasks.json

@@ -0,0 +1,52 @@
+{
+  "SHEP-5-上-Unit2": [
+    {
+      "id": "td-unit2-1",
+      "titleZh": "我的上学方式",
+      "titleEn": "How I get to school",
+      "subtitle": "和 AI 聊聊你每天怎么上学",
+      "difficulty": 1,
+      "rounds": 6,
+      "durationMinutes": 4,
+      "vocabulary": ["on foot", "by bus", "by bike", "by car", "by underground", "bus stop", "near", "far from"],
+      "sentences": ["How do you come to school?", "I come to school by bus.", "Do you live near school?", "I live far from school.", "How long does it take?"],
+      "note": "核心句型:How do you come to school? I come to school by/on..."
+    },
+    {
+      "id": "td-unit2-2",
+      "titleZh": "比较交通方式",
+      "titleEn": "Comparing ways to travel",
+      "subtitle": "讨论不同交通方式的优缺点",
+      "difficulty": 2,
+      "rounds": 8,
+      "durationMinutes": 5,
+      "vocabulary": ["fast", "slow", "cheap", "expensive", "safe", "convenient", "crowded", "on foot", "by bus", "by underground"],
+      "sentences": ["Which is faster, bus or underground?", "I think... is more convenient.", "Going by bike is good for health."],
+      "note": "对比 on foot, by bus, by bike, by underground 等方式"
+    },
+    {
+      "id": "td-unit2-3",
+      "titleZh": "家人的出行方式",
+      "titleEn": "How my family gets around",
+      "subtitle": "介绍家人上班上学的交通方式",
+      "difficulty": 2,
+      "rounds": 7,
+      "durationMinutes": 5,
+      "vocabulary": ["goes to work", "takes the bus", "drives", "walk to", "by underground", "far from", "near", "every day"],
+      "sentences": ["My father/mother goes to work by...", "How does your father come to work?", "I go to school on foot.", "We live near the school."],
+      "note": "句型:My father/mother goes to work by... I go to school..."
+    },
+    {
+      "id": "td-unit2-4",
+      "titleZh": "上学路上的见闻",
+      "titleEn": "Things I see on my way to school",
+      "subtitle": "分享上学路上看到的有趣事物",
+      "difficulty": 3,
+      "rounds": 8,
+      "durationMinutes": 6,
+      "vocabulary": ["bus stop", "traffic lights", "road", "cross", "walk", "along", "turn", "corner", "shop", "people"],
+      "sentences": ["On my way to school, I can see...", "I walk along...", "I turn left/right at...", "It takes me ... minutes to get to school."],
+      "note": "综合运用本单元词汇和句型,培养叙事能力"
+    }
+  ]
+}

+ 63 - 0
src/views/Editor/EnglishSpeaking/layers/Layer1Home.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="layer1-home">
+    <div class="layer1-content">
+      <!-- 教材/年级/单元联动选择 -->
+      <CurriculumSelector
+        :selectedTextbook="selectedTextbook"
+        :selectedGrade="selectedGrade"
+        :selectedUnit="selectedUnit"
+        @update:selectedTextbook="$emit('update:selectedTextbook', $event)"
+        @update:selectedGrade="$emit('update:selectedGrade', $event)"
+        @update:selectedUnit="$emit('update:selectedUnit', $event)"
+      />
+
+      <!-- 单元信息卡片 -->
+      <UnitInfoCard :unit="currentUnit" />
+
+      <!-- 练习类型网格 -->
+      <ExerciseTypeGrid @select="$emit('selectExerciseType', $event)" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import curriculumData from '../data/curriculum.json'
+import type { CurriculumData, ExerciseType } from '@/types/englishSpeaking'
+import CurriculumSelector from '../components/CurriculumSelector.vue'
+import UnitInfoCard from '../components/UnitInfoCard.vue'
+import ExerciseTypeGrid from '../components/ExerciseTypeGrid.vue'
+
+const props = defineProps<{
+  selectedTextbook: string
+  selectedGrade: string
+  selectedUnit: string
+}>()
+
+defineEmits<{
+  (e: 'update:selectedTextbook', value: string): void
+  (e: 'update:selectedGrade', value: string): void
+  (e: 'update:selectedUnit', value: string): void
+  (e: 'selectExerciseType', type: ExerciseType): void
+}>()
+
+const data = curriculumData as CurriculumData
+
+const currentUnit = computed(() => {
+  const tb = data.textbooks.find(t => t.id === props.selectedTextbook)
+  const grade = tb?.grades.find(g => g.id === props.selectedGrade)
+  return grade?.units.find(u => u.id === props.selectedUnit)
+})
+</script>
+
+<style lang="scss" scoped>
+.layer1-home {
+  padding: 16px 20px;
+}
+
+.layer1-content {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+</style>

+ 336 - 0
src/views/Editor/EnglishSpeaking/layers/Layer2Speaking.vue

@@ -0,0 +1,336 @@
+<template>
+  <div class="layer2-speaking">
+    <!-- 创建方式切换 -->
+    <CreationModeSwitch v-model="creationMode" />
+
+    <!-- AI 生成模式 -->
+    <AIGenerationForm v-if="creationMode === 'ai'" />
+
+    <!-- 手动创建模式 —— 直接进入配置页 -->
+    <div v-else-if="creationMode === 'manual'" class="manual-create">
+      <div class="manual-hint">
+        <p class="manual-text">选择一种口语任务类型开始手动创建</p>
+      </div>
+      <div class="manual-task-list">
+        <button
+          v-for="taskType in manualTaskTypes"
+          :key="taskType.id"
+          class="manual-task-item"
+          @click="handleManualCreate(taskType.id)"
+        >
+          <div class="task-item-icon" v-html="taskType.icon"></div>
+          <div class="task-item-info">
+            <span class="task-item-name">{{ taskType.name }}</span>
+            <span class="task-item-category">{{ taskType.category }}</span>
+          </div>
+          <svg class="task-item-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M9 18l6-6-6-6" />
+          </svg>
+        </button>
+      </div>
+    </div>
+
+    <!-- 智能推荐模式 -->
+    <template v-else>
+      <!-- 任务类型筛选 -->
+      <div class="filter-bar">
+        <div class="filter-left">
+          <span class="filter-label">任务类型</span>
+          <div class="filter-select-wrap">
+            <select v-model="selectedTaskType" class="filter-select">
+              <option v-for="opt in taskTypeOptions" :key="opt.id" :value="opt.id">
+                {{ opt.name }}
+              </option>
+            </select>
+            <svg class="select-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M6 9l6 6 6-6" />
+            </svg>
+          </div>
+        </div>
+      </div>
+
+      <!-- 推荐卡片列表 -->
+      <div v-if="filteredTasks.length > 0" class="card-grid">
+        <RecommendCard
+          v-for="task in filteredTasks"
+          :key="task.id"
+          :task="task"
+          @select="handleSelectTask(task)"
+        />
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="empty-state">
+        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+          <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2" />
+          <rect x="9" y="3" width="6" height="4" rx="1" />
+          <line x1="9" y1="12" x2="15" y2="12" />
+          <line x1="9" y1="16" x2="13" y2="16" />
+        </svg>
+        <p class="empty-text">{{ lang.ssNoRecommendations }}</p>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { lang } from '@/main'
+import type { CreationMode, TopicDiscussionTask } from '@/types/englishSpeaking'
+import { useSpeakingStore } from '@/store/speaking'
+import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
+import { createSpeakingConfig } from '@/services/speaking'
+import message from '@/utils/message'
+import tasksData from '../data/topicDiscussionTasks.json'
+import CreationModeSwitch from '../components/CreationModeSwitch.vue'
+import RecommendCard from '../components/RecommendCard.vue'
+import AIGenerationForm from '../components/AIGenerationForm.vue'
+
+const props = defineProps<{
+  unitId: string
+}>()
+
+const emit = defineEmits<{
+  (e: 'openConfig'): void
+}>()
+
+const speakingStore = useSpeakingStore()
+
+const creationMode = ref<CreationMode>('smart')
+const selectedTaskType = ref('all')
+
+// 任务类型筛选选项(本次仅口语 - 话题讨论)
+const taskTypeOptions = [
+  { id: 'all', name: lang.ssAll as string },
+  { id: 'topic-discussion', name: lang.ssTopicDiscussion as string },
+]
+
+// 手动创建的任务类型列表
+const manualTaskTypes = [
+  {
+    id: 'topic-discussion',
+    name: lang.ssTopicDiscussion as string,
+    category: lang.ssFreeDialogue as string,
+    icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>',
+  },
+]
+
+// 根据 unitId 获取推荐任务
+const unitTasks = computed<TopicDiscussionTask[]>(() => {
+  const data = tasksData as Record<string, TopicDiscussionTask[]>
+  return data[props.unitId] || []
+})
+
+// 按任务类型筛选
+const filteredTasks = computed(() => {
+  if (selectedTaskType.value === 'all') return unitTasks.value
+  // 目前所有数据都是话题讨论类型,所以 topic-discussion 返回全部
+  return unitTasks.value
+})
+
+// 新交互:点卡片即新建一页 + 创建配置 + 插入画布元素;配置页改由"点击画布里的 77 型元素"唤起
+const { createFrameElement } = useCreateElement()
+const { createSlide } = useSlideHandler()
+const inserting = ref(false)
+
+async function insertSpeakingToolToCanvas(source: 'select' | 'manual') {
+  if (inserting.value) return
+  inserting.value = true
+  try {
+    const { id } = await createSpeakingConfig(speakingStore.config)
+    createSlide()
+    createFrameElement(id, 77)
+    // 创建后立即唤起左侧配置面板(与点击画布 77 型 frame 同款信号),使用户可直接编辑
+    speakingStore.openConfigPanel()
+    message.success(source === 'select' ? '已添加口语练习到幻灯片' : '已添加空白口语工具,点击画布上的元素进行配置')
+  } catch (err: any) {
+    console.error('[speaking] create failed:', err)
+    message.error(err?.message || '创建口语工具失败')
+  } finally {
+    inserting.value = false
+  }
+}
+
+// 选择推荐卡片 → 预填充 store → 立即创建配置 + 插入元素
+const handleSelectTask = (task: TopicDiscussionTask) => {
+  speakingStore.prefillFromTask(task.titleEn, task.vocabulary, task.sentences)
+  insertSpeakingToolToCanvas('select')
+}
+
+// 手动创建 → 清空 store → 立即创建配置 + 插入元素
+const handleManualCreate = (_taskTypeId: string) => {
+  speakingStore.resetConfigEmpty()
+  insertSpeakingToolToCanvas('manual')
+}
+</script>
+
+<style lang="scss" scoped>
+.layer2-speaking {
+  padding: 16px 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+// 筛选栏
+.filter-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.filter-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex: 1;
+}
+
+.filter-label {
+  font-size: 12px;
+  font-weight: 500;
+  color: #6b7280;
+  white-space: nowrap;
+}
+
+.filter-select-wrap {
+  position: relative;
+  flex: 1;
+}
+
+.filter-select {
+  width: 100%;
+  appearance: none;
+  padding: 6px 28px 6px 12px;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  border-radius: 12px;
+  background: #f9fafb;
+  font-size: 13px;
+  color: #374151;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:focus {
+    outline: none;
+    border-color: #f97316;
+    background: #fff;
+    box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.1);
+  }
+}
+
+.select-arrow {
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #9ca3af;
+  pointer-events: none;
+}
+
+// 推荐卡片网格
+.card-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 10px;
+}
+
+// 空状态
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  padding: 32px 16px;
+  color: #d1d5db;
+}
+
+.empty-text {
+  font-size: 13px;
+  color: #9ca3af;
+  margin: 0;
+}
+
+// 手动创建
+.manual-create {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.manual-hint {
+  padding: 0;
+}
+
+.manual-text {
+  font-size: 13px;
+  color: #6b7280;
+  margin: 0;
+}
+
+.manual-task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.manual-task-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 12px 14px;
+  background: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  border-radius: 14px;
+  cursor: pointer;
+  transition: all 0.2s;
+  text-align: left;
+
+  &:hover {
+    border-color: rgba(0, 0, 0, 0.12);
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    .task-item-arrow {
+      color: #f97316;
+    }
+  }
+}
+
+.task-item-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #fff7ed, #fff);
+  border: 1px solid rgba(249, 115, 22, 0.15);
+  color: #f97316;
+  flex-shrink: 0;
+}
+
+.task-item-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.task-item-name {
+  font-size: 13px;
+  font-weight: 500;
+  color: #111827;
+}
+
+.task-item-category {
+  font-size: 11px;
+  color: #9ca3af;
+}
+
+.task-item-arrow {
+  color: #d1d5db;
+  flex-shrink: 0;
+  transition: color 0.2s;
+}
+</style>

+ 374 - 0
src/views/Editor/EnglishSpeaking/preview/DetailedReport.vue

@@ -0,0 +1,374 @@
+<template>
+  <div class="detailed-report">
+    <div class="report-header">
+      <h3 class="report-title">详细报告</h3>
+      <button class="toggle-all-btn" @click="toggleAll">
+        {{ allExpanded ? '收起全部' : '展开全部' }}
+        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :class="{ 'chevron-up': allExpanded }">
+          <polyline points="6 9 12 15 18 9" />
+        </svg>
+      </button>
+    </div>
+
+    <!-- 按轮次分组 -->
+    <div
+      v-for="(roundData, roundIndex) in groupedByRound"
+      :key="roundIndex"
+      class="round-group"
+    >
+      <div class="round-label">Round {{ roundIndex + 1 }}</div>
+      <div class="round-cards">
+        <div
+          v-for="sentence in roundData"
+          :key="sentence.id"
+          class="sentence-card"
+          :class="{ 'card-student': sentence.role === 'student', 'card-expanded': expandedIds.has(sentence.id) }"
+          @click="sentence.role === 'student' && hasDetails(sentence) && toggleExpand(sentence.id)"
+        >
+          <!-- 卡片主体 -->
+          <div class="card-body" :class="{ 'body-student': sentence.role === 'student' }">
+            <div class="card-left">
+              <!-- 语音条(仅学生卡片,AI 无 audio_url 故纯文字呈现) -->
+              <div v-if="sentence.role === 'student'" class="card-voice-bar bar-student">
+                <button
+                  class="card-play-btn play-student"
+                  :disabled="!sentence.audioUrl"
+                  @click.stop="handleSentencePlay(sentence)"
+                >
+                  <!-- loading -->
+                  <svg
+                    v-if="player.loadingId.value === sentence.id"
+                    class="play-spinner"
+                    width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                    stroke-width="2" stroke-linecap="round"
+                  >
+                    <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+                  </svg>
+                  <!-- playing -->
+                  <svg
+                    v-else-if="player.playingId.value === sentence.id"
+                    width="10" height="10" viewBox="0 0 24 24" fill="currentColor"
+                  >
+                    <rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" />
+                  </svg>
+                  <!-- error -->
+                  <svg
+                    v-else-if="player.errorId.value === sentence.id"
+                    width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                    stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                  >
+                    <path d="M12 9v4" /><path d="M12 17h.01" />
+                    <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
+                  </svg>
+                  <!-- idle -->
+                  <svg
+                    v-else
+                    width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                    stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                  >
+                    <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
+                    <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
+                  </svg>
+                </button>
+                <div class="card-waveform">
+                  <div
+                    v-for="i in 8"
+                    :key="i"
+                    class="card-wave-bar cw-student"
+                    :style="{ height: `${Math.sin(i * 0.6) * 4 + 3}px` }"
+                  />
+                </div>
+                <span class="card-duration cd-student">
+                  {{ formatDuration(sentence.audioDuration) }}
+                </span>
+              </div>
+
+              <!-- 文本 -->
+              <p class="card-text">{{ sentence.content }}</p>
+              <p v-if="sentence.contentZh" class="card-text-zh">{{ sentence.contentZh }}</p>
+            </div>
+
+            <!-- 评分标签 -->
+            <div v-if="sentence.role === 'student' && hasDetails(sentence)" class="card-right">
+              <span v-if="sentence.score !== undefined" class="card-score" :class="getScoreClass(sentence.score)">{{ sentence.score }}</span>
+              <svg class="expand-icon" :class="{ 'expanded': expandedIds.has(sentence.id) }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <polyline points="6 9 12 15 18 9" />
+              </svg>
+            </div>
+          </div>
+
+          <!-- 展开详情 -->
+          <div v-if="sentence.role === 'student' && expandedIds.has(sentence.id)" class="card-detail">
+            <!-- 发音评分 -->
+            <div v-if="sentence.pronunciation" class="pron-scores">
+              <div class="pron-row">
+                <span class="pron-label">准确度</span>
+                <div class="pron-track"><div class="pron-fill" :style="{ width: `${sentence.pronunciation.accuracy}%` }" /></div>
+                <span class="pron-value">{{ sentence.pronunciation.accuracy }}</span>
+              </div>
+              <div class="pron-row">
+                <span class="pron-label">语调</span>
+                <div class="pron-track"><div class="pron-fill" :style="{ width: `${sentence.pronunciation.intonation}%` }" /></div>
+                <span class="pron-value">{{ sentence.pronunciation.intonation }}</span>
+              </div>
+              <div class="pron-row">
+                <span class="pron-label">重音</span>
+                <div class="pron-track"><div class="pron-fill" :style="{ width: `${sentence.pronunciation.stress}%` }" /></div>
+                <span class="pron-value">{{ sentence.pronunciation.stress }}</span>
+              </div>
+              <div class="pron-row">
+                <span class="pron-label">流畅度</span>
+                <div class="pron-track"><div class="pron-fill" :style="{ width: `${sentence.pronunciation.fluency}%` }" /></div>
+                <span class="pron-value">{{ sentence.pronunciation.fluency }}</span>
+              </div>
+            </div>
+
+            <!-- 反馈信息 -->
+            <div v-if="sentence.feedback" class="feedback-section">
+              <div v-if="sentence.feedback.comment" class="feedback-block">
+                <div class="feedback-block-label"><span class="fb-good">✓</span> 一句话点评</div>
+                <p class="feedback-text">{{ sentence.feedback.comment }}</p>
+              </div>
+              <div v-if="sentence.feedback.betterExpression" class="feedback-block">
+                <div class="feedback-block-label"><span class="fb-suggest">→</span> 进阶表达</div>
+                <p class="better-expression">{{ sentence.feedback.betterExpression }}</p>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import type { SentenceEvaluation } from '@/types/englishSpeaking'
+import { useAudioPlayer } from '../composables/useAudioPlayer'
+
+interface Props {
+  sentenceEvaluations: SentenceEvaluation[]
+}
+
+const props = defineProps<Props>()
+
+const player = useAudioPlayer()
+
+const expandedIds = ref(new Set<string>())
+const allExpanded = ref(false)
+
+const groupedByRound = computed(() => {
+  const groups: SentenceEvaluation[][] = []
+  for (const s of props.sentenceEvaluations) {
+    const idx = s.round - 1
+    if (!groups[idx]) groups[idx] = []
+    groups[idx].push(s)
+  }
+  return groups
+})
+
+function toggleExpand(id: string) {
+  if (expandedIds.value.has(id)) {
+    expandedIds.value.delete(id)
+  } else {
+    expandedIds.value.add(id)
+  }
+}
+
+function toggleAll() {
+  if (allExpanded.value) {
+    expandedIds.value.clear()
+  } else {
+    props.sentenceEvaluations
+      .filter(s => s.role === 'student' && hasDetails(s))
+      .forEach(s => expandedIds.value.add(s.id))
+  }
+  allExpanded.value = !allExpanded.value
+}
+
+function hasDetails(sentence: SentenceEvaluation) {
+  return Boolean(sentence.pronunciation || sentence.feedback)
+}
+
+function formatDuration(seconds: number | null | undefined): string {
+  if (seconds == null || !Number.isFinite(seconds)) return '--:--'
+  const total = Math.round(seconds)
+  const mins = Math.floor(total / 60)
+  const secs = total % 60
+  return `${mins}:${String(secs).padStart(2, '0')}`
+}
+
+function handleSentencePlay(s: SentenceEvaluation) {
+  if (!s.audioUrl) return
+  // 同 id 二次点击:toggle 停止;否则播放新源
+  if (player.playingId.value === s.id || player.loadingId.value === s.id) {
+    player.stop()
+    return
+  }
+  player.play(s.id, { kind: 'url', url: s.audioUrl })
+}
+
+function getScoreClass(score: number) {
+  if (score >= 90) return 'score-excellent'
+  if (score >= 80) return 'score-good'
+  if (score >= 70) return 'score-fair'
+  return 'score-low'
+}
+</script>
+
+<style lang="scss" scoped>
+.detailed-report {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.report-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.report-title { font-size: 14px; font-weight: 600; color: #111827; margin: 0; }
+.toggle-all-btn {
+  font-size: 11px;
+  color: #f97316;
+  background: none;
+  border: none;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  &:hover { color: #ea580c; }
+  svg { transition: transform 0.2s; }
+  .chevron-up { transform: rotate(180deg); }
+}
+
+.round-group { display: flex; flex-direction: column; gap: 6px; }
+.round-label { font-size: 10px; font-weight: 500; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; }
+
+.round-cards { display: flex; flex-direction: column; gap: 6px; }
+
+.sentence-card {
+  background: #fff;
+  border-radius: 10px;
+  border: 1px solid #f3f4f6;
+  overflow: hidden;
+  transition: border-color 0.2s;
+}
+.card-student { cursor: pointer; &:hover { border-color: #fed7aa; } }
+.card-expanded { border-color: rgba(249,115,22,0.3); }
+
+.card-body {
+  padding: 10px 12px;
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 8px;
+}
+.card-left { flex: 1; min-width: 0; }
+
+// 卡片语音条
+.card-voice-bar {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 8px;
+  border-radius: 8px;
+  margin-bottom: 4px;
+}
+.bar-ai { background: #fff; border: 1px solid #f3f4f6; }
+.bar-student { background: #f97316; }
+
+.card-play-btn {
+  padding: 2px;
+  border-radius: 50%;
+  border: none;
+  cursor: pointer;
+}
+.play-ai { background: rgba(249,115,22,0.1); color: #f97316; }
+.play-student { background: rgba(255,255,255,0.2); color: #fff; }
+
+.card-waveform { display: flex; align-items: center; gap: 1px; }
+.card-wave-bar { width: 1.5px; border-radius: 999px; }
+.cw-ai { background: rgba(249,115,22,0.4); }
+.cw-student { background: rgba(255,255,255,0.5); }
+
+.card-duration { font-size: 9px; }
+.cd-ai { color: #9ca3af; }
+.cd-student { color: rgba(255,255,255,0.8); }
+
+.card-text { font-size: 12px; color: #1f2937; margin: 0; line-height: 1.5; }
+.card-text-zh { font-size: 10px; color: #9ca3af; margin: 2px 0 0; }
+
+.card-right {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+}
+.card-score {
+  font-size: 12px;
+  font-weight: 600;
+  padding: 2px 8px;
+  border-radius: 999px;
+}
+.score-excellent { background: #dcfce7; color: #16a34a; }
+.score-good { background: #dbeafe; color: #2563eb; }
+.score-fair { background: #fef3c7; color: #d97706; }
+.score-low { background: #fee2e2; color: #ef4444; }
+
+.expand-icon {
+  transition: transform 0.2s;
+  &.expanded { transform: rotate(180deg); }
+}
+
+// 展开详情
+.card-detail {
+  padding: 10px 12px;
+  border-top: 1px solid #f9fafb;
+  background: #fafbfc;
+}
+
+.pron-scores { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
+.pron-row { display: flex; align-items: center; gap: 8px; }
+.pron-label { font-size: 10px; color: #6b7280; width: 36px; flex-shrink: 0; }
+.pron-track { flex: 1; height: 4px; background: #e5e7eb; border-radius: 999px; overflow: hidden; }
+.pron-fill { height: 100%; background: #f97316; border-radius: 999px; transition: width 0.5s; }
+.pron-value { font-size: 10px; font-weight: 500; color: #f97316; width: 20px; text-align: right; }
+
+.feedback-section { display: flex; flex-direction: column; gap: 10px; }
+.feedback-block { margin-bottom: 2px; }
+.feedback-block-label { font-size: 10px; color: #6b7280; display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
+.fb-good { color: #22c55e; }
+.fb-fix { color: #3b82f6; }
+.fb-suggest { color: #f59e0b; }
+.feedback-text {
+  margin: 0;
+  font-size: 11px;
+  color: #4b5563;
+  line-height: 1.5;
+}
+.better-expression {
+  margin: 0;
+  padding: 8px 10px;
+  background: #fff;
+  border: 1px solid #f3f4f6;
+  border-radius: 8px;
+  font-size: 12px;
+  color: #111827;
+  line-height: 1.5;
+}
+.feedback-list {
+  margin: 0;
+  padding-left: 16px;
+  li { font-size: 10px; color: #4b5563; line-height: 1.6; }
+}
+
+.correction-item { margin-bottom: 6px; }
+.correction-row { display: flex; align-items: center; gap: 4px; font-size: 11px; }
+.correction-original { color: #ef4444; text-decoration: line-through; }
+.correction-arrow { color: #9ca3af; }
+.correction-corrected { color: #22c55e; font-weight: 500; }
+.correction-explanation { font-size: 10px; color: #6b7280; margin: 2px 0 0; }
+</style>

+ 1936 - 0
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -0,0 +1,1936 @@
+<template>
+  <div class="dialogue-chat-view">
+    <!-- ── 结束态全屏 loading ── -->
+    <div v-if="reportFetchInflight" class="finalizing-screen">
+      <div class="finalizing-spinner">
+        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+          stroke-width="2" stroke-linecap="round">
+          <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+        </svg>
+      </div>
+      <p class="finalizing-title">AI 正在为你整理报告...</p>
+      <div class="finalizing-stats-card">
+        <span class="finalizing-stat">{{ finalizingStats.rounds }} 轮</span>
+        <span class="finalizing-stats-sep">·</span>
+        <span class="finalizing-stat">{{ finalizingStats.sentences }} 句</span>
+        <span class="finalizing-stats-sep">·</span>
+        <span class="finalizing-stat">累计 {{ finalizingStats.durationText }}</span>
+      </div>
+    </div>
+
+    <template v-else>
+      <!-- ── HEADER ── -->
+      <div class="chat-header">
+        <div class="header-left">
+          <div
+            class="ai-avatar"
+            :class="{ breathing: state === 'idle' || state === 'ai_thinking' }"
+          >{{ aiAvatar }}</div>
+          <span class="ai-name">{{ aiName }}</span>
+
+          <span v-if="state === 'idle' && showIdleHint" class="idle-hint fade-in">
+            在等你的回答...
+          </span>
+          <span
+            v-else-if="state !== 'idle' && state !== 'ai_thinking'"
+            class="online-dot"
+            title="在线"
+          />
+        </div>
+
+        <div class="header-right">
+          <span class="round-indicator">{{ currentRound }} / {{ totalRounds }} 轮</span>
+          <span v-if="engine.countdownSeconds.value != null" class="total-time">
+            {{ formatSeconds(engine.countdownSeconds.value) }}
+          </span>
+          <button class="icon-btn" title="更多操作" @click="showExitConfirm = true">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />
+            </svg>
+          </button>
+        </div>
+      </div>
+
+      <!-- 麦克风权限引导 -->
+      <div v-if="recorder.permissionState.value === 'denied'" class="permission-banner">
+        <span class="permission-icon">🎤</span>
+        <span>麦克风权限已被拒绝,请在浏览器设置中开启后刷新页面</span>
+      </div>
+
+      <!-- ── CHAT AREA ── -->
+      <div ref="chatContainerRef" class="chat-area">
+        <template v-for="message in engine.messages.value" :key="message.id">
+          <!-- AI 消息 -->
+          <div v-if="message.role === 'ai'" class="msg-row msg-ai fade-in">
+            <div class="avatar-sm">{{ aiAvatar }}</div>
+            <div class="msg-col">
+              <!-- 音频条 -->
+              <div v-if="message.content || message.status === 'done'" class="voice-bar voice-ai">
+                <button
+                  class="play-btn play-ai"
+                  :class="{ 'play-btn-error': player.errorId.value === message.id }"
+                  @click="togglePlay(message.id)"
+                >
+                  <svg
+                    v-if="player.loadingId.value === message.id"
+                    class="play-spinner"
+                    width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                    stroke-width="2" stroke-linecap="round"
+                  >
+                    <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+                  </svg>
+                  <svg
+                    v-else-if="player.playingId.value === message.id"
+                    width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                  >
+                    <rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" />
+                  </svg>
+                  <svg
+                    v-else-if="player.errorId.value === message.id"
+                    width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                    stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                  >
+                    <path d="M12 9v4" />
+                    <path d="M12 17h.01" />
+                    <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
+                  </svg>
+                  <svg
+                    v-else
+                    width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                  >
+                    <polygon points="5 3 19 12 5 21 5 3" />
+                  </svg>
+                </button>
+                <div class="wave-bar-group">
+                  <div
+                    v-for="i in 14"
+                    :key="i"
+                    class="wave-bar wave-ai"
+                    :style="{ height: `${Math.abs(Math.sin(i * 0.7)) * 8 + 3}px` }"
+                  />
+                </div>
+                <span
+                  v-if="player.errorId.value === message.id"
+                  class="play-error-hint"
+                >点击重试</span>
+              </div>
+
+              <!-- 英文文本 -->
+              <div v-if="showEnglishText && message.content" class="bubble bubble-ai">
+                {{ message.content }}
+              </div>
+
+              <!-- 流式加载中 -->
+              <div v-if="message.status === 'loading' && !message.content" class="typing-bubble">
+                <span class="typing-dot" style="animation-delay: 0ms" />
+                <span class="typing-dot" style="animation-delay: 150ms" />
+                <span class="typing-dot" style="animation-delay: 300ms" />
+              </div>
+
+              <!-- AI 错误 -->
+              <div v-if="message.status === 'error'" class="error-card">
+                <span class="error-text">{{ message.error || '生成失败' }}</span>
+                <button
+                  v-if="hasRetryButton(message)"
+                  class="retry-btn"
+                  :disabled="engine.greetingInflight.value"
+                  @click="handleRetry(message)"
+                >{{ retryButtonLabel(message) }}</button>
+              </div>
+            </div>
+          </div>
+
+          <!-- 学生消息 -->
+          <div v-else class="msg-row msg-student fade-in">
+            <!-- 音频条(橙色) -->
+            <div v-if="message.content || message.status !== 'loading'" class="voice-bar voice-student">
+              <span
+                v-if="player.errorId.value === message.id"
+                class="play-error-hint play-error-hint-student"
+              >点击重试</span>
+              <span
+                v-else
+                class="voice-duration voice-duration-student"
+              >{{ formatDuration(message.audioDuration) }}</span>
+              <div class="wave-bar-group">
+                <div
+                  v-for="i in 14"
+                  :key="i"
+                  class="wave-bar wave-student"
+                  :style="{ height: `${Math.abs(Math.sin(i * 0.7)) * 8 + 3}px` }"
+                />
+              </div>
+              <button
+                class="play-btn play-student"
+                :class="{ 'play-btn-error': player.errorId.value === message.id }"
+                @click="togglePlay(message.id)"
+              >
+                <svg
+                  v-if="player.loadingId.value === message.id"
+                  class="play-spinner"
+                  width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round"
+                >
+                  <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+                </svg>
+                <svg
+                  v-else-if="player.playingId.value === message.id"
+                  width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                >
+                  <rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" />
+                </svg>
+                <svg
+                  v-else-if="player.errorId.value === message.id"
+                  width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                >
+                  <path d="M12 9v4" />
+                  <path d="M12 17h.01" />
+                  <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
+                </svg>
+                <svg
+                  v-else
+                  width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                >
+                  <polygon points="5 3 19 12 5 21 5 3" />
+                </svg>
+              </button>
+            </div>
+
+            <!-- 学生 STT 加载中(与 AI typing-bubble 对称,避免按完成后 student 侧空白) -->
+            <div v-if="message.status === 'loading' && !message.content" class="typing-bubble typing-bubble-student">
+              <span class="typing-dot" style="animation-delay: 0ms" />
+              <span class="typing-dot" style="animation-delay: 150ms" />
+              <span class="typing-dot" style="animation-delay: 300ms" />
+            </div>
+
+            <!-- 英文识别文本(带高亮) -->
+            <div v-if="showEnglishText && message.content" class="bubble bubble-student">
+              <template v-if="message.evaluation?.wordAnalysis">
+                <template v-for="(word, idx) in message.content.split(' ')" :key="idx">
+                  <span
+                    v-if="getWordAnalysis(message, word)?.status === 'improvable'"
+                    class="improvable-word"
+                    @click="openPhonemeDetail(getWordAnalysis(message, word)!)"
+                  >{{ word }}</span>
+                  <span v-else>{{ word }}</span>
+                  {{ ' ' }}
+                </template>
+              </template>
+              <template v-else>{{ message.content }}</template>
+            </div>
+
+            <!-- 学生错误 -->
+            <div v-if="message.status === 'error'" class="error-card">
+              <span class="error-text">{{ message.error || '发送失败' }}</span>
+              <button
+                v-if="hasRetryButton(message)"
+                class="retry-btn"
+                @click="handleRetry(message)"
+              >{{ retryButtonLabel(message) }}</button>
+              <button
+                v-if="hasRerecordButton(message)"
+                class="rerecord-btn"
+                @click="handleRerecord(message)"
+              >重录</button>
+            </div>
+
+            <!-- L1 评分卡 -->
+            <div v-if="message.evaluation" class="eval-card">
+              <div class="eval-l1">
+                <div class="dim-row">
+                  <span class="dim-label">准确</span>
+                  <DimBadge :level="message.evaluation.dimensions.accuracy" />
+                  <span class="dim-sep">|</span>
+                  <span class="dim-label">流畅</span>
+                  <DimBadge :level="message.evaluation.dimensions.fluency" />
+                  <span class="dim-sep">|</span>
+                  <span class="dim-label">完整</span>
+                  <DimBadge :level="message.evaluation.dimensions.completeness" />
+                  <span class="dim-sep">|</span>
+                  <span class="dim-label">节奏</span>
+                  <DimBadge :level="message.evaluation.dimensions.rhythm" />
+                </div>
+                <div class="sugg-row">
+                  <p class="sugg-text">
+                    <span class="sugg-icon">💡</span>
+                    <span class="truncate">{{ message.evaluation.suggestion }}</span>
+                  </p>
+                  <button class="detail-toggle" @click="toggleExpand(message.id)">
+                    {{ expandedMessageId === message.id ? '收起' : '详情' }}
+                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                      stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                      :class="{ 'chev-up': expandedMessageId === message.id }">
+                      <polyline points="6 9 12 15 18 9" />
+                    </svg>
+                  </button>
+                </div>
+              </div>
+
+              <div v-if="expandedMessageId === message.id" class="eval-l2 fade-in">
+                <div v-if="message.evaluation.betterExpression" class="better-exp">
+                  <p class="detail-label"><span>✨</span> Better expression:</p>
+                  <p class="detail-content">{{ message.evaluation.betterExpression }}</p>
+                </div>
+                <div v-if="message.evaluation.suggestedWords?.length" class="suggested-words">
+                  <p class="detail-label"><span>🎯</span> Try these words:</p>
+                  <div class="word-tags">
+                    <span v-for="(w, i) in message.evaluation.suggestedWords" :key="i" class="word-tag">{{ w }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+
+      </div>
+
+      <!-- 沉默提示浮层 -->
+      <div v-if="state === 'recording' && silenceHintText" class="silence-hint-wrap">
+        <div class="silence-hint fade-in">
+          <span class="silence-icon">💡</span>
+          <p class="silence-text">{{ silenceHintText }}</p>
+          <button class="silence-close" @click="silenceHintText = ''">
+            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+            </svg>
+          </button>
+        </div>
+      </div>
+
+      <!-- ── CONTROL ZONE ── -->
+      <div class="control-zone">
+        <!-- 进度条(仅录音时可见) -->
+        <div class="progress-wrap">
+          <div
+            class="progress-track"
+            :style="{ background: state === 'recording' ? '#f3f4f6' : 'transparent' }"
+          >
+            <div
+              class="progress-fill"
+              :class="{ 'near-limit': isNearLimit }"
+              :style="{
+                width: state === 'recording' ? `${progressPct}%` : '0%',
+                opacity: state === 'recording' ? 1 : 0,
+              }"
+            />
+          </div>
+        </div>
+
+        <!-- 状态叠放区 -->
+        <div class="state-stack">
+          <!-- idle -->
+          <div
+            class="state-layer state-idle"
+            :style="stateStyle('idle')"
+          >
+            <button class="hint-btn" @click="openTaskHint">
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M9 18h6" /><path d="M10 22h4" />
+                <path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14" />
+              </svg>
+              提示
+            </button>
+            <button
+              class="mic-btn"
+              :disabled="!engine.canRecord.value"
+              @click="handleStartRecording"
+            >
+              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
+                <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
+                <line x1="12" y1="19" x2="12" y2="23" />
+                <line x1="8" y1="23" x2="16" y2="23" />
+              </svg>
+              开始录音
+            </button>
+          </div>
+
+          <!-- starting -->
+          <div class="state-layer state-starting" :style="stateStyle('starting')">
+            <div class="record-capsule">
+              <button class="cancel-btn" @click="handleCancelStarting">
+                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+                </svg>
+                取消
+              </button>
+              <div class="record-meter">
+                <svg class="spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round">
+                  <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+                </svg>
+                <span class="record-time">准备录音中...</span>
+              </div>
+              <button class="finish-btn" disabled>
+                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
+                  <polyline points="22 4 12 14.01 9 11.01" />
+                </svg>
+                完成
+              </button>
+            </div>
+          </div>
+
+          <!-- recording -->
+          <div class="state-layer state-recording" :style="stateStyle('recording')">
+            <div class="record-capsule">
+              <button class="cancel-btn" @click="handleCancelRecording">
+                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+                </svg>
+                取消
+              </button>
+              <div class="record-meter">
+                <div class="animated-wave">
+                  <div
+                    v-for="i in 7"
+                    :key="i"
+                    class="aw-bar"
+                    :class="{ 'near-limit': isNearLimit }"
+                    :style="{
+                      height: `${Math.abs(Math.sin(i * 0.9)) * 9 + 3}px`,
+                      animationDelay: `${(i - 1) * 0.1}s`,
+                    }"
+                  />
+                </div>
+                <span class="record-time" :class="{ 'near-limit': isNearLimit }">
+                  {{ formatSeconds(recorder.recordingDuration.value) }}
+                </span>
+                <span class="record-time-max">/ {{ formatSeconds(MAX_RECORDING_SECONDS) }}</span>
+              </div>
+              <button class="finish-btn" @click="handleFinishRecording">
+                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
+                  <polyline points="22 4 12 14.01 9 11.01" />
+                </svg>
+                完成
+              </button>
+            </div>
+          </div>
+
+          <!-- stt -->
+          <div class="state-layer state-center" :style="stateStyle('stt')">
+            <svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+              stroke-width="2" stroke-linecap="round">
+              <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+            </svg>
+            <span class="center-text">正在识别语音...</span>
+          </div>
+
+          <!-- ai_thinking -->
+          <div class="state-layer state-center" :style="stateStyle('ai_thinking')">
+            <div class="mini-avatar">{{ aiAvatar }}</div>
+            <span class="center-text">{{ aiName }} 正在回复...</span>
+          </div>
+
+          <!-- error -->
+          <div class="state-layer state-error" :style="stateStyle('error')">
+            <div class="error-info">
+              <span class="warn-icon">⚠️</span>
+              <span class="warn-text">{{ lastErrorText }}</span>
+            </div>
+            <button
+              v-if="lastErroredMessage && hasRetryButton(lastErroredMessage)"
+              class="retry-pill"
+              @click="handleRetry(lastErroredMessage)"
+            >{{ retryButtonLabel(lastErroredMessage) }}</button>
+          </div>
+
+        </div>
+      </div>
+
+      <!-- ─────── OVERLAYS ─────── -->
+
+      <!-- 任务提示弹窗 -->
+      <TaskHintModal
+        :visible="showHintModal"
+        :loading="taskHintLoading"
+        :error="taskHintError"
+        :hint="taskHint"
+        :ai-name="aiName"
+        @close="showHintModal = false"
+        @retry="loadTaskHint"
+      />
+
+      <!-- 音素详情弹窗 -->
+      <div v-if="phonemeDetail" class="modal-mask" @click.self="phonemeDetail = null">
+        <div class="modal phoneme-modal scale-in">
+          <div class="modal-head">
+            <div>
+              <h3 class="phoneme-word">{{ phonemeDetail.word }}</h3>
+              <p class="phoneme-sub">发音详情</p>
+            </div>
+            <button class="close-btn" @click="phonemeDetail = null">
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+              </svg>
+            </button>
+          </div>
+
+          <div class="phoneme-body">
+            <div class="pho-card pho-user">
+              <div>
+                <p class="pho-label">你的发音</p>
+                <p class="pho-value">{{ phonemeDetail.userPronunciation }}</p>
+              </div>
+              <button class="pho-play pho-play-user">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3" /></svg>
+              </button>
+            </div>
+            <div class="pho-card pho-standard">
+              <div>
+                <p class="pho-label">标准发音</p>
+                <p class="pho-value pho-value-green">{{ phonemeDetail.standardPronunciation }}</p>
+              </div>
+              <button class="pho-play pho-play-standard">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3" /></svg>
+              </button>
+            </div>
+            <div v-if="phonemeDetail.tip" class="pho-card pho-tip">
+              <p class="pho-label">小提示</p>
+              <p class="pho-tip-text">{{ phonemeDetail.tip }}</p>
+            </div>
+
+            <button class="pho-practice-btn" @click="practiceThisWord">
+              针对这个词重练一次
+            </button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 退出/重开确认弹窗 -->
+      <div v-if="showExitConfirm" class="modal-mask" @click.self="showExitConfirm = false">
+        <div class="modal exit-modal scale-in">
+          <div class="modal-head">
+            <h3 class="modal-title">选择操作</h3>
+            <button class="close-btn" @click="showExitConfirm = false">
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+              </svg>
+            </button>
+          </div>
+          <p class="exit-hint">请选择你的操作:</p>
+          <div class="exit-actions">
+            <button class="exit-secondary" @click="showExitConfirm = false">继续练习</button>
+            <button class="exit-secondary" @click="handleRestart">重新开始</button>
+            <button class="exit-primary" @click="handleExitConfirm">结束并查看报告</button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 徽章弹窗 -->
+      <div v-if="showBadge" class="badge-popup scale-in">
+        <div class="badge-card">
+          <span class="badge-icon">{{ showBadge.icon }}</span>
+          <div>
+            <p class="badge-name">{{ showBadge.name }}</p>
+            <p class="badge-desc">{{ showBadge.description }}</p>
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted, onUnmounted, nextTick, h, defineComponent } from 'vue'
+import type { PropType } from 'vue'
+import type { PreviewChatMessage, BadgeAchievement, DialogueReport, SessionStartInfo, TaskHint } from '@/types/englishSpeaking'
+import { useDialogueEngine } from '../composables/useDialogueEngine'
+import { useAudioRecorder } from '../composables/useAudioRecorder'
+import { useAudioPlayer } from '../composables/useAudioPlayer'
+import TaskHintModal from './TaskHintModal.vue'
+import { createDialogueApi } from '../services/llmService'
+import { SPEAKING_TRANSPORT } from '../services/speakingApiConfig'
+
+// ─────────────────────────────────────────────
+// Props / Emits
+// ─────────────────────────────────────────────
+
+interface Props {
+  topic?: string
+  keywords?: string[]
+  aiName?: string
+  aiAvatar?: string
+  totalRounds?: number
+  showEnglishText?: boolean
+  showChineseText?: boolean
+  sessionInfo?: SessionStartInfo | null
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  topic: '',
+  keywords: () => ['animal', 'zoo', 'cute', 'favorite'],
+  aiName: 'Amy',
+  aiAvatar: '😊',
+  totalRounds: 3,
+  showEnglishText: true,
+  showChineseText: false,
+  sessionInfo: null,
+})
+
+const emit = defineEmits<{
+  complete: [report: DialogueReport | null]
+  restart: []
+}>()
+
+// ─────────────────────────────────────────────
+// Config
+// ─────────────────────────────────────────────
+
+const MAX_RECORDING_SECONDS = 60
+
+// 学生录音上送方式:'websocket'(默认,流式)或 'http'(单次 POST /speak)。
+// 由环境变量 VITE_SPEAKING_TRANSPORT 控制;未配置时默认 websocket。
+const speakTransport = SPEAKING_TRANSPORT
+
+const SILENCE_HINTS = [
+  'You could say: "I really like pandas because they are so cute!"',
+  'Try: "My favorite animal is the elephant. It\'s very smart."',
+  'You could say: "I went to the zoo last weekend with my family."',
+]
+
+const BADGE_CONFIG: Record<string, BadgeAchievement> = {
+  smooth_talker: { id: 'smooth_talker', name: '流畅达人', nameEn: 'Smooth Talker', icon: '💬', description: '连续3句流畅度优秀' },
+  pronunciation_pro: { id: 'pronunciation_pro', name: '发音专家', nameEn: 'Pronunciation Pro', icon: '🎯', description: '连续5句发音准确' },
+  perfect_round: { id: 'perfect_round', name: '完美一轮', nameEn: 'Perfect Round', icon: '⭐', description: '单轮四维度全优' },
+}
+
+// ─────────────────────────────────────────────
+// Composables
+// ─────────────────────────────────────────────
+
+const engine = useDialogueEngine()
+const recorder = useAudioRecorder()
+const player = useAudioPlayer()
+
+// ─────────────────────────────────────────────
+// Local UI State
+// ─────────────────────────────────────────────
+
+const chatContainerRef = ref<HTMLDivElement>()
+const expandedMessageId = ref<string | null>(null)
+const showHintModal = ref(false)
+const taskHint = ref<TaskHint | null>(null)
+const taskHintLoading = ref(false)
+const taskHintError = ref<string | null>(null)
+const taskHintApi = createDialogueApi()
+const showExitConfirm = ref(false)
+const phonemeDetail = ref<{
+  word: string
+  userPronunciation?: string
+  standardPronunciation?: string
+  tip?: string
+} | null>(null)
+
+const silenceHintText = ref('')
+const showIdleHint = ref(false)
+let idleHintTimer: ReturnType<typeof setTimeout> | null = null
+
+const isStarting = ref(false)
+const reportFetchInflight = ref(false)
+let startAbortController: AbortController | null = null
+
+// 徽章
+const showBadge = ref<BadgeAchievement | null>(null)
+const consecutiveFluent = ref(0)
+const consecutiveAccurate = ref(0)
+let badgeTimer: ReturnType<typeof setTimeout> | null = null
+
+// ─────────────────────────────────────────────
+// Derived State
+// ─────────────────────────────────────────────
+
+const currentRound = computed(() => engine.currentRound.value)
+
+// 状态机:starting → recording → stt → ai_thinking → idle / error / done
+const state = computed<
+  'idle' | 'starting' | 'recording' | 'stt' | 'ai_thinking' | 'error' | 'done'
+>(() => {
+  if (isStarting.value) return 'starting'
+  if (recorder.isRecording.value) return 'recording'
+  if (engine.isComplete.value) return 'done'
+
+  const msgs = engine.messages.value
+  const last = msgs[msgs.length - 1]
+
+  if (last?.status === 'error') return 'error'
+
+  // 学生消息 loading 且无 content → 正在 STT
+  if (last?.role === 'student' && last.status === 'loading' && !last.content) return 'stt'
+
+  if (engine.isProcessing.value) return 'ai_thinking'
+
+  return 'idle'
+})
+
+const progressPct = computed(() => Math.min((recorder.recordingDuration.value / MAX_RECORDING_SECONDS) * 100, 100))
+const isNearLimit = computed(() => recorder.recordingDuration.value >= MAX_RECORDING_SECONDS * 0.8)
+
+const lastErrorText = computed(() => {
+  const msgs = engine.messages.value
+  const last = msgs[msgs.length - 1]
+  return last?.error || '请求异常,请稍后再试'
+})
+
+const finalizingStats = computed(() => {
+  const studentMsgs = engine.messages.value.filter(m => m.role === 'student')
+  const sentenceCount = studentMsgs.filter(m => m.status !== 'error').length
+  const totalDurationSec = studentMsgs.reduce(
+    (sum, m) => sum + (m.audioDuration ?? 0),
+    0,
+  )
+  return {
+    rounds: engine.currentRound.value,
+    sentences: sentenceCount,
+    durationText: formatSeconds(Math.round(totalDurationSec)),
+  }
+})
+
+// ─────────────────────────────────────────────
+// Sub-Component: DimBadge
+// ─────────────────────────────────────────────
+
+const DimBadge = defineComponent({
+  props: { level: { type: String as PropType<'excellent' | 'good' | 'improve'>, required: true } },
+  setup(p) {
+    return () => {
+      if (p.level === 'excellent') return h('span', { class: 'dim-badge dim-excellent' }, '✓✓')
+      if (p.level === 'good') return h('span', { class: 'dim-badge dim-good' }, '✓')
+      return h('span', { class: 'dim-badge dim-improve' }, '△')
+    }
+  },
+})
+
+// ─────────────────────────────────────────────
+// Helpers
+// ─────────────────────────────────────────────
+
+function formatSeconds(s: number): string {
+  const m = Math.floor(s / 60)
+  const sec = s % 60
+  return `${m}:${sec.toString().padStart(2, '0')}`
+}
+
+function formatDuration(seconds: number | null | undefined): string {
+  if (seconds == null || !Number.isFinite(seconds)) return '--:--'
+  const total = Math.round(seconds)
+  const mins = Math.floor(total / 60)
+  const secs = total % 60
+  return `${mins}:${String(secs).padStart(2, '0')}`
+}
+
+function getWordAnalysis(message: PreviewChatMessage, word: string) {
+  const clean = word.replace(/[.,!?]/g, '')
+  return message.evaluation?.wordAnalysis?.find(w => w.word === clean)
+}
+
+function stateStyle(target: string) {
+  const active = state.value === target
+  return {
+    opacity: active ? 1 : 0,
+    pointerEvents: active ? 'auto' : 'none',
+    transition: 'opacity 0.18s ease-out',
+  } as const
+}
+
+// ─────────────────────────────────────────────
+// Actions
+// ─────────────────────────────────────────────
+
+// 当前流式会话控制器(开始录音时打开 WebSocket,结束时收尾)
+let streamCtl: ReturnType<typeof engine.beginStudentStream> = null
+
+async function handleStartRecording() {
+  if (!engine.canRecord.value || recorder.isRecording.value || isStarting.value) return
+  player.stop()
+
+  isStarting.value = true
+  startAbortController = new AbortController()
+
+  try {
+    await recorder.startRecording(startAbortController.signal)
+    // 学生录音上送方式由 speakingApiConfig.SPEAKING_TRANSPORT 决定:
+    // - websocket(默认):开 WS 流式推 PCM;失败由 useDialogueEngine 暴露 error,用户点"重试"才走 HTTP。
+    // - http:跳过 WS,handleFinishRecording 直接走 sendStudentMessage → /speak。
+    if (speakTransport === 'websocket') {
+      streamCtl = engine.beginStudentStream({
+        sampleRate: recorder.sampleRate.value,
+        bits: 16,
+        channels: 1,
+      })
+      if (streamCtl) {
+        recorder.onChunk.value = streamCtl.pushChunk
+      }
+    }
+  } catch (err: any) {
+    if (err.name === 'AbortError') {
+      // User cancelled during getUserMedia. State naturally returns to idle.
+    } else {
+      console.error('Failed to start recording:', err)
+    }
+  } finally {
+    isStarting.value = false
+    startAbortController = null
+  }
+}
+
+function handleCancelStarting() {
+  startAbortController?.abort()
+  // recorder.startRecording will reject with AbortError; no further cleanup needed.
+}
+
+function handleCancelRecording() {
+  // 停止录音 + 中止流式会话(丢弃本次录音)
+  recorder.onChunk.value = null
+  if (streamCtl) {
+    streamCtl.abort()  // close WS; nothing on the message list to clean
+    streamCtl = null
+  }
+  if (recorder.isRecording.value) {
+    recorder.stopRecording().catch(() => {})
+  }
+  recorder.cleanup()
+}
+
+async function handleFinishRecording() {
+  if (!recorder.isRecording.value) return
+  const ctl = streamCtl
+  streamCtl = null
+  try {
+    const blob = await recorder.stopRecording()
+    recorder.onChunk.value = null
+    if (ctl) {
+      ctl.commit(blob)  // pushes placeholders, attaches blob, sends 'stop'
+    } else {
+      // Fallback: direct HTTP path
+      await engine.sendStudentMessage(blob, crypto.randomUUID())
+    }
+  } catch (err) {
+    console.error('Recording/send failed:', err)
+  }
+}
+
+function hasRetryButton(m: PreviewChatMessage): boolean {
+  return m.recovery === 'retry' || m.recovery === 'restart'
+}
+function hasRerecordButton(m: PreviewChatMessage): boolean {
+  return m.role === 'student' && (m.recovery === 'retry' || m.recovery === 'rerecord')
+}
+function retryButtonLabel(m: PreviewChatMessage): string {
+  if (m.recovery === 'restart') return '返回重开'
+  return '重试'
+}
+async function handleRetry(m: PreviewChatMessage) {
+  if (m.recovery === 'restart') {
+    emit('restart')
+    return
+  }
+  if (m.role === 'student') {
+    await engine.retryMessage(m.id)
+  } else {
+    // greeting case: first AI message with no prior student
+    const idx = engine.messages.value.indexOf(m)
+    const isGreeting = !engine.messages.value
+      .slice(0, idx)
+      .some(x => x.role === 'student')
+    if (isGreeting) {
+      await engine.retryGreeting()
+    } else {
+      await engine.regenerateAiMessage(m.id)
+    }
+  }
+}
+function handleRerecord(m: PreviewChatMessage) {
+  engine.discardCurrentTurn(m.id)
+}
+
+const lastErroredMessage = computed<PreviewChatMessage | null>(() => {
+  const msgs = engine.messages.value
+  for (let i = msgs.length - 1; i >= 0; i--) {
+    if (msgs[i].status === 'error') return msgs[i]
+  }
+  return null
+})
+
+function toggleExpand(id: string) {
+  expandedMessageId.value = expandedMessageId.value === id ? null : id
+}
+
+function openTaskHint() {
+  showHintModal.value = true
+  if (!taskHint.value && !taskHintLoading.value) {
+    loadTaskHint()
+  }
+}
+
+async function loadTaskHint() {
+  if (!props.sessionInfo?.sessionId) {
+    taskHintError.value = '当前会话未准备好,请稍后重试'
+    return
+  }
+
+  taskHintLoading.value = true
+  taskHintError.value = null
+
+  try {
+    taskHint.value = await taskHintApi.generateTaskHint(props.sessionInfo.sessionId)
+  } catch {
+    taskHintError.value = '生成任务提示失败,请重试'
+  } finally {
+    taskHintLoading.value = false
+  }
+}
+
+function togglePlay(id: string) {
+  // Same id is currently playing or loading → stop.
+  if (player.playingId.value === id || player.loadingId.value === id) {
+    player.stop()
+    return
+  }
+
+  const msg = engine.messages.value.find(m => m.id === id)
+  if (!msg) return
+
+  if (msg.role === 'student' && msg.audioBlob) {
+    player.play(id, { kind: 'blob', blob: msg.audioBlob })
+  }
+  else if (msg.role === 'ai' && msg.content) {
+    player.play(id, { kind: 'tts', text: msg.content })
+  }
+}
+
+function openPhonemeDetail(wa: NonNullable<NonNullable<PreviewChatMessage['evaluation']>['wordAnalysis']>[0]) {
+  phonemeDetail.value = {
+    word: wa.word,
+    userPronunciation: wa.userPronunciation,
+    standardPronunciation: wa.standardPronunciation,
+    tip: wa.tip,
+  }
+}
+
+function practiceThisWord() {
+  phonemeDetail.value = null
+  handleStartRecording()
+}
+
+async function handleExitConfirm() {
+  showExitConfirm.value = false
+  emit('complete', await fetchReportSafe())
+}
+
+async function fetchReportSafe(): Promise<DialogueReport | null> {
+  reportFetchInflight.value = true
+  try {
+    await engine.completeSession()
+    return await engine.getReport()
+  } catch (err) {
+    console.warn('[speaking] getReport failed:', err)
+    return null
+  } finally {
+    reportFetchInflight.value = false
+  }
+}
+
+function handleRestart() {
+  showExitConfirm.value = false
+  engine.abort()
+  player.stop()
+  emit('restart')
+}
+
+// ─────────────────────────────────────────────
+// Badge detection
+// ─────────────────────────────────────────────
+
+function triggerBadge(id: string) {
+  const badge = BADGE_CONFIG[id]
+  if (!badge) return
+  showBadge.value = badge
+  if (badgeTimer) clearTimeout(badgeTimer)
+  badgeTimer = setTimeout(() => { showBadge.value = null }, 2800)
+}
+
+function checkBadges(ev: NonNullable<PreviewChatMessage['evaluation']>) {
+  const d = ev.dimensions
+  if (d.fluency === 'excellent') {
+    consecutiveFluent.value += 1
+    if (consecutiveFluent.value === 3) triggerBadge('smooth_talker')
+  } else {
+    consecutiveFluent.value = 0
+  }
+  if (d.accuracy === 'excellent') {
+    consecutiveAccurate.value += 1
+    if (consecutiveAccurate.value === 5) triggerBadge('pronunciation_pro')
+  } else {
+    consecutiveAccurate.value = 0
+  }
+  if (d.accuracy === 'excellent' && d.fluency === 'excellent' && d.completeness === 'excellent' && d.rhythm === 'excellent') {
+    setTimeout(() => triggerBadge('perfect_round'), 400)
+  }
+}
+
+// ─────────────────────────────────────────────
+// Watchers
+// ─────────────────────────────────────────────
+
+// 自动滚动到底部
+watch(
+  () => engine.messages.value.map(m => m.content + m.status).join('|'),
+  () => {
+    nextTick(() => {
+      if (chatContainerRef.value) {
+        chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
+      }
+    })
+  },
+)
+
+// 自动播放:AI 消息流式 done 后,合成并播一次。
+// 用 Set 去重防止 watcher 因为不相关重渲染重复触发。
+const autoPlayedIds = new Set<string>()
+const seededHistoricalMessageIds = new Set<string>()
+watch(
+  () => engine.messages.value.map(m => `${m.id}:${m.status}`).join('|'),
+  () => {
+    for (const m of engine.messages.value) {
+      if (
+        m.role === 'ai' &&
+        m.status === 'done' &&
+        m.content &&
+        !seededHistoricalMessageIds.has(m.id) &&
+        !autoPlayedIds.has(m.id)
+      ) {
+        autoPlayedIds.add(m.id)
+        player.play(m.id, { kind: 'tts', text: m.content })
+      }
+    }
+  },
+)
+
+// AI 消息 done 后,延迟 600ms 显示 "在等你的回答..."
+watch(
+  () => {
+    const msgs = engine.messages.value
+    const last = msgs[msgs.length - 1]
+    return last?.role === 'ai' && last.status === 'done' ? last.id : null
+  },
+  (val) => {
+    if (idleHintTimer) clearTimeout(idleHintTimer)
+    if (val) {
+      showIdleHint.value = false
+      idleHintTimer = setTimeout(() => { showIdleHint.value = true }, 600)
+    } else {
+      showIdleHint.value = false
+    }
+  },
+)
+
+// 学生消息产生 evaluation 时,检查徽章 + 清理沉默提示
+watch(
+  () => engine.messages.value.filter(m => m.role === 'student' && m.evaluation).length,
+  () => {
+    const done = engine.messages.value.filter(m => m.role === 'student' && m.evaluation)
+    const last = done[done.length - 1]
+    if (last?.evaluation) checkBadges(last.evaluation)
+  },
+)
+
+// 对话完成 → 通知父组件
+watch(
+  () => engine.isComplete.value,
+  async (complete) => {
+    if (complete) emit('complete', await fetchReportSafe())
+  },
+)
+
+// 沉默检测 → 随机提示
+watch(
+  () => recorder.silenceDetected.value,
+  (silent) => {
+    if (silent && recorder.isRecording.value) {
+      silenceHintText.value = SILENCE_HINTS[Math.floor(Math.random() * SILENCE_HINTS.length)]
+    } else {
+      silenceHintText.value = ''
+    }
+  },
+)
+
+// 录音时长达到上限,自动完成
+watch(
+  () => recorder.recordingDuration.value,
+  (v) => {
+    if (v >= MAX_RECORDING_SECONDS && recorder.isRecording.value) {
+      handleFinishRecording()
+    }
+  },
+)
+
+// ─────────────────────────────────────────────
+// Lifecycle
+// ─────────────────────────────────────────────
+
+onMounted(() => {
+  if (props.sessionInfo) {
+    const hasHistory = !!props.sessionInfo.messages?.length
+    seededHistoricalMessageIds.clear()
+    for (const message of props.sessionInfo.messages ?? []) {
+      seededHistoricalMessageIds.add(message.id)
+    }
+    engine.attachSession({
+      sessionId: props.sessionInfo.sessionId,
+      expiresAt: props.sessionInfo.expiresAt,
+      totalRounds: props.totalRounds,
+      currentRound: props.sessionInfo.currentRound,
+      messages: props.sessionInfo.messages,
+    })
+    if (!hasHistory) engine.generateGreeting()
+  } else {
+    console.warn('[DialogueChatView] mounted without sessionInfo; chat is inert. Parent must createSession before mounting.')
+  }
+  // 无 sessionInfo 时聊天区保持空(父组件应当先创建 session 再挂载本组件)
+})
+
+onUnmounted(() => {
+  if (idleHintTimer) clearTimeout(idleHintTimer)
+  if (badgeTimer) clearTimeout(badgeTimer)
+})
+</script>
+
+<style lang="scss" scoped>
+// ─────────────────────────────────────────────
+// Root
+// ─────────────────────────────────────────────
+.dialogue-chat-view {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  user-select: none;
+  background: #fff;
+  font-size: 12px;
+}
+
+// ─────────────────────────────────────────────
+// Header
+// ─────────────────────────────────────────────
+.chat-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 12px;
+  border-bottom: 1px solid #f3f4f6;
+  background: #fff;
+  flex-shrink: 0;
+}
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+.ai-avatar {
+  width: 24px; height: 24px;
+  border-radius: 50%;
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 12px;
+  flex-shrink: 0;
+  &.breathing { animation: breathing 2.4s ease-in-out infinite; }
+}
+.ai-name { font-size: 12px; font-weight: 600; color: #1f2937; }
+.idle-hint { font-size: 11px; color: #9ca3af; }
+.online-dot {
+  width: 6px; height: 6px;
+  background: #10b981;
+  border-radius: 50%;
+}
+.header-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.round-indicator { font-size: 11px; font-weight: 500; color: #f97316; }
+.total-time { font-size: 11px; font-weight: 500; color: #6b7280; font-variant-numeric: tabular-nums; }
+.icon-btn {
+  padding: 6px;
+  border: none; background: transparent;
+  color: #9ca3af;
+  border-radius: 6px;
+  display: flex; align-items: center; justify-content: center;
+  cursor: pointer;
+  transition: background 0.15s, color 0.15s;
+  &:hover { background: #f3f4f6; color: #4b5563; }
+}
+
+// ─────────────────────────────────────────────
+// Permission banner
+// ─────────────────────────────────────────────
+.permission-banner {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  background: #fef3c7;
+  border-bottom: 1px solid #fde68a;
+  font-size: 11px;
+  color: #92400e;
+  flex-shrink: 0;
+}
+.permission-icon { font-size: 14px; }
+
+// ─────────────────────────────────────────────
+// Chat area
+// ─────────────────────────────────────────────
+.chat-area {
+  flex: 1;
+  overflow-y: auto;
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  background: #f7f8fa;
+  min-height: 0;
+}
+.msg-row {
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+}
+.msg-ai { justify-content: flex-start; }
+.msg-student {
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 4px;
+}
+.msg-col {
+  max-width: 78%;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.avatar-sm {
+  width: 28px; height: 28px;
+  border-radius: 50%;
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 14px;
+  flex-shrink: 0;
+  margin-bottom: 2px;
+}
+
+// 语音条
+.voice-bar {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  width: clamp(140px, 44%, 210px);
+}
+.voice-ai {
+  background: #fff;
+  border: 1px solid #f3f4f6;
+  border-radius: 16px;
+  border-top-left-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+}
+.voice-student {
+  background: #f97316;
+  border-radius: 16px;
+  border-top-right-radius: 4px;
+}
+.play-btn {
+  width: 24px; height: 24px;
+  border-radius: 50%;
+  border: none;
+  display: flex; align-items: center; justify-content: center;
+  flex-shrink: 0;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+.play-ai {
+  background: rgba(249,115,22,0.1);
+  color: #f97316;
+  &:hover { background: rgba(249,115,22,0.2); }
+}
+.play-student {
+  background: rgba(255,255,255,0.2);
+  color: #fff;
+  &:hover { background: rgba(255,255,255,0.3); }
+}
+.play-btn-error {
+  background: #fef2f2 !important;
+  color: #dc2626 !important;
+  border: 1px solid #fecaca;
+  &:hover { background: #fee2e2 !important; }
+}
+.play-spinner {
+  animation: spin 1s linear infinite;
+}
+.play-error-hint {
+  font-size: 10px;
+  color: #dc2626;
+  font-weight: 500;
+  flex-shrink: 0;
+  white-space: nowrap;
+}
+.play-error-hint-student {
+  color: #fff;
+}
+.wave-bar-group {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 1px;
+}
+.wave-bar {
+  width: 2px;
+  border-radius: 999px;
+  flex-shrink: 0;
+}
+.wave-ai { background: rgba(249,115,22,0.4); }
+.wave-student { background: rgba(255,255,255,0.5); }
+.voice-duration { font-size: 10px; flex-shrink: 0; }
+.voice-duration-ai { color: #9ca3af; }
+.voice-duration-student { color: rgba(255,255,255,0.7); }
+
+// 气泡
+.bubble {
+  padding: 8px 12px;
+  border-radius: 12px;
+  font-size: 12px;
+  line-height: 1.55;
+  max-width: 100%;
+}
+.bubble-ai {
+  background: #fff;
+  border: 1px solid #f3f4f6;
+  border-top-left-radius: 4px;
+  color: #374151;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+}
+.bubble-student {
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+  border-top-right-radius: 4px;
+  color: #374151;
+  max-width: 78%;
+  padding: 6px 12px;
+}
+.improvable-word {
+  color: #d97706;
+  text-decoration: underline wavy #fbbf24;
+  cursor: pointer;
+  padding: 0 2px;
+  border-radius: 2px;
+  transition: background 0.2s;
+  &:hover { background: #fef3c7; }
+}
+
+// typing 指示器
+.typing-bubble {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 10px 14px;
+  background: #fff;
+  border: 1px solid #f3f4f6;
+  border-radius: 16px;
+  border-top-left-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+}
+.typing-bubble-student {
+  border-top-left-radius: 16px;
+  border-top-right-radius: 4px;
+}
+.typing-dot {
+  width: 6px; height: 6px;
+  background: rgba(249,115,22,0.7);
+  border-radius: 50%;
+  animation: typing-bounce 1s ease-in-out infinite;
+}
+@keyframes typing-bounce {
+  0%, 100% { transform: translateY(0); opacity: 0.5; }
+  50%      { transform: translateY(-3px); opacity: 1; }
+}
+
+// 错误卡
+.error-card {
+  margin-top: 4px;
+  padding: 6px 10px;
+  background: #fef2f2;
+  border: 1px solid #fecaca;
+  border-radius: 10px;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+}
+.error-text { font-size: 11px; color: #dc2626; }
+.retry-btn {
+  padding: 3px 10px;
+  background: #fff;
+  border: 1px solid #fecaca;
+  border-radius: 999px;
+  font-size: 11px;
+  color: #dc2626;
+  cursor: pointer;
+  &:hover { background: #fef2f2; border-color: #f87171; }
+}
+.rerecord-btn {
+  padding: 3px 10px;
+  background: transparent;
+  border: 1px solid #d1d5db;
+  border-radius: 999px;
+  font-size: 11px;
+  color: #6b7280;
+  cursor: pointer;
+  margin-left: 4px;
+  &:hover { background: #f9fafb; border-color: #9ca3af; color: #374151; }
+}
+
+// 评分卡
+.eval-card {
+  background: #fff;
+  border: 1px solid #f3f4f6;
+  border-radius: 12px;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+  overflow: hidden;
+  width: clamp(260px, 88%, 420px);
+  text-align: left;
+  margin-top: 2px;
+}
+.eval-l1 { padding: 8px 12px; }
+.dim-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 10px;
+  color: #6b7280;
+  flex-wrap: wrap;
+}
+.dim-label { color: #6b7280; }
+.dim-sep { color: #e5e7eb; margin: 0 2px; }
+.dim-badge {
+  font-size: 10px;
+  font-weight: 600;
+  padding: 1px 4px;
+  border-radius: 3px;
+}
+.dim-excellent { color: #059669; background: #ecfdf5; }
+.dim-good      { color: #10b981; background: #ecfdf5; }
+.dim-improve   { color: #f59e0b; background: #fffbeb; }
+
+.sugg-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  margin-top: 6px;
+}
+.sugg-text {
+  flex: 1;
+  min-width: 0;
+  font-size: 10px;
+  color: #6b7280;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin: 0;
+}
+.sugg-icon { color: #f59e0b; flex-shrink: 0; }
+.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.detail-toggle {
+  font-size: 10px;
+  color: #f97316;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  flex-shrink: 0;
+  &:hover { color: #ea580c; }
+  svg { transition: transform 0.2s; }
+  .chev-up { transform: rotate(180deg); }
+}
+.eval-l2 {
+  padding: 10px 12px;
+  border-top: 1px solid #f9fafb;
+  background: #fafbfc;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.detail-label {
+  font-size: 10px;
+  color: #9ca3af;
+  margin: 0 0 4px;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.detail-content {
+  font-size: 12px;
+  color: #374151;
+  background: #fff;
+  padding: 6px 10px;
+  border: 1px solid #f3f4f6;
+  border-radius: 8px;
+  margin: 0;
+  line-height: 1.5;
+}
+.word-tags { display: flex; flex-wrap: wrap; gap: 4px; }
+.word-tag {
+  padding: 2px 8px;
+  background: #fff7ed;
+  color: #ea580c;
+  font-size: 10px;
+  border-radius: 999px;
+  border: 1px solid #fed7aa;
+}
+
+// ─────────────────────────────────────────────
+// Silence hint
+// ─────────────────────────────────────────────
+.silence-hint-wrap {
+  position: absolute;
+  bottom: 76px;
+  left: 0; right: 0;
+  display: flex;
+  justify-content: center;
+  z-index: 10;
+  padding: 0 16px;
+  pointer-events: none;
+}
+.silence-hint {
+  background: rgba(255,255,255,0.95);
+  backdrop-filter: blur(6px);
+  border: 1px solid #fed7aa;
+  border-radius: 14px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+  padding: 10px 14px;
+  display: flex;
+  align-items: flex-start;
+  gap: 10px;
+  max-width: 320px;
+  width: 100%;
+  pointer-events: auto;
+}
+.silence-icon { color: #fb923c; font-size: 14px; flex-shrink: 0; margin-top: 2px; }
+.silence-text { font-size: 12px; color: #4b5563; margin: 0; line-height: 1.5; flex: 1; }
+.silence-close {
+  background: transparent; border: none;
+  color: #d1d5db;
+  cursor: pointer;
+  padding: 2px;
+  flex-shrink: 0;
+  &:hover { color: #6b7280; }
+}
+
+// ─────────────────────────────────────────────
+// Control zone
+// ─────────────────────────────────────────────
+.control-zone {
+  flex-shrink: 0;
+  border-top: 1px solid #f3f4f6;
+  background: #fff;
+  padding: 10px 16px 12px;
+}
+.progress-wrap {
+  max-width: 384px;
+  margin: 0 auto 8px;
+}
+.progress-track {
+  height: 1px;
+  border-radius: 999px;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: #fb923c;
+  transition: width 1s linear, background-color 0.4s ease, opacity 0.2s ease;
+  &.near-limit { background: #f87171; }
+}
+.state-stack {
+  position: relative;
+  height: 36px;
+  max-width: 384px;
+  margin: 0 auto;
+}
+.state-layer {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+}
+.state-idle {
+  justify-content: center;
+  gap: 12px;
+}
+.state-center { justify-content: center; gap: 8px; }
+.state-error {
+  justify-content: space-between;
+  gap: 12px;
+}
+// idle buttons
+.hint-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  padding: 5px 14px;
+  border-radius: 999px;
+  background: #f9fafb;
+  border: 1px solid #e5e7eb;
+  color: #6b7280;
+  font-size: 11px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: background 0.2s, border-color 0.2s, color 0.2s;
+  &:hover {
+    background: #fff7ed;
+    border-color: #fed7aa;
+    color: #f97316;
+  }
+}
+.mic-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 7px 24px;
+  border-radius: 999px;
+  background: #f97316;
+  border: none;
+  color: #fff;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  box-shadow: 0 4px 10px rgba(249,115,22,0.25);
+  transition: background 0.2s, transform 0.15s;
+  &:hover { background: #ea580c; }
+  &:active { transform: scale(0.96); }
+  &:disabled { opacity: 0.5; cursor: not-allowed; background: #d1d5db; box-shadow: none; }
+}
+
+// recording capsule
+.record-capsule {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 10px;
+  border-radius: 999px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+}
+.cancel-btn, .finish-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 10px;
+  border-radius: 999px;
+  font-size: 11px;
+  font-weight: 500;
+  cursor: pointer;
+  flex-shrink: 0;
+  transition: background 0.2s, border-color 0.2s, color 0.2s;
+}
+.cancel-btn {
+  background: #fff;
+  border: 1px solid #e5e7eb;
+  color: #6b7280;
+  &:hover { background: #fef2f2; border-color: #fecaca; color: #ef4444; }
+}
+.finish-btn {
+  background: #f97316;
+  border: none;
+  color: #fff;
+  &:hover:not(:disabled) { background: #ea580c; }
+  &:disabled {
+    background: #d1d5db;
+    color: #9ca3af;
+    cursor: not-allowed;
+  }
+}
+.record-meter {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+}
+.animated-wave { display: flex; align-items: center; gap: 1px; }
+.aw-bar {
+  width: 2px;
+  border-radius: 999px;
+  background: #f97316;
+  animation: aw-pulse 1.2s ease-in-out infinite;
+  &.near-limit { background: #ef4444; }
+}
+@keyframes aw-pulse {
+  0%, 100% { opacity: 0.55; }
+  50%      { opacity: 1; }
+}
+.record-time {
+  font-size: 12px;
+  font-family: monospace;
+  font-weight: 600;
+  color: #1f2937;
+  font-variant-numeric: tabular-nums;
+  &.near-limit { color: #ef4444; }
+}
+.record-time-max { font-size: 10px; color: #d1d5db; }
+
+// stt / thinking
+.spinner {
+  color: #fb923c;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+.mini-avatar {
+  width: 16px; height: 16px;
+  border-radius: 50%;
+  background: #fff7ed;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 10px;
+}
+.center-text { font-size: 12px; color: #9ca3af; }
+
+// error
+.error-info { display: flex; align-items: center; gap: 6px; }
+.warn-icon { color: #f59e0b; font-size: 12px; }
+.warn-text { font-size: 12px; color: #4b5563; }
+.retry-pill {
+  padding: 5px 14px;
+  border-radius: 999px;
+  background: #f97316;
+  color: #fff;
+  border: none;
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  &:hover { background: #ea580c; }
+}
+
+// ─────────────────────────────────────────────
+// Modals
+// ─────────────────────────────────────────────
+.modal-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,0.3);
+  backdrop-filter: blur(2px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 50;
+  padding: 16px;
+}
+.modal {
+  background: #fff;
+  border-radius: 16px;
+  width: 100%;
+  max-height: 80vh;
+  overflow-y: auto;
+  box-shadow: 0 20px 60px rgba(0,0,0,0.15);
+}
+.phoneme-modal { max-width: 320px; }
+.exit-modal { max-width: 320px; padding: 20px; }
+
+.modal-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+.modal-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #111827;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin: 0;
+}
+.close-btn {
+  width: 26px; height: 26px;
+  background: #f3f4f6;
+  border: none;
+  border-radius: 8px;
+  color: #6b7280;
+  cursor: pointer;
+  display: flex; align-items: center; justify-content: center;
+  &:hover { background: #e5e7eb; }
+}
+
+// 音素
+.phoneme-word { text-align: left; font-size: 16px; font-weight: 700; color: #111827; margin: 0; }
+.phoneme-sub { font-size: 11px; color: #9ca3af; margin: 2px 0 0; }
+.phoneme-body {
+  padding: 0 20px 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.modal-head + .phoneme-body { padding-top: 0; }
+.phoneme-modal .modal-head { padding: 20px 20px 12px; margin-bottom: 0; }
+.pho-card {
+  border-radius: 12px;
+  padding: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+.pho-user { background: #fffbeb; border: 1px solid #fde68a; }
+.pho-standard { background: #ecfdf5; border: 1px solid #bbf7d0; }
+.pho-tip { background: #eff6ff; border: 1px solid #bfdbfe; display: block; }
+.pho-label { font-size: 10px; font-weight: 500; margin: 0 0 2px; }
+.pho-user .pho-label { color: #d97706; }
+.pho-standard .pho-label { color: #059669; }
+.pho-tip .pho-label { color: #2563eb; }
+.pho-value { font-size: 14px; font-family: monospace; color: #b45309; margin: 0; }
+.pho-value-green { color: #15803d; }
+.pho-tip-text { font-size: 12px; color: #1d4ed8; margin: 0; line-height: 1.5; }
+.pho-play {
+  width: 30px; height: 30px;
+  border-radius: 8px;
+  border: 1px solid;
+  display: flex; align-items: center; justify-content: center;
+  cursor: pointer;
+}
+.pho-play-user { background: #fef3c7; border-color: #fde68a; color: #d97706; &:hover { background: #fde68a; } }
+.pho-play-standard { background: #d1fae5; border-color: #bbf7d0; color: #059669; &:hover { background: #bbf7d0; } }
+.pho-practice-btn {
+  width: 100%;
+  padding: 9px;
+  border-radius: 12px;
+  background: #f97316;
+  color: #fff;
+  border: none;
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  &:hover { background: #ea580c; }
+}
+
+// 退出弹窗
+.exit-hint { font-size: 11px; color: #9ca3af; margin: 0 0 14px; }
+.exit-actions { display: flex; flex-direction: column; gap: 10px; }
+.exit-secondary {
+  padding: 9px 0;
+  border-radius: 12px;
+  background: transparent;
+  border: 1px solid #e5e7eb;
+  color: #4b5563;
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  &:hover { background: #f9fafb; }
+}
+.exit-primary {
+  padding: 9px 0;
+  border-radius: 12px;
+  background: #f97316;
+  border: none;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  &:hover { background: #ea580c; }
+}
+
+// 徽章
+.badge-popup {
+  position: fixed;
+  top: 64px;
+  right: 16px;
+  z-index: 60;
+}
+.badge-card {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 16px;
+  background: linear-gradient(to right, #f97316, #f59e0b);
+  border-radius: 16px;
+  box-shadow: 0 8px 24px rgba(249,115,22,0.3);
+}
+.badge-icon { font-size: 24px; }
+.badge-name { font-size: 12px; font-weight: 600; color: #fff; margin: 0; }
+.badge-desc { font-size: 10px; color: rgba(255,255,255,0.8); margin: 2px 0 0; }
+
+// ─────────────────────────────────────────────
+// Animations
+// ─────────────────────────────────────────────
+@keyframes breathing {
+  0%, 100% { transform: scale(1); }
+  50%      { transform: scale(1.06); box-shadow: 0 0 0 3px rgba(249,115,22,0.12); }
+}
+@keyframes fade-in-frames {
+  from { opacity: 0; transform: translateY(4px); }
+  to   { opacity: 1; transform: translateY(0); }
+}
+.fade-in { animation: fade-in-frames 0.22s ease-out; }
+@keyframes scale-in-frames {
+  from { opacity: 0; transform: scale(0.96); }
+  to   { opacity: 1; transform: scale(1); }
+}
+.scale-in { animation: scale-in-frames 0.22s ease-out; }
+
+// ─────────────────────────────────────────────
+// Finalizing screen (full-view loading while fetching report)
+// ─────────────────────────────────────────────
+.finalizing-screen {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  padding: 24px;
+  background: #fff;
+}
+.finalizing-spinner {
+  color: #fb923c;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  svg { animation: spin 1s linear infinite; }
+}
+.finalizing-title {
+  font-size: 14px;
+  color: #374151;
+  margin: 0;
+  font-weight: 500;
+}
+.finalizing-stats-card {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 18px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+  border-radius: 12px;
+  font-size: 12px;
+  color: #6b7280;
+  font-variant-numeric: tabular-nums;
+}
+.finalizing-stat { white-space: nowrap; }
+.finalizing-stats-sep { color: #d1d5db; }
+</style>

+ 336 - 0
src/views/Editor/EnglishSpeaking/preview/OverallReport.vue

@@ -0,0 +1,336 @@
+<template>
+  <div class="overall-report">
+    <template v-if="evaluation">
+      <!-- 完成提示 -->
+      <div class="completion-header">
+        <div class="completion-emoji">🎉</div>
+        <h2 class="completion-title">对话完成!</h2>
+        <p class="completion-subtitle">
+          与 {{ role?.name || 'AI' }} 对话 {{ evaluation.statistics.totalRounds }} 轮 · {{ formatDuration(evaluation.statistics.totalDuration) }}
+        </p>
+      </div>
+
+      <!-- 综合评分区域 -->
+      <div v-if="scoreDisplayMode !== 'comment_only'" class="score-section">
+        <div class="score-header">
+          <div>
+            <div class="score-label">综合评分</div>
+            <!-- 数字模式 -->
+            <div v-if="scoreDisplayMode === 'numeric'" class="score-display">
+              <span class="score-number">{{ evaluation.overallScore }}</span>
+              <span class="score-unit">分</span>
+            </div>
+            <!-- 字母模式 -->
+            <div v-else class="score-display">
+              <span class="score-letter" :style="{ color: letterColorMap[scoreToLetter(evaluation.overallScore)] }">
+                {{ scoreToLetter(evaluation.overallScore) }}
+              </span>
+              <span class="score-unit">级</span>
+            </div>
+          </div>
+          <div class="score-meta">
+            <div class="level-badge" :style="{ backgroundColor: levelConfig.bgColor, color: levelConfig.color }">
+              {{ levelConfig.label }}
+            </div>
+            <div class="percentile-text">
+              超过 <span class="percentile-value">{{ evaluation.percentile }}%</span> 同学
+            </div>
+          </div>
+        </div>
+
+        <!-- 四维能力分析 -->
+        <div class="ability-section">
+          <div class="ability-label">能力分析</div>
+          <div class="ability-bars">
+            <div v-for="dim in visibleDimensions" :key="dim.key" class="ability-row">
+              <span class="ability-name">{{ dim.label }}</span>
+              <div class="ability-track">
+                <div class="ability-fill" :style="{ width: `${getDimValue(dim.key)}%` }" />
+              </div>
+              <span class="ability-value">
+                {{ scoreDisplayMode === 'letter' ? scoreToLetter(getDimValue(dim.key)) : getDimValue(dim.key) }}
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- AI 点评 -->
+      <div class="ai-comment-section">
+        <div class="ai-comment-inner">
+          <div class="ai-avatar-circle">{{ role?.avatar || '👦' }}</div>
+          <div class="ai-comment-body">
+            <div class="ai-comment-name">{{ role?.name || 'AI' }} 说</div>
+            <p class="ai-comment-text">{{ evaluation.aiComment }}</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 亮点与建议 -->
+      <div class="highlights-grid">
+        <div class="highlight-card">
+          <div class="highlight-header"><span class="highlight-icon-good">✓</span> 亮点</div>
+          <div class="highlight-list">
+            <div v-for="(h, i) in evaluation.highlights.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
+          </div>
+        </div>
+        <div class="highlight-card">
+          <div class="highlight-header"><span class="highlight-icon-improve">→</span> 建议</div>
+          <div class="highlight-list">
+            <div v-for="(h, i) in evaluation.improvements.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
+          </div>
+        </div>
+      </div>
+    </template>
+
+    <!-- 无评分时的占位提示 -->
+    <div v-else class="completion-header">
+      <div class="completion-emoji">🎉</div>
+      <h2 class="completion-title">对话完成!</h2>
+    </div>
+
+    <!-- 统计数据 -->
+    <div class="stats-section">
+      <!-- <div class="stats-grid" :class="{ 'stats-narrow': scoreDisplayMode === 'comment_only' }">
+        <template v-if="scoreDisplayMode !== 'comment_only'">
+          <div class="stat-cell">
+            <div class="stat-value">
+              {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.averageScore) : evaluation.statistics.averageScore }}
+            </div>
+            <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '平均等级' : '平均分' }}</div>
+          </div>
+          <div class="stat-cell">
+            <div class="stat-value stat-green">
+              {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.highestScore) : evaluation.statistics.highestScore }}
+            </div>
+            <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '最高等级' : '最高分' }}</div>
+          </div>
+        </template>
+        <div class="stat-cell">
+          <div class="stat-value stat-blue">{{ evaluation.statistics.excellentExpressions }}</div>
+          <div class="stat-label">优秀表达</div>
+        </div>
+        <div class="stat-cell">
+          <div class="stat-value stat-amber">{{ evaluation.statistics.grammarErrors }}</div>
+          <div class="stat-label">语法错误</div>
+        </div>
+      </div> -->
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="action-buttons">
+      <button class="btn-secondary" @click="$emit('restart')">
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
+        </svg>
+        再来一次
+      </button>
+      <button class="btn-primary" @click="$emit('complete')">
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <polyline points="20 6 9 17 4 12" />
+        </svg>
+        完成
+      </button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import type { OverallEvaluation, ScoreLevel, ScoreDisplayMode, PreviewAIRole } from '@/types/englishSpeaking'
+
+interface Props {
+  evaluation: OverallEvaluation | null
+  role: PreviewAIRole | null
+  scoreDisplayMode?: ScoreDisplayMode
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  scoreDisplayMode: 'numeric',
+})
+
+defineEmits<{
+  restart: []
+  complete: []
+}>()
+
+const scoreLevelConfig: Record<ScoreLevel, { label: string; color: string; bgColor: string }> = {
+  excellent: { label: '优秀', color: '#22c55e', bgColor: '#dcfce7' },
+  good: { label: '良好', color: '#3b82f6', bgColor: '#dbeafe' },
+  fair: { label: '不错', color: '#f59e0b', bgColor: '#fef3c7' },
+  needsWork: { label: '继续加油', color: '#ef4444', bgColor: '#fee2e2' },
+}
+
+const letterColorMap: Record<string, string> = {
+  A: '#22c55e', B: '#3b82f6', C: '#f59e0b', D: '#f97316', E: '#ef4444',
+}
+
+const dimensionConfig = [
+  { label: '准确度', key: 'grammar' },
+  { label: '语调', key: 'interaction' },
+  { label: '重音', key: 'vocabulary' },
+  { label: '流畅度', key: 'fluency' },
+]
+
+const levelConfig = computed(() => scoreLevelConfig[props.evaluation?.scoreLevel ?? 'good'])
+const visibleDimensions = computed(() => dimensionConfig)
+
+function scoreToLetter(score: number): string {
+  if (score >= 90) return 'A'
+  if (score >= 80) return 'B'
+  if (score >= 70) return 'C'
+  if (score >= 60) return 'D'
+  return 'E'
+}
+
+function formatDuration(seconds: number) {
+  const mins = Math.floor(seconds / 60)
+  const secs = seconds % 60
+  return `${mins}分${secs}秒`
+}
+
+function getDimValue(key: string): number {
+  const dims = props.evaluation?.dimensions as Record<string, number> | undefined
+  return dims?.[key] || 0
+}
+</script>
+
+<style lang="scss" scoped>
+.overall-report {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.completion-header { text-align: center; }
+.completion-emoji { font-size: 30px; margin-bottom: 4px; }
+.completion-title { font-size: 16px; font-weight: 600; color: #111827; margin: 0; }
+.completion-subtitle { font-size: 12px; color: #6b7280; margin: 2px 0 0; }
+
+// 评分区域
+.score-section {
+  background: #fff;
+  border-radius: 12px;
+  border: 1px solid #f3f4f6;
+  overflow: hidden;
+}
+.score-header {
+  padding: 12px 16px;
+  background: linear-gradient(to right, #fff7ed, #f6f8ff);
+  border-bottom: 1px solid #f3f4f6;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.score-label { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
+.score-display { display: flex; align-items: baseline; gap: 4px; }
+.score-number { font-size: 32px; font-weight: 700; color: #f97316; }
+.score-letter { font-size: 32px; font-weight: 700; }
+.score-unit { font-size: 14px; color: #9ca3af; }
+.score-meta { text-align: right; }
+.level-badge {
+  display: inline-block;
+  padding: 4px 10px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 500;
+}
+.percentile-text { font-size: 10px; color: #9ca3af; margin-top: 4px; }
+.percentile-value { color: #f97316; font-weight: 500; }
+
+.ability-section { padding: 12px 16px; }
+.ability-label { font-size: 12px; color: #6b7280; margin-bottom: 8px; }
+.ability-bars { display: flex; flex-direction: column; gap: 10px; }
+.ability-row { display: flex; align-items: center; gap: 12px; }
+.ability-name { font-size: 12px; color: #4b5563; width: 40px; flex-shrink: 0; }
+.ability-track { flex: 1; height: 8px; background: #f3f4f6; border-radius: 999px; overflow: hidden; }
+.ability-fill { height: 100%; background: #f97316; border-radius: 999px; transition: width 0.5s; }
+.ability-value { font-size: 12px; font-weight: 500; color: #f97316; width: 32px; text-align: right; }
+
+// AI 点评
+.ai-comment-section {
+  background: #fff;
+  border-radius: 12px;
+  border: 1px solid #f3f4f6;
+  padding: 12px;
+}
+.ai-comment-inner { display: flex; gap: 10px; }
+.ai-avatar-circle {
+  width: 32px; height: 32px;
+  border-radius: 50%;
+  background: #fff7ed;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 16px;
+  flex-shrink: 0;
+}
+.ai-comment-body { flex: 1; min-width: 0; }
+.ai-comment-name { font-size: 12px; color: #9ca3af; margin-bottom: 4px; }
+.ai-comment-text { font-size: 12px; color: #374151; line-height: 1.6; margin: 0; }
+
+// 亮点与建议
+.highlights-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+.highlight-card {
+  background: #fff;
+  border-radius: 12px;
+  border: 1px solid #f3f4f6;
+  padding: 10px;
+}
+.highlight-header { font-size: 12px; color: #6b7280; margin-bottom: 6px; display: flex; align-items: center; gap: 4px; }
+.highlight-icon-good { color: #22c55e; }
+.highlight-icon-improve { color: #f59e0b; }
+.highlight-list { display: flex; flex-direction: column; gap: 4px; }
+.highlight-item { font-size: 10px; color: #4b5563; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+// 统计数据
+.stats-section {
+  background: #fff;
+  border-radius: 12px;
+  border: 1px solid #f3f4f6;
+  padding: 10px;
+}
+.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; text-align: center; }
+.stats-narrow { grid-template-columns: repeat(2, 1fr); }
+.stat-value { font-size: 16px; font-weight: 600; color: #1f2937; }
+.stat-green { color: #22c55e; }
+.stat-blue { color: #3b82f6; }
+.stat-amber { color: #f59e0b; }
+.stat-label { font-size: 9px; color: #9ca3af; }
+
+// 按钮
+.action-buttons {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  padding-top: 4px;
+}
+.btn-secondary {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  border-radius: 8px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #4b5563;
+  background: #f3f4f6;
+  border: none;
+  cursor: pointer;
+  &:hover { background: #e5e7eb; }
+}
+.btn-primary {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  border-radius: 8px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #fff;
+  background: #f97316;
+  border: none;
+  cursor: pointer;
+  &:hover { background: #ea580c; }
+}
+</style>

+ 155 - 0
src/views/Editor/EnglishSpeaking/preview/StudentPreview.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="student-preview" :class="{ 'fullscreen': fullscreen }">
+    <!-- PPT 预览模式:16:9 比例容器 -->
+    <div v-if="!fullscreen" class="preview-wrapper">
+      <div class="preview-card">
+        <!-- 标题区 -->
+        <div v-if="title || subtitle || progress" class="title-area">
+          <h1 v-if="title" class="preview-title">
+            <span v-if="titleIcon" class="title-icon">{{ titleIcon }}</span>
+            {{ title }}
+          </h1>
+          <p v-if="subtitle" class="preview-subtitle">{{ subtitle }}</p>
+          <p v-if="progress" class="preview-progress">{{ progress }}</p>
+        </div>
+
+        <!-- 内容区 -->
+        <div class="content-area">
+          <slot />
+        </div>
+
+        <!-- 操作区 -->
+        <div v-if="$slots.action" class="action-area">
+          <slot name="action" />
+        </div>
+      </div>
+    </div>
+
+    <!-- 全屏模式 -->
+    <div v-else class="fullscreen-wrapper">
+      <div v-if="title || subtitle || progress" class="title-area">
+        <h1 v-if="title" class="preview-title">
+          <span v-if="titleIcon" class="title-icon">{{ titleIcon }}</span>
+          {{ title }}
+        </h1>
+        <p v-if="subtitle" class="preview-subtitle">{{ subtitle }}</p>
+        <p v-if="progress" class="preview-progress">{{ progress }}</p>
+      </div>
+
+      <div class="content-area fullscreen-content">
+        <slot />
+      </div>
+
+      <div v-if="$slots.action" class="action-area">
+        <slot name="action" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+interface Props {
+  title?: string
+  subtitle?: string
+  titleIcon?: string
+  progress?: string
+  fullscreen?: boolean
+}
+
+withDefaults(defineProps<Props>(), {
+  fullscreen: false,
+})
+</script>
+
+<style lang="scss" scoped>
+.student-preview {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  overflow: hidden;
+  border-radius: 16px;
+}
+
+.preview-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  background: #f9fafb;
+}
+
+.preview-card {
+  width: 100%;
+  max-width: 900px;
+  aspect-ratio: 16 / 9;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.fullscreen-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  overflow: hidden;
+}
+
+.title-area {
+  padding: 24px 32px 16px;
+  text-align: center;
+  border-bottom: 1px solid #f9fafb;
+}
+
+.preview-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: #111827;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  margin: 0;
+}
+
+.title-icon {
+  font-size: 24px;
+}
+
+.preview-subtitle {
+  font-size: 14px;
+  color: #9ca3af;
+  margin: 4px 0 0;
+}
+
+.preview-progress {
+  font-size: 12px;
+  color: #9ca3af;
+  margin: 8px 0 0;
+}
+
+.content-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 24px 32px;
+  overflow: auto;
+}
+
+.fullscreen-content {
+  overflow: auto;
+}
+
+.action-area {
+  padding: 16px 32px;
+  border-top: 1px solid #f9fafb;
+  background: #fafbfc;
+}
+</style>

+ 490 - 0
src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue

@@ -0,0 +1,490 @@
+<template>
+  <div v-if="visible" class="modal-mask" @click.self="emit('close')">
+    <div
+      ref="dialogRef"
+      class="modal hint-modal scale-in"
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="task-hint-modal-title"
+      tabindex="-1"
+      @keydown="handleKeydown"
+    >
+      <div class="modal-head">
+        <h3 id="task-hint-modal-title" class="modal-title">
+          <svg
+            width="14"
+            height="14"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="#f97316"
+            stroke-width="2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          >
+            <path d="M9 18h6" />
+            <path d="M10 22h4" />
+            <path
+              d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"
+            />
+          </svg>
+          <span class="modal-title-text">
+            {{ aiName ? `${aiName} 的任务提示` : '任务提示' }}
+          </span>
+        </h3>
+        <button class="close-btn" type="button" aria-label="关闭" @click="emit('close')">
+          <svg
+            width="14"
+            height="14"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          >
+            <line x1="18" y1="6" x2="6" y2="18" />
+            <line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div v-if="loading" class="state-panel">
+        <p class="state-text">正在生成任务提示...</p>
+      </div>
+
+      <div v-else-if="error" class="state-panel error-panel">
+        <p class="error-text">{{ error }}</p>
+        <button class="retry-btn" type="button" @click="emit('retry')">
+          重试
+        </button>
+      </div>
+
+      <template v-else-if="hint">
+        <div class="hint-context">
+          <p class="context-label">当前问题</p>
+          <p class="context-body">{{ hint.current_question }}</p>
+        </div>
+
+        <div class="hint-section">
+          <p class="section-label">参考句子</p>
+          <div class="sentences">
+            <div
+              v-for="(sentence, index) in hint.example_sentences"
+              :key="`${sentence.english}-${index}`"
+              class="sentence-card"
+            >
+              <p class="sentence-en">{{ sentence.english }}</p>
+              <p class="sentence-zh">{{ sentence.chinese }}</p>
+            </div>
+          </div>
+        </div>
+
+        <div class="hint-section">
+          <p class="section-label">关键词汇</p>
+          <div class="vocab-grid">
+            <div
+              v-for="(vocab, index) in hint.key_vocabulary"
+              :key="`${vocab.word}-${index}`"
+              class="vocab-item"
+            >
+              <p class="vocab-word">{{ vocab.word }}</p>
+              <p class="vocab-meaning">{{ vocab.meaning }}</p>
+            </div>
+          </div>
+        </div>
+
+        <p class="hint-footer">用自己的话表达更棒哦</p>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
+import type { TaskHint } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  visible: boolean
+  loading: boolean
+  error?: string | null
+  hint?: TaskHint | null
+  aiName?: string
+}>()
+
+const emit = defineEmits<{
+  close: []
+  retry: []
+}>()
+
+const dialogRef = ref<HTMLElement | null>(null)
+const previouslyFocusedElement = ref<HTMLElement | null>(null)
+
+const focusableSelector = [
+  'a[href]',
+  'button:not([disabled])',
+  'textarea:not([disabled])',
+  'input:not([disabled])',
+  'select:not([disabled])',
+  '[tabindex]:not([tabindex="-1"])',
+].join(',')
+
+const getFocusableElements = () => {
+  const dialog = dialogRef.value
+  if (!dialog) return []
+
+  return Array.from(dialog.querySelectorAll<HTMLElement>(focusableSelector)).filter((el) => {
+    if (el.hasAttribute('disabled') || el.getAttribute('aria-hidden') === 'true') return false
+    return el.offsetParent !== null || el === document.activeElement
+  })
+}
+
+const restorePreviousFocus = () => {
+  const element = previouslyFocusedElement.value
+  previouslyFocusedElement.value = null
+
+  if (element && document.contains(element)) {
+    element.focus()
+  }
+}
+
+const focusDialog = async () => {
+  previouslyFocusedElement.value = document.activeElement instanceof HTMLElement
+    ? document.activeElement
+    : null
+
+  await nextTick()
+  dialogRef.value?.focus()
+}
+
+const containTabFocus = (event: KeyboardEvent) => {
+  const dialog = dialogRef.value
+  if (!dialog) return
+
+  const focusableElements = getFocusableElements()
+  if (!focusableElements.length) {
+    event.preventDefault()
+    dialog.focus()
+    return
+  }
+
+  const firstElement = focusableElements[0]
+  const lastElement = focusableElements[focusableElements.length - 1]
+  const activeElement = document.activeElement
+
+  if (event.shiftKey) {
+    if (activeElement === dialog || activeElement === firstElement || !dialog.contains(activeElement)) {
+      event.preventDefault()
+      lastElement.focus()
+    }
+    return
+  }
+
+  if (activeElement === dialog) {
+    event.preventDefault()
+    firstElement.focus()
+    return
+  }
+
+  if (activeElement === lastElement) {
+    event.preventDefault()
+    firstElement.focus()
+  }
+}
+
+const handleKeydown = (event: KeyboardEvent) => {
+  if (event.key === 'Escape') {
+    event.preventDefault()
+    emit('close')
+    return
+  }
+
+  if (event.key === 'Tab') {
+    containTabFocus(event)
+  }
+}
+
+watch(
+  () => props.visible,
+  (visible) => {
+    if (visible) {
+      void focusDialog()
+      return
+    }
+
+    restorePreviousFocus()
+  },
+  { flush: 'post', immediate: true },
+)
+
+onBeforeUnmount(() => {
+  restorePreviousFocus()
+})
+</script>
+
+<style lang="scss" scoped>
+.modal-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 50;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16px;
+  background: rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(2px);
+}
+
+.modal {
+  width: 100%;
+  max-height: 80vh;
+  overflow-y: auto;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
+}
+
+.hint-modal {
+  max-width: 440px;
+  min-width: 280px;
+  padding: 20px;
+}
+
+.modal-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.modal-title {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  gap: 6px;
+  margin: 0;
+  color: #111827;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 1.4;
+}
+
+.modal-title svg {
+  flex: 0 0 auto;
+}
+
+.modal-title-text {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.close-btn {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 26px;
+  height: 26px;
+  color: #6b7280;
+  cursor: pointer;
+  background: #f3f4f6;
+  border: none;
+  border-radius: 8px;
+
+  &:hover {
+    background: #e5e7eb;
+  }
+}
+
+.state-panel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 128px;
+  padding: 16px;
+  text-align: center;
+}
+
+.state-text,
+.error-text {
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.state-text {
+  color: #6b7280;
+}
+
+.error-panel {
+  gap: 12px;
+}
+
+.error-text {
+  color: #ef4444;
+  overflow-wrap: anywhere;
+}
+
+.retry-btn {
+  min-width: 72px;
+  height: 32px;
+  padding: 0 14px;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  line-height: 1;
+  cursor: pointer;
+  background: #f97316;
+  border: none;
+  border-radius: 8px;
+
+  &:hover {
+    background: #ea580c;
+  }
+}
+
+.hint-context {
+  padding: 12px 14px;
+  margin-bottom: 16px;
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+  border-radius: 12px;
+}
+
+.context-label,
+.section-label {
+  margin: 0 0 8px;
+  font-size: 10px;
+  font-weight: 500;
+  line-height: 1.4;
+  letter-spacing: 0.03em;
+  text-transform: uppercase;
+}
+
+.context-label {
+  margin-bottom: 4px;
+  color: #f97316;
+}
+
+.context-body {
+  margin: 0;
+  color: #374151;
+  font-size: 13px;
+  line-height: 1.5;
+  overflow-wrap: anywhere;
+}
+
+.hint-section {
+  margin-bottom: 16px;
+}
+
+.section-label {
+  color: #9ca3af;
+}
+
+.sentences {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.sentence-card {
+  min-width: 0;
+  padding: 10px 12px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+  border-radius: 12px;
+  transition: border-color 0.2s;
+
+  &:hover {
+    border-color: #fed7aa;
+  }
+}
+
+.sentence-en,
+.sentence-zh,
+.vocab-word,
+.vocab-meaning {
+  overflow-wrap: anywhere;
+}
+
+.sentence-en {
+  margin: 0;
+  color: #1f2937;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.sentence-zh {
+  margin: 2px 0 0;
+  color: #9ca3af;
+  font-size: 11px;
+  line-height: 1.5;
+}
+
+.vocab-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.vocab-item {
+  min-width: 0;
+  padding: 8px 10px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+  border-radius: 10px;
+}
+
+.vocab-word {
+  margin: 0;
+  color: #1f2937;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1.4;
+}
+
+.vocab-meaning {
+  margin: 2px 0 0;
+  color: #9ca3af;
+  font-size: 10px;
+  line-height: 1.5;
+}
+
+.hint-footer {
+  margin: 16px 0 0;
+  color: #d1d5db;
+  font-size: 11px;
+  line-height: 1.5;
+  text-align: center;
+}
+
+.scale-in {
+  animation: scale-in 0.18s ease-out;
+}
+
+@keyframes scale-in {
+  from {
+    opacity: 0;
+    transform: scale(0.96);
+  }
+
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+@media (max-width: 420px) {
+  .hint-modal {
+    padding: 16px;
+  }
+
+  .vocab-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 678 - 0
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue

@@ -0,0 +1,678 @@
+<template>
+  <div class="topic-discussion-fit-wrapper" ref="fitWrapperRef">
+  <div class="topic-discussion-preview" :style="fitStyle">
+    <!-- Ready 阶段:极简首页(参照 enspeak 布局) -->
+    <div v-if="dialogueState === 'checking-history'" class="ready-stage">
+      <div class="ready-header">
+        <h1 class="ready-title">
+          <!-- <span class="ready-title-icon">💬</span> -->
+          Topic Discussion
+        </h1>
+        <p class="ready-subtitle">正在读取你的练习记录...</p>
+      </div>
+      <div class="ready-body">
+        <span class="start-btn-spinner" />
+      </div>
+    </div>
+
+    <div v-else-if="dialogueState === 'ready'" class="ready-stage">
+      <div class="ready-header">
+        <h1 class="ready-title">
+          <!-- <span class="ready-title-icon">💬</span> -->
+          Topic Discussion
+        </h1>
+        <p class="ready-subtitle">话题讨论 · 与 AI 伙伴练习英语对话</p>
+      </div>
+
+      <div class="ready-body">
+        <!-- <span class="topic-emoji">🐼</span> -->
+        <h3 class="topic-name">{{ speakingStore.config.topic || topic }}</h3>
+      </div>
+
+      <div class="ready-footer">
+        <button class="start-btn" :disabled="sessionCreating || topicMissing" @click="startDialogue">
+          <svg v-if="!sessionCreating" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+            stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
+            <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
+            <line x1="12" y1="19" x2="12" y2="23" />
+            <line x1="8" y1="23" x2="16" y2="23" />
+          </svg>
+          <span v-else class="start-btn-spinner" />
+          {{ sessionCreating ? '创建中…' : '开始对话' }}
+        </button>
+        <p v-if="topicMissing" class="session-error-text">至少设置topic</p>
+        <p v-else-if="sessionError" class="session-error-text">{{ sessionError }}</p>
+      </div>
+    </div>
+
+    <!-- Chatting 阶段:直接渲染 DialogueChatView -->
+    <DialogueChatView
+      v-else-if="dialogueState === 'chatting'"
+      :topic="speakingStore.config.topic || topic"
+      :keywords="speakingStore.config.learningGoals.vocabulary.length ? speakingStore.config.learningGoals.vocabulary : keywords"
+      :ai-name="mockRole.name"
+      :ai-avatar="mockRole.avatar"
+      :total-rounds="speakingStore.config.practice.rounds || totalRounds"
+      :session-info="preparedSession"
+      @complete="handleDialogueComplete"
+      @restart="resetPreview"
+    />
+
+    <!-- Completed 阶段:直接渲染报告 -->
+    <div v-else class="report-stage">
+      <div class="report-scroll">
+        <div v-if="reportError" class="report-error">
+          {{ reportError }}
+        </div>
+        <OverallReport
+          :evaluation="overallEvaluationForDisplay"
+          :role="mockRole"
+          :scoreDisplayMode="'numeric'"
+          @restart="resetPreview"
+          @complete="resetPreview"
+        />
+        <div class="report-divider" />
+        <DetailedReport :sentenceEvaluations="displaySentenceEvaluations" />
+      </div>
+    </div>
+
+  </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted, onUnmounted, inject } from 'vue'
+import type { DialogueReport, OverallEvaluation, PreviewAIRole, PreviewDialogueState, SessionStartInfo } from '@/types/englishSpeaking'
+import { useSpeakingStore } from '@/store/speaking'
+import { getSpeakingConfig } from '@/services/speaking'
+import { createDialogueApi, DialogueApiError } from '../services/llmService'
+
+import DialogueChatView from './DialogueChatView.vue'
+import OverallReport from './OverallReport.vue'
+import DetailedReport from './DetailedReport.vue'
+
+interface Props {
+  topic?: string
+  keywords?: string[]
+  totalRounds?: number
+  /** 配置 id(由 FrameElement 从 elementInfo.url 传入) */
+  configId?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  topic: '',
+  keywords: () => ['favorite', 'adorable', 'bamboo', 'habitat', 'wildlife', 'endangered'],
+  totalRounds: 3,
+  configId: '',
+})
+
+type SpeakingNotify = (
+  status: 'active' | 'completed',
+  payload: { configId: string; sessionId: string },
+) => void
+const notifySpeakingProgress = inject<SpeakingNotify>('notifySpeakingProgress', () => {})
+const recordSpeakingStart = inject<(sessionId: string) => void>('recordSpeakingStart', () => {})
+
+const speakingStore = useSpeakingStore()
+
+// 让组件按 DESIGN_WIDTH 设计宽度等比放大到任意槽位尺寸:
+// 槽位(type 77 的 frame 默认为整张 slide ~1000×562)若直接装下按手机尺寸设计的 chat
+// 会显得字小、留白大、布局像桌面端。这里用 ResizeObserver + transform: scale 让内部
+// 始终按 DESIGN_WIDTH px 布局,再视觉缩放到槽位实际宽度。
+// 调小 → 内部元素更大;调大 → 内部元素更小(接近槽位实际像素)。
+const DESIGN_WIDTH = 1000
+const fitWrapperRef = ref<HTMLDivElement>()
+const fitScale = ref(1)
+const fitInnerHeight = ref(0)
+
+const fitStyle = computed(() => {
+  if (fitInnerHeight.value === 0) {
+    return { width: '100%', height: '100%' }
+  }
+  return {
+    width: DESIGN_WIDTH + 'px',
+    height: fitInnerHeight.value + 'px',
+    transform: `scale(${fitScale.value})`,
+    transformOrigin: 'top left',
+  }
+})
+
+let fitResizeObserver: ResizeObserver | null = null
+function updateFit() {
+  const el = fitWrapperRef.value
+  if (!el) return
+  const w = el.clientWidth
+  const h = el.clientHeight
+  if (w === 0 || h === 0) return
+  const scale = w / DESIGN_WIDTH
+  fitScale.value = scale
+  fitInnerHeight.value = h / scale
+}
+
+const dialogueState = ref<PreviewDialogueState>('ready')
+const sessionCreating = ref(false)
+const sessionError = ref<string | null>(null)
+const preparedSession = ref<SessionStartInfo | null>(null)
+const historyChecked = ref(false)
+const historyLoadToken = ref(0)
+
+const topicMissing = computed(() => !(speakingStore.config.topic ?? '').trim())
+
+const runtimeParams = computed(() => {
+  const params = new URLSearchParams(window.location.search)
+  return {
+    mode: params.get('mode'),
+    userId: params.get('userid'),
+  }
+})
+
+const isStudentRuntime = computed(() => runtimeParams.value.mode === 'student')
+const runtimeUserId = computed(() => runtimeParams.value.userId || '')
+
+const mockRole: PreviewAIRole = {
+  id: 'tom',
+  name: 'Amy',
+  avatar: '😊',
+  identity: 'Friendly Teacher',
+  personality: 'Patient and encouraging',
+  speakingStyle: 'casual',
+  speed: 'normal',
+  isCustom: false,
+  isRecommended: true,
+}
+
+const mockEvaluation: OverallEvaluation = {
+  overallScore: 85,
+  scoreLevel: 'good',
+  percentile: 78,
+  dimensions: {
+    fluency: 82,
+    interaction: 88,
+    vocabulary: 76,
+    grammar: 90,
+  },
+  aiComment: 'Great job! You showed good understanding of the topic. Your pronunciation was clear and your responses were relevant. Keep practicing to improve your fluency and try using more advanced vocabulary!',
+  highlights: [
+    'Clear pronunciation of key words',
+    'Good use of complete sentences',
+    'Natural conversation flow',
+  ],
+  improvements: [
+    'Try using more descriptive adjectives',
+    'Practice linking words for smoother speech',
+    'Expand vocabulary range for the topic',
+  ],
+  nextChallenge: {
+    difficulty: 'Medium',
+    unlockedTopic: 'My Dream Job',
+    suggestedMode: 'Free Talk',
+  },
+  statistics: {
+    totalRounds: 3,
+    averageScore: 83,
+    highestScore: 92,
+    highestRound: 2,
+    grammarErrors: 2,
+    excellentExpressions: 4,
+    totalDuration: 180,
+  },
+  sentenceEvaluations: [
+    {
+      id: 'se-1', round: 1, role: 'ai',
+      content: "Hi! What's your favorite animal?",
+      audioDuration: 3,
+    },
+    {
+      id: 'se-2', round: 1, role: 'student',
+      content: 'I like pandas. They are very cute!',
+      audioDuration: 4, score: 88,
+      pronunciation: { accuracy: 90, intonation: 85, stress: 82, fluency: 88 },
+      feedback: {
+        comment: 'Good pronunciation of "pandas" with natural intonation.',
+        betterExpression: 'I really love pandas because they are incredibly cute!',
+      },
+    },
+    {
+      id: 'se-3', round: 2, role: 'ai',
+      content: 'Pandas are adorable! Have you seen them at the zoo?',
+      audioDuration: 4,
+    },
+    {
+      id: 'se-4', round: 2, role: 'student',
+      content: 'Yes, I went to the zoo last month.',
+      audioDuration: 3, score: 92,
+      pronunciation: { accuracy: 95, intonation: 90, stress: 88, fluency: 92 },
+      feedback: {
+        comment: 'Excellent fluency and clear past tense usage.',
+        betterExpression: 'I visited the Beijing Zoo last month and saw giant pandas there.',
+      },
+    },
+    {
+      id: 'se-5', round: 3, role: 'ai',
+      content: "That's great! What do pandas like to eat?",
+      audioDuration: 3,
+    },
+    {
+      id: 'se-6', round: 3, role: 'student',
+      content: 'They like to eat bamboo.',
+      audioDuration: 3, score: 78,
+      pronunciation: { accuracy: 80, intonation: 75, stress: 76, fluency: 82 },
+      feedback: {
+        comment: 'Correct answer. Try using a more natural phrase for animal diets.',
+        betterExpression: 'Pandas mainly feed on bamboo, but they also eat fruits and vegetables.',
+      },
+    },
+  ],
+}
+
+const realEvaluation = ref<OverallEvaluation | null>(null)
+const reportStatus = ref<DialogueReport['status'] | null>(null)
+const reportError = ref('')
+const reportFetchFailed = ref(false)
+
+const displayEvaluation = computed<OverallEvaluation | null>(() => {
+  const real = realEvaluation.value
+  if (real) return real
+  if (reportFetchFailed.value) return null
+  return mockEvaluation
+})
+
+const shouldShowOverallReport = computed(() => {
+  return !!displayEvaluation.value
+    && !reportError.value
+    && reportStatus.value !== 'failed'
+    && reportStatus.value !== 'incomplete'
+})
+
+const overallEvaluationForDisplay = computed(() => shouldShowOverallReport.value ? displayEvaluation.value : null)
+const displaySentenceEvaluations = computed(() => displayEvaluation.value?.sentenceEvaluations ?? [])
+
+function isHistoryTokenCurrent(token: number) {
+  return token === historyLoadToken.value
+}
+
+function nextHistoryLoadToken() {
+  historyLoadToken.value += 1
+  return historyLoadToken.value
+}
+
+async function startDialogue() {
+  if (sessionCreating.value) return
+  if (isStudentRuntime.value) {
+    if (!props.configId) {
+      sessionError.value = '口语工具配置缺失,请联系老师重新发布。'
+      return
+    }
+    if (!runtimeUserId.value) {
+      sessionError.value = '学生身份缺失,请从课程入口重新进入。'
+      return
+    }
+  }
+  sessionCreating.value = true
+  sessionError.value = null
+  reportFetchFailed.value = false
+  try {
+    const api = createDialogueApi()
+    const info = await api.createSession({
+      topic: speakingStore.config.topic || props.topic,
+      grade: speakingStore.config.grade,
+      totalRounds: speakingStore.config.practice.rounds ?? props.totalRounds,
+      durationMinutes: speakingStore.config.practice.duration,
+      roleId: mockRole.id,
+      vocabulary: speakingStore.config.learningGoals.vocabulary,
+      sentences: speakingStore.config.learningGoals.sentences,
+      configId: props.configId || null,
+      userId: isStudentRuntime.value ? runtimeUserId.value : null,
+    })
+    preparedSession.value = {
+      sessionId: info.sessionId,
+      expiresAt: info.expiresAt,
+      currentRound: info.currentRound,
+    }
+    dialogueState.value = 'chatting'
+    notifySpeakingProgress('active', {
+      configId: props.configId || '',
+      sessionId: preparedSession.value?.sessionId || '',
+    })
+    recordSpeakingStart(preparedSession.value?.sessionId || '')
+  } catch (err: unknown) {
+    if (err instanceof DialogueApiError) {
+      sessionError.value = `创建会话失败(${err.status}),请重试`
+    } else {
+      sessionError.value = '创建会话失败,请重试'
+    }
+  } finally {
+    sessionCreating.value = false
+  }
+}
+
+async function waitForCompletedHistoryReport(
+  api: ReturnType<typeof createDialogueApi>,
+  sessionId: string,
+  token: number,
+): Promise<DialogueReport | null> {
+  const maxAttempts = 30
+  const intervalMs = 2000
+
+  try {
+    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+      if (!isHistoryTokenCurrent(token)) return null
+      const report = await api.getReport(sessionId)
+      if (!isHistoryTokenCurrent(token)) return null
+      if (report.status === 'ready' || report.status === 'failed' || report.status === 'incomplete') {
+        return report
+      }
+      await new Promise(resolve => setTimeout(resolve, intervalMs))
+      if (!isHistoryTokenCurrent(token)) return null
+    }
+  } catch (err) {
+    console.error('[speaking] load completed history report failed:', err)
+    return null
+  }
+
+  return null
+}
+
+async function loadLatestStudentSession(token = nextHistoryLoadToken()) {
+  if (historyChecked.value) return
+  historyChecked.value = true
+  if (!isStudentRuntime.value) return
+
+  if (!props.configId) {
+    if (!isHistoryTokenCurrent(token)) return
+    sessionError.value = '口语工具配置缺失,请联系老师重新发布。'
+    return
+  }
+  if (!runtimeUserId.value) {
+    if (!isHistoryTokenCurrent(token)) return
+    sessionError.value = '学生身份缺失,请从课程入口重新进入。'
+    return
+  }
+
+  if (!isHistoryTokenCurrent(token)) return
+  dialogueState.value = 'checking-history'
+  sessionError.value = null
+  try {
+    const api = createDialogueApi()
+    const { session } = await api.getLatestSession(props.configId, runtimeUserId.value)
+    if (!isHistoryTokenCurrent(token)) return
+    if (!session) {
+      preparedSession.value = null
+      dialogueState.value = 'ready'
+      return
+    }
+
+    preparedSession.value = {
+      sessionId: session.sessionId,
+      expiresAt: session.expiresAt,
+      currentRound: session.currentRound,
+      messages: session.messages,
+    }
+
+    if (session.status === 'completed') {
+      const report = await waitForCompletedHistoryReport(api, session.sessionId, token)
+      if (!isHistoryTokenCurrent(token)) return
+      handleDialogueComplete(report)
+      return
+    }
+
+    if (session.status !== 'active') {
+      preparedSession.value = null
+      sessionError.value = '上次练习已失效,请重新开始。'
+      dialogueState.value = 'ready'
+      return
+    }
+
+    dialogueState.value = 'chatting'
+    notifySpeakingProgress('active', {
+      configId: props.configId || '',
+      sessionId: preparedSession.value?.sessionId || '',
+    })
+  } catch (err: unknown) {
+    if (!isHistoryTokenCurrent(token)) return
+    console.error('[speaking] load latest session failed:', err)
+    dialogueState.value = 'ready'
+    if (err instanceof DialogueApiError) {
+      sessionError.value = `读取历史会话失败(${err.status}),请刷新重试`
+    } else {
+      sessionError.value = '读取历史会话失败,请刷新重试'
+    }
+  }
+}
+
+function handleDialogueComplete(report: DialogueReport | null) {
+  reportFetchFailed.value = !report
+  reportStatus.value = report?.status ?? null
+  realEvaluation.value = report?.evaluation ?? null
+  if (!report) reportError.value = '报告生成超时或获取失败,请稍后重试。'
+  else if (report.status === 'failed') reportError.value = '报告生成失败,部分语音评分未完成。'
+  else if (report?.status === 'incomplete') reportError.value = '本次练习没有足够的有效回答生成报告。'
+  else reportError.value = ''
+  dialogueState.value = 'completed'
+  if (preparedSession.value?.sessionId) {
+    notifySpeakingProgress('completed', {
+      configId: props.configId || '',
+      sessionId: preparedSession.value.sessionId,
+    })
+  }
+}
+
+function resetPreview() {
+  nextHistoryLoadToken()
+  dialogueState.value = 'ready'
+  realEvaluation.value = null
+  reportStatus.value = null
+  reportError.value = ''
+  reportFetchFailed.value = false
+  preparedSession.value = null
+  sessionError.value = null
+  sessionCreating.value = false
+}
+
+// ── Sync with speakingStore (让 CanvasTool 可以驱动"重置预览") ──
+watch(dialogueState, (s) => { speakingStore.setPreviewState(s) }, { immediate: true })
+
+watch(
+  () => speakingStore.resetSignal,
+  (val, old) => {
+    if (val !== old) resetPreview()
+  },
+)
+
+// ── 根据 configId 从后端拉回配置注入 store ──
+async function loadConfigFromBackend(id: string) {
+  const token = nextHistoryLoadToken()
+  if (!id) {
+    await loadLatestStudentSession(token)
+    return
+  }
+  try {
+    const { config } = await getSpeakingConfig(id)
+    if (!isHistoryTokenCurrent(token)) return
+    speakingStore.$patch({ config })
+  } catch (err) {
+    if (!isHistoryTokenCurrent(token)) return
+    console.error('[speaking] load config failed:', err)
+  } finally {
+    if (isHistoryTokenCurrent(token)) {
+      await loadLatestStudentSession(token)
+    }
+  }
+}
+
+watch(() => props.configId, (id) => {
+  historyChecked.value = false
+  loadConfigFromBackend(id)
+})
+
+onMounted(() => {
+  speakingStore.setPreviewState(dialogueState.value)
+  loadConfigFromBackend(props.configId)
+  if (fitWrapperRef.value) {
+    fitResizeObserver = new ResizeObserver(updateFit)
+    fitResizeObserver.observe(fitWrapperRef.value)
+    updateFit()
+  }
+})
+onUnmounted(() => {
+  nextHistoryLoadToken()
+  speakingStore.setPreviewState('ready')
+  fitResizeObserver?.disconnect()
+  fitResizeObserver = null
+})
+</script>
+
+<style lang="scss" scoped>
+// 外层 fit-wrapper 始终撑满槽位(type 77 的 frame 默认 ~1000×562 设计 CSS-px)。
+// ResizeObserver 观察它的尺寸,再用 transform: scale() 把内层按 DESIGN_WIDTH 设计尺寸放大填满。
+.topic-discussion-fit-wrapper {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+}
+
+.topic-discussion-preview {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  position: relative;
+  overflow: hidden;
+
+  // flex 嵌套 overflow 生效的前提:flex 子默认 min-height: auto 会被内容撑大,
+  // 让 ready-stage / report-stage / DialogueChatView 根节点能 shrink 到父高度
+  > * {
+    min-height: 0;
+  }
+}
+
+// ─── Ready 阶段 ───
+.ready-stage {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+}
+.ready-header {
+  padding: 20px 24px 12px;
+  text-align: center;
+  border-bottom: 1px solid #f9fafb;
+}
+.ready-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: #111827;
+  margin: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+.ready-title-icon { font-size: 22px; }
+.ready-subtitle {
+  font-size: 13px;
+  color: #9ca3af;
+  margin: 4px 0 0;
+}
+.ready-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  padding: 24px;
+}
+.topic-emoji { font-size: 56px; line-height: 1; }
+.topic-name {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2937;
+  margin: 0;
+}
+.ready-footer {
+  padding: 16px 24px 20px;
+  border-top: 1px solid #f9fafb;
+  background: #fafbfc;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.start-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 9px 28px;
+  border-radius: 999px;
+  background: #f97316;
+  color: #fff;
+  border: none;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  box-shadow: 0 4px 12px rgba(249, 115, 22, 0.25);
+  transition: background 0.2s, transform 0.15s;
+  &:hover { background: #ea580c; }
+  &:active { transform: scale(0.97); }
+}
+.start-btn:disabled {
+  opacity: 0.6;
+  cursor: wait;
+  background: #fb923c;
+}
+.start-btn-spinner {
+  width: 14px;
+  height: 14px;
+  border: 2px solid rgba(255, 255, 255, 0.4);
+  border-top-color: #fff;
+  border-radius: 50%;
+  animation: start-btn-spin 0.8s linear infinite;
+  display: inline-block;
+}
+@keyframes start-btn-spin {
+  to { transform: rotate(360deg); }
+}
+.session-error-text {
+  margin: 8px 0 0;
+  font-size: 12px;
+  color: #dc2626;
+  text-align: center;
+}
+
+// ─── Completed 阶段 ───
+.report-stage {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: #fff;
+}
+.report-scroll {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+.report-divider {
+  height: 1px;
+  background: #e5e7eb;
+  margin: 0;
+}
+.report-error {
+  max-width: 448px;
+  margin: 0 auto 12px;
+  padding: 10px 12px;
+  border: 1px solid #fed7aa;
+  border-radius: 8px;
+  background: #fff7ed;
+  color: #c2410c;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+</style>

+ 320 - 0
src/views/Editor/EnglishSpeaking/services/llmService.ts

@@ -0,0 +1,320 @@
+import type {
+  DialogueAPI,
+  LatestSessionResponse,
+  SSEEvent,
+  SessionConfig,
+  SessionInfo,
+  GreetingInfo,
+  TaskHint,
+  DialogueReport,
+  SentenceEvaluation,
+} from '@/types/englishSpeaking'
+import { SPEAKING_DIALOGUE_API_BASE_URL } from './speakingApiConfig'
+
+const API_BASE = SPEAKING_DIALOGUE_API_BASE_URL
+
+export class DialogueApiError extends Error {
+  status: number
+  constructor(message: string, status: number) {
+    super(message)
+    this.status = status
+    this.name = 'DialogueApiError'
+  }
+}
+
+// ==================== SSE 解析 ====================
+
+async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<SSEEvent> {
+  const decoder = new TextDecoder()
+  let buffer = ''
+
+  try {
+    while (true) {
+      const { done, value } = await reader.read()
+      if (done) break
+
+      buffer += decoder.decode(value, { stream: true })
+      const lines = buffer.split('\n')
+      buffer = lines.pop() || ''
+
+      let eventType = ''
+      let data = ''
+
+      for (const line of lines) {
+        if (line.startsWith('event:')) {
+          eventType = line.slice(6).trim()
+        } else if (line.startsWith('data:')) {
+          data = line.slice(5).trim()
+        } else if (line === '' && eventType && data) {
+          try {
+            const parsed = JSON.parse(data)
+            if (eventType === 'transcript') {
+              yield {
+                type: 'transcript',
+                text: parsed.text,
+                audioDuration: parsed.audioDuration ?? null,
+              }
+            } else if (eventType === 'token') {
+              yield { type: 'token', text: parsed.content ?? parsed.text }
+            } else if (eventType === 'done') {
+              yield { type: 'done', isComplete: parsed.isComplete }
+            } else if (eventType === 'error') {
+              yield { type: 'error', message: parsed.message }
+            }
+          } catch {
+            // skip malformed JSON
+          }
+          eventType = ''
+          data = ''
+        }
+      }
+    }
+  } finally {
+    reader.releaseLock()
+  }
+}
+
+// ==================== Backend shape types ====================
+
+interface BackendEvaluation {
+  status: 'pending' | 'completed' | 'failed'
+  accuracyScore: number | null
+  fluencyScore: number | null
+  completenessScore: number | null
+  prosodyScore: number | null
+  wordAnalysis: unknown
+  contentFeedback: {
+    comment: string
+    betterExpression: string
+  } | null
+}
+
+interface BackendRound {
+  round: number
+  role: 'ai' | 'student'
+  content: string
+  audioUrl: string | null
+  audioDuration: number | null   // NEW: 秒,由后端 /report 透传
+  evaluation?: BackendEvaluation
+}
+
+interface BackendReportResponse {
+  sessionId: string
+  topic: string
+  status: 'evaluating' | 'ready' | 'failed' | 'incomplete'
+  rounds: BackendRound[]
+  overall: BackendOverall | null
+  summary: string | null
+  totalDurationSeconds: number | null
+}
+
+interface BackendOverall {
+  aiComment: string
+  highlights: string[]
+  improvements: string[]
+}
+
+function hasCompleteScores(evaluation?: BackendEvaluation): evaluation is BackendEvaluation & {
+  accuracyScore: number
+  fluencyScore: number
+  completenessScore: number
+  prosodyScore: number
+} {
+  return !!evaluation
+    && evaluation.status === 'completed'
+    && typeof evaluation.accuracyScore === 'number'
+    && typeof evaluation.fluencyScore === 'number'
+    && typeof evaluation.completenessScore === 'number'
+    && typeof evaluation.prosodyScore === 'number'
+}
+
+function adaptReport(raw: BackendReportResponse): DialogueReport {
+  const sentenceEvaluations: SentenceEvaluation[] = raw.rounds.map((r, idx) => {
+    const pronunciation = r.role === 'student' && hasCompleteScores(r.evaluation)
+      ? {
+          accuracy: r.evaluation.accuracyScore,
+          fluency: r.evaluation.fluencyScore,
+          intonation: r.evaluation.prosodyScore,
+          stress: r.evaluation.completenessScore,
+        }
+      : undefined
+
+    return {
+      id: `${raw.sessionId}-${idx}`,
+      round: r.round,
+      role: r.role,
+      content: r.content,
+      audioUrl: r.audioUrl ?? undefined,
+      audioDuration: r.audioDuration ?? undefined,
+      score: pronunciation
+        ? Math.round((pronunciation.accuracy + pronunciation.fluency + pronunciation.intonation + pronunciation.stress) / 4)
+        : undefined,
+      pronunciation,
+      feedback: r.evaluation?.contentFeedback ?? undefined,
+    }
+  })
+
+  const studentEvals = sentenceEvaluations.filter(s => s.role === 'student' && s.pronunciation)
+  const avg = studentEvals.length > 0
+    ? Math.round(
+        studentEvals.reduce(
+          (sum, s) => sum + (s.pronunciation!.accuracy + s.pronunciation!.fluency + s.pronunciation!.intonation + s.pronunciation!.stress) / 4,
+          0,
+        ) / studentEvals.length,
+      )
+    : 0
+  const avgDim = (key: 'accuracy' | 'fluency' | 'intonation' | 'stress') => {
+    if (studentEvals.length === 0) return 0
+    return Math.round(studentEvals.reduce((sum, s) => sum + (s.pronunciation?.[key] ?? 0), 0) / studentEvals.length)
+  }
+  const overall = raw.overall
+  const highest = studentEvals.reduce<SentenceEvaluation | null>(
+    (best, s) => (!best || (s.score ?? 0) > (best.score ?? 0) ? s : best),
+    null,
+  )
+
+  return {
+    status: raw.status,
+    evaluation: {
+      overallScore: avg,
+      scoreLevel: avg >= 85 ? 'excellent' : avg >= 70 ? 'good' : avg >= 60 ? 'fair' : 'needsWork',
+      percentile: 0,
+      dimensions: {
+        fluency: avgDim('fluency'),
+        interaction: avgDim('intonation'),
+        vocabulary: avgDim('stress'),
+        grammar: avgDim('accuracy'),
+      },
+      aiComment: overall?.aiComment ?? raw.summary ?? '',
+      highlights: overall?.highlights ?? [],
+      improvements: overall?.improvements ?? [],
+      nextChallenge: {},
+      statistics: {
+        totalRounds: sentenceEvaluations.length ? Math.max(...sentenceEvaluations.map(s => s.round)) : 0,
+        averageScore: avg,
+        highestScore: highest?.score ?? 0,
+        highestRound: highest?.round ?? 0,
+        grammarErrors: 0,
+        excellentExpressions: 0,
+        totalDuration: raw.totalDurationSeconds ?? 0,
+      },
+      sentenceEvaluations,
+    },
+  }
+}
+
+// ==================== Real API ====================
+
+export class RealDialogueAPI implements DialogueAPI {
+  async createSession(config: SessionConfig): Promise<SessionInfo> {
+    const res = await fetch(`${API_BASE}/session`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      credentials: 'include',
+      body: JSON.stringify({
+        topic: config.topic,
+        grade: config.grade,
+        vocabulary: config.vocabulary ?? [],
+        sentences: config.sentences ?? [],
+        totalRounds: config.totalRounds,
+        durationMinutes: config.durationMinutes,
+        roleId: config.roleId,
+        configId: config.configId ?? null,
+        userId: config.userId ?? null,
+      }),
+    })
+    if (!res.ok) {
+      throw new DialogueApiError(`createSession failed: ${res.status}`, res.status)
+    }
+    const body = await res.json()
+    return {
+      sessionId: body.sessionId,
+      totalRounds: body.totalRounds,
+      currentRound: body.currentRound,
+      expiresAt: body.expiresAt ?? null,
+    }
+  }
+
+  async getLatestSession(configId: string, userId: string): Promise<LatestSessionResponse> {
+    const params = new URLSearchParams({ configId, userId })
+    const res = await fetch(`${API_BASE}/sessions/latest?${params.toString()}`, {
+      method: 'GET',
+      credentials: 'include',
+    })
+    if (!res.ok) {
+      throw new DialogueApiError(`getLatestSession failed: ${res.status}`, res.status)
+    }
+    return res.json()
+  }
+
+  async completeSession(sessionId: string): Promise<void> {
+    const res = await fetch(`${API_BASE}/session/${encodeURIComponent(sessionId)}/complete`, {
+      method: 'POST',
+      credentials: 'include',
+    })
+    if (!res.ok) {
+      throw new DialogueApiError(`completeSession failed: ${res.status}`, res.status)
+    }
+  }
+
+  async generateGreeting(sessionId: string, turnId: string, signal?: AbortSignal): Promise<GreetingInfo> {
+    const res = await fetch(`${API_BASE}/session/${sessionId}/greeting`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      credentials: 'include',
+      signal,
+      body: JSON.stringify({ turnId }),
+    })
+    if (!res.ok) {
+      const text = await res.text().catch(() => '')
+      throw new DialogueApiError(
+        `greeting failed: ${res.status}${text ? ` (${text.slice(0, 100)})` : ''}`,
+        res.status,
+      )
+    }
+    const body = await res.json()
+    return { aiMessage: body.aiMessage }
+  }
+
+  async generateTaskHint(sessionId: string): Promise<TaskHint> {
+    const res = await fetch(`${API_BASE}/session/${sessionId}/task-hint`, {
+      method: 'POST',
+      credentials: 'include',
+    })
+    if (!res.ok) {
+      throw new DialogueApiError(`task hint failed: ${res.status}`, res.status)
+    }
+    return await res.json()
+  }
+
+  async *speak(sessionId: string, audioBlob: Blob, signal: AbortSignal, turnId: string): AsyncGenerator<SSEEvent> {
+    const formData = new FormData()
+    formData.append('sessionId', sessionId)
+    formData.append('audio', audioBlob, 'recording.webm')
+    formData.append('turnId', turnId)
+
+    const res = await fetch(`${API_BASE}/speak`, {
+      method: 'POST',
+      credentials: 'include',
+      body: formData,
+      signal,
+    })
+    if (!res.ok) throw new Error(`speak failed: ${res.status}`)
+    if (!res.body) throw new Error('No response body')
+
+    yield* parseSSEStream(res.body.getReader())
+  }
+
+  async getReport(sessionId: string): Promise<DialogueReport> {
+    const res = await fetch(`${API_BASE}/report?sessionId=${encodeURIComponent(sessionId)}`, {
+      credentials: 'include',
+    })
+    if (!res.ok) throw new Error(`getReport failed: ${res.status}`)
+    const raw: BackendReportResponse = await res.json()
+    return adaptReport(raw)
+  }
+}
+
+export function createDialogueApi(): DialogueAPI {
+  return new RealDialogueAPI()
+}

+ 18 - 0
src/views/Editor/EnglishSpeaking/services/speakingApiConfig.ts

@@ -0,0 +1,18 @@
+export const FALLBACK_SPEAKING_API_HOST = 'https://ppt-english-speaking-api.cocorobo.cn'
+
+const ENV_SPEAKING_API_HOST = import.meta.env.VITE_SPEAKING_API_HOST?.trim()
+const SPEAKING_API_HOST = (ENV_SPEAKING_API_HOST || FALLBACK_SPEAKING_API_HOST).replace(/\/+$/, '')
+
+export const SPEAKING_DIALOGUE_API_BASE_URL = `${SPEAKING_API_HOST}/api/speaking/dialogue`
+export const SPEAKING_CONFIG_API_BASE_URL = `${SPEAKING_API_HOST}/api/speaking/config`
+
+export type SpeakTransport = 'websocket' | 'http'
+
+const ENV_SPEAKING_TRANSPORT = import.meta.env.VITE_SPEAKING_TRANSPORT?.trim().toLowerCase()
+
+export const SPEAKING_TRANSPORT: SpeakTransport = ENV_SPEAKING_TRANSPORT === 'http' ? 'http' : 'websocket'
+
+export function buildSpeakingWsUrl(path: string): string {
+  const normalizedPath = path.startsWith('/') ? path : `/${path}`
+  return `${SPEAKING_DIALOGUE_API_BASE_URL.replace(/^http/, 'ws')}${normalizedPath}`
+}

+ 60 - 0
src/views/Editor/EnglishSpeaking/services/speechService.ts

@@ -0,0 +1,60 @@
+const KEY = import.meta.env.VITE_AZURE_SPEECH_KEY as string | undefined
+const REGION = import.meta.env.VITE_AZURE_SPEECH_REGION as string | undefined
+// 美式男声,口齿清晰、随和。备选(都是 en-US-*Neural):
+//   GuyNeural / AndrewNeural / ChristopherNeural / TonyNeural(男)
+//   JennyNeural / EmmaNeural / AvaNeural(女,清晰度也高)
+const VOICE = 'en-US-JennyNeural'
+const FORMAT = 'audio-24khz-48kbitrate-mono-mp3'
+// 默认偏自然偏快;调慢改 -10%、调更快改 +25% 之类。
+const RATE = '+10%'
+
+/**
+ * Synthesize English text via Azure Speech REST.
+ * Returns an MP3 Blob. Throws on credential / network / non-2xx.
+ *
+ * Pass an AbortSignal so callers (the audio player) can cancel
+ * an in-flight synthesis when the user starts recording or
+ * triggers a different playback.
+ */
+export async function synthesize(text: string, signal?: AbortSignal): Promise<Blob> {
+  if (!KEY || !REGION) {
+    throw new Error('Azure Speech credentials not configured (VITE_AZURE_SPEECH_KEY / VITE_AZURE_SPEECH_REGION)')
+  }
+
+  const ssml =
+    `<speak version='1.0' xml:lang='en-US'>` +
+    `<voice name='${VOICE}'>` +
+    `<prosody rate='${RATE}'>${escapeXml(text)}</prosody>` +
+    `</voice>` +
+    `</speak>`
+
+  const res = await fetch(
+    `https://${REGION}.tts.speech.microsoft.com/cognitiveservices/v1`,
+    {
+      method: 'POST',
+      signal,
+      headers: {
+        'Ocp-Apim-Subscription-Key': KEY,
+        'Content-Type': 'application/ssml+xml',
+        'X-Microsoft-OutputFormat': FORMAT,
+        'User-Agent': 'PPT-EnglishSpeaking',
+      },
+      body: ssml,
+    },
+  )
+
+  if (!res.ok) {
+    throw new Error(`Azure TTS failed: ${res.status} ${res.statusText}`)
+  }
+  return res.blob()
+}
+
+function escapeXml(s: string): string {
+  return s.replace(/[<>&'"]/g, c => ({
+    '<': '&lt;',
+    '>': '&gt;',
+    '&': '&amp;',
+    "'": '&apos;',
+    '"': '&quot;',
+  }[c]!))
+}

+ 2 - 0
src/views/Editor/index3.vue

@@ -329,6 +329,7 @@ const handleDelete = () => {
 }
 
 const changeCourse = () => {
+  console.trace('[TRACE] changeCourse fired → parentWindow.save(3)')
   parentWindow.setCouresTitle?.(courseTitle.value)
   parentWindow.save?.(3)
 }
@@ -348,6 +349,7 @@ const handlePreview = () => {
 }
 
 const handleSave = () => {
+  console.trace('[TRACE] handleSave fired → parentWindow.save(1)')
   console.log('保存课程')
   // lastSaveTime.value = new Date().toLocaleString()
   parentWindow.save?.(1)

+ 117 - 0
src/views/Student/components/SpeakingClassPanel/AISummary.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="ai-summary">
+    <div class="ai-header">
+      <h3 class="ai-title">{{ lang.ssSpkAISummary }}</h3>
+      <button class="ai-refresh-btn" :disabled="loading" @click="$emit('refresh')">
+        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+             stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+             :class="{ spinning: loading }">
+          <path d="M1 4v6h6M23 20v-6h-6" />
+          <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
+        </svg>
+        {{ lang.ssSpkRefresh }}
+      </button>
+    </div>
+    <ul class="ai-bullets">
+      <li>• {{ bullets[0] }}</li>
+      <li v-if="loading && !bullets[1]" class="ai-skeleton"><span /></li>
+      <li v-else>• {{ bullets[1] }}</li>
+      <li v-if="loading && !bullets[2]" class="ai-skeleton"><span /></li>
+      <li v-else>• {{ bullets[2] }}</li>
+    </ul>
+    <div v-if="generatedAtLabel" class="ai-meta">{{ generatedAtLabel }}</div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, onMounted, onUnmounted } from 'vue'
+import { lang } from '@/main'
+
+const props = defineProps<{
+  bullets: [string, string, string]
+  loading: boolean
+  generatedAt: string | null
+}>()
+defineEmits<{ refresh: [] }>()
+
+// 「刚刚生成」/「N 秒前生成」/「N 分钟前生成」 — 每秒重算
+const now = ref(Date.now())
+let timer: number | null = null
+onMounted(() => { timer = window.setInterval(() => { now.value = Date.now() }, 1000) })
+onUnmounted(() => { if (timer) clearInterval(timer) })
+
+function formatTpl(tpl: string, vars: Record<string, string | number>): string {
+  return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ''))
+}
+
+const generatedAtLabel = computed(() => {
+  if (!props.generatedAt) return ''
+  const ts = new Date(props.generatedAt).getTime()
+  if (isNaN(ts)) return ''
+  const elapsed = Math.max(0, Math.floor((now.value - ts) / 1000))
+  if (elapsed < 5)  return (lang as any).ssSpkJustNow
+  if (elapsed < 60) return formatTpl((lang as any).ssSpkSecondsAgoTpl, { n: elapsed })
+  return formatTpl((lang as any).ssSpkMinutesAgoTpl, { n: Math.floor(elapsed / 60) })
+})
+</script>
+
+<style lang="scss" scoped>
+.ai-summary {
+  background: #f9fafb;
+  border: 1px solid #e5e7eb;
+  border-radius: 12px;
+  padding: 20px;
+}
+
+.ai-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+.ai-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #111827;
+  margin: 0;
+}
+.ai-refresh-btn {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  color: #6b7280;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  &:hover:not(:disabled) { color: #374151; }
+  &:disabled { opacity: .6; cursor: default; }
+  svg.spinning { animation: spin 1s linear infinite; }
+}
+@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
+
+.ai-bullets {
+  list-style: none; padding: 0; margin: 0;
+  display: flex; flex-direction: column; gap: 4px;
+  font-size: 12px; color: #374151;
+}
+
+.ai-skeleton span {
+  display: inline-block;
+  width: 60%; height: 12px;
+  background: linear-gradient(90deg, #e5e7eb 0%, #f3f4f6 50%, #e5e7eb 100%);
+  background-size: 200% 100%;
+  border-radius: 4px;
+  animation: shimmer 1.4s ease-in-out infinite;
+}
+@keyframes shimmer {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+
+.ai-meta {
+  margin-top: 8px;
+  font-size: 11px;
+  color: #9ca3af;
+}
+</style>

+ 68 - 0
src/views/Student/components/SpeakingClassPanel/NotStartedTip.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="not-started-overlay" @click="$emit('close')">
+    <div class="not-started-card" @click.stop>
+      <div class="icon-wrap">
+        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+             stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <circle cx="12" cy="12" r="10" />
+          <line x1="8" y1="12" x2="16" y2="12" />
+        </svg>
+      </div>
+      <p class="name">{{ studentName }}</p>
+      <p class="msg">{{ lang.ssSpkNotStartedMsg }}</p>
+      <button class="ack-btn" @click="$emit('close')">{{ lang.ssSpkNotStartedAck }}</button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { lang } from '@/main'
+
+defineProps<{ studentName: string }>()
+defineEmits<{ close: [] }>()
+</script>
+
+<style lang="scss" scoped>
+.not-started-overlay {
+  position: fixed; inset: 0;
+  background: rgba(0,0,0,.3);
+  display: flex; align-items: center; justify-content: center;
+  z-index: 50;
+}
+.not-started-card {
+  background: #fff;
+  border-radius: 16px;
+  padding: 24px 32px;
+  max-width: 280px;
+  text-align: center;
+  box-shadow: 0 20px 25px rgba(0,0,0,.1);
+}
+.icon-wrap {
+  width: 40px; height: 40px;
+  border-radius: 50%;
+  background: #f3f4f6;
+  color: #9ca3af;
+  display: flex; align-items: center; justify-content: center;
+  margin: 0 auto 12px;
+}
+.name {
+  font-size: 14px; font-weight: 500; color: #111827;
+  margin: 0 0 4px;
+}
+.msg {
+  font-size: 12px; color: #6b7280;
+  margin: 0 0 20px;
+}
+.ack-btn {
+  width: 100%;
+  padding: 8px 0;
+  font-size: 14px; font-weight: 500;
+  color: #374151;
+  background: #f3f4f6;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background .15s;
+  &:hover { background: #e5e7eb; }
+}
+</style>

+ 99 - 0
src/views/Student/components/SpeakingClassPanel/SortMenu.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="sort-menu">
+    <button
+      class="sort-btn"
+      :class="{ 'active': modelValue !== 'time-asc' }"
+      @click.stop="open = !open"
+      :title="lang.ssSpkSortTimeAsc"
+    >
+      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+           stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+        <path d="M3 6h18M3 12h12M3 18h6" />
+      </svg>
+    </button>
+    <template v-if="open">
+      <div class="sort-overlay" @click="open = false" />
+      <div class="sort-dropdown">
+        <button v-for="opt in options" :key="opt.key"
+                class="sort-option"
+                :class="{ 'active': modelValue === opt.key }"
+                @click="onSelect(opt.key)">
+          {{ opt.label }}
+        </button>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { lang } from '@/main'
+
+export type SortKey = 'time-asc' | 'time-desc' | 'name-asc'
+
+const props = defineProps<{ modelValue: SortKey }>()
+const emit = defineEmits<{ 'update:modelValue': [val: SortKey] }>()
+
+const open = ref(false)
+
+const options = computed(() => [
+  { key: 'time-asc'  as SortKey, label: (lang as any).ssSpkSortTimeAsc },
+  { key: 'time-desc' as SortKey, label: (lang as any).ssSpkSortTimeDesc },
+  { key: 'name-asc'  as SortKey, label: (lang as any).ssSpkSortNameAsc },
+])
+
+function onSelect(key: SortKey) {
+  emit('update:modelValue', key)
+  open.value = false
+}
+</script>
+
+<style lang="scss" scoped>
+.sort-menu { position: relative; }
+
+.sort-btn {
+  width: 28px; height: 28px;
+  display: flex; align-items: center; justify-content: center;
+  border-radius: 6px;
+  background: transparent;
+  border: none;
+  color: #9ca3af;
+  cursor: pointer;
+  transition: background .15s ease;
+  &:hover { background: #f3f4f6; }
+  &.active {
+    color: #ca8a04;
+    background: #fef3c7;
+  }
+}
+
+.sort-overlay {
+  position: fixed; inset: 0;
+  z-index: 30;
+}
+
+.sort-dropdown {
+  position: absolute;
+  right: 0; top: 32px;
+  z-index: 40;
+  background: #fff;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0,0,0,.08);
+  padding: 4px 0;
+  min-width: 128px;
+}
+
+.sort-option {
+  width: 100%;
+  text-align: left;
+  padding: 6px 12px;
+  font-size: 12px;
+  background: transparent;
+  border: none;
+  color: #4b5563;
+  cursor: pointer;
+  &:hover { background: #f9fafb; }
+  &.active { color: #ca8a04; background: #fef3c7; font-weight: 500; }
+}
+</style>

+ 98 - 0
src/views/Student/components/SpeakingClassPanel/StudentGrid.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="grid-4">
+    <div
+      v-for="s in students"
+      :key="s.userId"
+      class="student-card"
+      :class="['status-' + s.status]"
+      @click="$emit('click', s)"
+    >
+      <div class="avatar" :class="'avatar-' + s.status">
+        {{ s.name.charAt(0) }}
+      </div>
+      <span class="name">{{ s.name }}</span>
+      <span v-if="s.status === 'submitted'" class="indicator check">✓</span>
+      <span v-else-if="s.status === 'unsubmitted'" class="indicator dot pulse" />
+      <span v-else class="indicator dash">—</span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { ClassStudentSummary } from './types'
+
+defineProps<{ students: ClassStudentSummary[] }>()
+defineEmits<{ click: [student: ClassStudentSummary] }>()
+</script>
+
+<style lang="scss" scoped>
+.grid-4 {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.student-card {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 12px;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: box-shadow .15s ease;
+  background: #fff;
+  border: 2px solid transparent;
+
+  &:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08); }
+
+  &.status-submitted   { border-color: #facc15; }
+  &.status-unsubmitted { border-color: #fde68a; }
+  &.status-not_started {
+    background: #f3f4f6;
+    border: 2px dashed #9ca3af;
+  }
+}
+
+.avatar {
+  flex-shrink: 0;
+  width: 32px; height: 32px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 600;
+}
+.avatar-submitted   { background: #facc15; color: #111827; }
+.avatar-unsubmitted { background: #fef3c7; color: #92400e; }
+.avatar-not_started { background: #e5e7eb; color: #6b7280; }
+
+.name {
+  flex: 1;
+  font-size: 14px;
+  font-weight: 500;
+  color: #111827;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.status-not_started .name { color: #9ca3af; }
+
+.indicator {
+  flex-shrink: 0;
+  font-size: 12px;
+}
+.indicator.check { color: #ca8a04; font-weight: 600; }
+.indicator.dash  { color: #9ca3af; }
+
+.indicator.dot.pulse {
+  width: 8px; height: 8px;
+  border-radius: 50%;
+  background: #fbbf24;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50%      { opacity: .4; }
+}
+</style>

+ 200 - 0
src/views/Student/components/SpeakingClassPanel/StudentReportModal.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="modal-overlay" @click="$emit('close')">
+    <div class="modal-card" @click.stop>
+      <div class="modal-header">
+        <div>
+          <h3 class="modal-title">
+            {{ student.name }}{{ titleSuffix }}
+          </h3>
+          <p class="modal-subtitle">
+            <span v-if="student.rawStatus !== 'completed'" class="badge-in-progress">
+              {{ (lang as any).ssSpkInProgressBadge }}
+            </span>
+          </p>
+        </div>
+        <button class="close-btn" @click="$emit('close')">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <line x1="18" y1="6" x2="6" y2="18" />
+            <line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div class="modal-body">
+        <div v-if="loading" class="state-block">
+          <div class="loader" />
+        </div>
+
+        <div v-else-if="loadError" class="state-block">
+          <p class="error-msg">{{ (lang as any).ssSpkLoadFailed }}</p>
+          <button class="retry-btn" @click="loadReport">{{ (lang as any).ssSpkRetry }}</button>
+        </div>
+
+        <div v-else-if="reportPending" class="state-block">
+          <p>{{ (lang as any).ssSpkReportGenerating }}</p>
+        </div>
+
+        <div v-else-if="hasNoMessages" class="state-block">
+          <p>{{ (lang as any).ssSpkActiveEmpty }}</p>
+        </div>
+
+        <template v-else>
+          <OverallReport
+            :evaluation="showOverall ? (report?.evaluation ?? null) : null"
+            :role="role"
+            :scoreDisplayMode="'numeric'"
+            @restart="$emit('close')"
+            @complete="$emit('close')"
+          />
+          <DetailedReport
+            v-if="report?.evaluation?.sentenceEvaluations?.length"
+            :sentenceEvaluations="report.evaluation.sentenceEvaluations"
+            :class="{ 'mt-block': showOverall }"
+          />
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { lang } from '@/main'
+import { createDialogueApi } from '@/views/Editor/EnglishSpeaking/services/llmService'
+import OverallReport from '@/views/Editor/EnglishSpeaking/preview/OverallReport.vue'
+import DetailedReport from '@/views/Editor/EnglishSpeaking/preview/DetailedReport.vue'
+import type { PreviewAIRole, DialogueReport } from '@/types/englishSpeaking'
+import type { ClassStudentSummary } from './types'
+
+const props = defineProps<{
+  student: ClassStudentSummary
+  role: PreviewAIRole
+}>()
+defineEmits<{ close: [] }>()
+
+const loading = ref(false)
+const loadError = ref(false)
+const report = ref<DialogueReport | null>(null)
+
+const titleSuffix = computed(() =>
+  props.student.rawStatus === 'completed'
+    ? (lang as any).ssSpkReportTitleSuffix
+    : (lang as any).ssSpkRecordTitleSuffix,
+)
+
+const reportPending = computed(() =>
+  props.student.rawStatus === 'completed'
+  && !!report.value
+  && report.value.status !== 'ready'
+  && report.value.status !== 'failed',
+)
+
+const showOverall = computed(() =>
+  props.student.rawStatus === 'completed'
+  && report.value?.status === 'ready',
+)
+
+const hasNoMessages = computed(() =>
+  !loading.value
+  && !loadError.value
+  && !!report.value
+  && (!report.value.evaluation
+    || (report.value.evaluation.sentenceEvaluations || []).length === 0)
+  && props.student.rawStatus !== 'completed',
+)
+
+async function loadReport() {
+  if (!props.student.sessionId) return
+  loading.value = true
+  loadError.value = false
+  try {
+    const api = createDialogueApi()
+    report.value = await api.getReport(props.student.sessionId)
+  }
+  catch (e) {
+    console.error('[StudentReportModal] getReport failed:', e)
+    loadError.value = true
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+onMounted(loadReport)
+watch(() => props.student.sessionId, loadReport)
+</script>
+
+<style lang="scss" scoped>
+.modal-overlay {
+  position: fixed; inset: 0;
+  background: rgba(0,0,0,.5);
+  display: flex; align-items: center; justify-content: center;
+  z-index: 50;
+}
+.modal-card {
+  background: #fff;
+  border-radius: 12px;
+  width: 100%; max-width: 720px;
+  max-height: 85vh;
+  margin: 0 16px;
+  display: flex; flex-direction: column;
+  overflow: hidden;
+}
+.modal-header {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 16px 24px;
+  border-bottom: 1px solid #e5e7eb;
+}
+.modal-title {
+  font-size: 16px; font-weight: 600; color: #111827; margin: 0;
+}
+.modal-subtitle {
+  font-size: 13px; color: #6b7280; margin: 2px 0 0;
+}
+.badge-in-progress {
+  margin-left: 8px;
+  color: #d97706;
+  font-weight: 500;
+}
+.close-btn {
+  background: transparent; border: none;
+  color: #9ca3af; cursor: pointer;
+  &:hover { color: #4b5563; }
+}
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px 24px;
+}
+
+.state-block {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+  gap: 12px;
+  color: #6b7280;
+  font-size: 13px;
+}
+.error-msg { color: #dc2626; }
+.retry-btn {
+  padding: 6px 16px;
+  background: #f3f4f6;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 13px;
+  &:hover { background: #e5e7eb; }
+}
+.loader {
+  width: 24px; height: 24px;
+  border: 2px solid #e5e7eb;
+  border-top-color: #facc15;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
+
+.mt-block { margin-top: 16px; }
+</style>

+ 252 - 0
src/views/Student/components/SpeakingClassPanel/index.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="speaking-class-panel" :style="cardStyle">
+    <div class="content-card">
+      <div v-if="!props.configId" class="state-banner">
+        {{ lang.ssSpkConfigMissing }}
+      </div>
+
+      <template v-else>
+        <!-- 状态筛选 + 排序 -->
+        <div class="filter-row">
+          <div class="status-filter">
+            <button
+              v-for="f in filterTabs"
+              :key="f.key"
+              :class="{ 'active': statusFilter === f.key }"
+              @click="statusFilter = f.key"
+            >
+              {{ f.label }} {{ f.count }}
+            </button>
+          </div>
+          <SortMenu v-model="sortBy" />
+        </div>
+
+        <!-- 错误条 -->
+        <div v-if="error === 'FETCH_FAILED'" class="error-bar">
+          <span>{{ lang.ssSpkLoadFailed }}</span>
+          <button @click="fetchClassSummary">{{ lang.ssSpkRetry }}</button>
+        </div>
+
+        <!-- 学生网格 -->
+        <div class="grid-scroll">
+          <StudentGrid
+            :students="filteredAndSortedStudents"
+            @click="handleStudentClick"
+          />
+        </div>
+
+        <!-- AI 总结 -->
+        <AISummary
+          :bullets="aiBullets"
+          :loading="aiLoading"
+          :generatedAt="aiGeneratedAt"
+          @refresh="refreshAISummary"
+        />
+      </template>
+    </div>
+
+    <StudentReportModal
+      v-if="reportModalStudent && reportModalStudent.sessionId"
+      :student="reportModalStudent"
+      :role="defaultRole"
+      @close="reportModalStudent = null"
+    />
+
+    <NotStartedTip
+      v-if="notStartedTipStudent"
+      :studentName="notStartedTipStudent.name"
+      @close="notStartedTipStudent = null"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, toRef } from 'vue'
+import { lang } from '@/main'
+import StudentGrid from './StudentGrid.vue'
+import SortMenu, { type SortKey } from './SortMenu.vue'
+import AISummary from './AISummary.vue'
+import StudentReportModal from './StudentReportModal.vue'
+import NotStartedTip from './NotStartedTip.vue'
+import { useClassSummary } from './useClassSummary'
+import type { ClassStudent, ClassStudentSummary, ClassStudentStatus, Locale } from './types'
+import type { PreviewAIRole } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  configId: string
+  slideIndex: number
+  studentArray: ClassStudent[]
+  courseId: string
+  slideWidth: number
+  slideHeight: number
+}>()
+
+// locale 派生:简单按 hostname 决定(与 src/i18n/lang.ts 镜像)
+const locale = ref<Locale>(
+  /cocorobo\.com/i.test(window.location.href) ? 'en'
+  : /cocorobo\.hk/i.test(window.location.href) ? 'hk'
+  : 'zh'
+)
+
+const {
+  summaries, loading, error, fetchClassSummary, scheduleRefetch,
+  aiBullets, aiLoading, aiGeneratedAt, refreshAISummary,
+} = useClassSummary({
+  configId: toRef(props, 'configId'),
+  studentArray: toRef(props, 'studentArray'),
+  locale,
+})
+
+// 暴露给父级(Student/index.vue)用 ref 调用
+defineExpose({ scheduleRefetch, fetchClassSummary })
+
+// 状态筛选 / 排序
+type Filter = 'all' | ClassStudentStatus
+const statusFilter = ref<Filter>('all')
+const sortBy = ref<SortKey>('time-asc')
+
+const counts = computed(() => ({
+  all: summaries.value.length,
+  submitted: summaries.value.filter(s => s.status === 'submitted').length,
+  unsubmitted: summaries.value.filter(s => s.status === 'unsubmitted').length,
+  not_started: summaries.value.filter(s => s.status === 'not_started').length,
+}))
+
+const filterTabs = computed(() => [
+  { key: 'all'         as Filter, label: (lang as any).ssSpkFilterAll,         count: counts.value.all },
+  { key: 'submitted'   as Filter, label: (lang as any).ssSpkFilterSubmitted,   count: counts.value.submitted },
+  { key: 'unsubmitted' as Filter, label: (lang as any).ssSpkFilterUnsubmitted, count: counts.value.unsubmitted },
+  { key: 'not_started' as Filter, label: (lang as any).ssSpkFilterNotStarted,  count: counts.value.not_started },
+])
+
+const filteredAndSortedStudents = computed(() => {
+  let arr = summaries.value
+  if (statusFilter.value !== 'all') arr = arr.filter(s => s.status === statusFilter.value)
+  arr = [...arr]
+  if (sortBy.value === 'name-asc') {
+    arr.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
+  } else {
+    arr.sort((a, b) => {
+      const ta = a.completedAt || a.createdAt || ''
+      const tb = b.completedAt || b.createdAt || ''
+      return sortBy.value === 'time-desc' ? tb.localeCompare(ta) : ta.localeCompare(tb)
+    })
+  }
+  return arr
+})
+
+// 点击学生
+const reportModalStudent = ref<ClassStudentSummary | null>(null)
+const notStartedTipStudent = ref<ClassStudentSummary | null>(null)
+
+function handleStudentClick(s: ClassStudentSummary) {
+  if (s.status === 'not_started' || !s.sessionId) {
+    notStartedTipStudent.value = s
+  } else {
+    reportModalStudent.value = s
+  }
+}
+
+// 占位 role(MVP):后续可从 config 拉
+const defaultRole: PreviewAIRole = {
+  id: 'amy', name: 'Amy', avatar: '😊',
+  identity: 'Friendly Teacher',
+  personality: 'Patient and encouraging',
+  speakingStyle: 'casual', speed: 'normal',
+  isCustom: false, isRecommended: true,
+}
+
+// 卡片整体尺寸样式
+const cardStyle = computed(() => ({
+  width: props.slideWidth + 'px',
+  height: props.slideHeight + 'px',
+}))
+</script>
+
+<style lang="scss" scoped>
+.speaking-class-panel {
+  margin: 0 auto;
+  background: #f9fafb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  box-sizing: border-box;
+}
+
+.content-card {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 8px 32px rgba(0,0,0,.12);
+  width: 100%;
+  max-width: 900px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 32px 48px;
+  overflow: hidden;
+}
+
+.state-banner {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  color: #9ca3af;
+  font-size: 14px;
+}
+
+.filter-row {
+  display: flex; align-items: center; justify-content: space-between;
+  margin-bottom: 16px;
+}
+.status-filter {
+  display: flex; gap: 4px;
+
+  button {
+    padding: 4px 10px;
+    border-radius: 6px;
+    background: transparent;
+    border: none;
+    font-size: 12px;
+    font-weight: 500;
+    color: #6b7280;
+    cursor: pointer;
+    transition: background .15s;
+    &:hover:not(.active) { background: #f3f4f6; }
+    &.active {
+      background: #facc15;
+      color: #111827;
+    }
+  }
+}
+
+.error-bar {
+  background: #fef2f2;
+  color: #b91c1c;
+  padding: 8px 12px;
+  border-radius: 8px;
+  font-size: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+
+  button {
+    padding: 2px 8px;
+    background: #fff;
+    border: 1px solid #fecaca;
+    border-radius: 4px;
+    cursor: pointer;
+    color: #b91c1c;
+    font-size: 12px;
+    &:hover { background: #fee2e2; }
+  }
+}
+
+.grid-scroll {
+  flex: 1;
+  overflow-y: auto;
+  margin-bottom: 16px;
+}
+</style>

+ 31 - 0
src/views/Student/components/SpeakingClassPanel/types.ts

@@ -0,0 +1,31 @@
+import type { ClassSessionSummary } from '@/services/speaking'
+
+/** 班级花名册中的一条学生信息(来自 PPT 后端 selectWorksStudent) */
+export interface ClassStudent {
+  userid: string
+  name: string
+  classid?: string
+}
+
+/** 三态 UI 状态 */
+export type ClassStudentStatus = 'submitted' | 'unsubmitted' | 'not_started'
+
+/** 学生卡片渲染所需的合并数据 */
+export interface ClassStudentSummary {
+  userId: string
+  name: string
+  status: ClassStudentStatus
+  /** 仅 submitted 状态有效 */
+  overallScore: number | null
+  /** 用于点击下钻;未开始为 null */
+  sessionId: string | null
+  /** 用于排序时间正/倒序 */
+  createdAt: string | null
+  completedAt: string | null
+  /** 后端原始 session 状态(用于 modal 内分支) */
+  rawStatus: ClassSessionSummary['status'] | null
+  /** 报告就绪状态 */
+  overallStatus: ClassSessionSummary['overallStatus']
+}
+
+export type Locale = 'zh' | 'en' | 'hk'

+ 173 - 0
src/views/Student/components/SpeakingClassPanel/useClassSummary.ts

@@ -0,0 +1,173 @@
+import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue'
+import { lang } from '@/main'
+import {
+  listSpeakingSessionsByConfig,
+  generateClassSummary,
+  type ClassSessionSummary,
+  type ClassSummaryResponse,
+} from '@/services/speaking'
+import type { ClassStudent, ClassStudentSummary, Locale } from './types'
+
+interface UseClassSummaryOpts {
+  configId: Ref<string>
+  studentArray: Ref<ClassStudent[]>
+  locale: Ref<Locale>
+}
+
+/** 把 i18n 模板里的 {key} 替换成实际值 */
+function formatTpl(tpl: string, vars: Record<string, string | number>): string {
+  return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ''))
+}
+
+function mergeWithRoster(
+  serverSummaries: ClassSessionSummary[],
+  roster: ClassStudent[],
+): ClassStudentSummary[] {
+  const byUser = new Map(serverSummaries.map(s => [s.userId, s]))
+  return roster.map(stu => {
+    const found = byUser.get(stu.userid)
+    if (!found) {
+      return {
+        userId: stu.userid, name: stu.name,
+        status: 'not_started',
+        overallScore: null, sessionId: null,
+        createdAt: null, completedAt: null,
+        rawStatus: null, overallStatus: null,
+      }
+    }
+    const status = found.status === 'completed' ? 'submitted' : 'unsubmitted'
+    return {
+      userId: stu.userid, name: stu.name,
+      status,
+      overallScore: found.overallScore,
+      sessionId: found.sessionId,
+      createdAt: found.createdAt,
+      completedAt: found.completedAt,
+      rawStatus: found.status,
+      overallStatus: found.overallStatus,
+    }
+  })
+}
+
+// 前端 fallback 规则版本(必须与后端 class_summary_rules.py 保持镜像)
+function frontendRuleBullet2(summaries: ClassStudentSummary[]): string {
+  const submitted = summaries.filter(s => s.status === 'submitted')
+  if (submitted.length === 0) return (lang as any).ssSpkWaitingFirst
+  const scores = submitted.map(s => s.overallScore).filter((x): x is number => x != null)
+  if (!scores.length) return (lang as any).ssSpkWaitingFirst
+  const avg = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
+  const max = Math.max(...scores)
+  return formatTpl((lang as any).ssSpkAvgScoreTpl, { avg, max })
+}
+
+function frontendRuleBullet3(summaries: ClassStudentSummary[]): string {
+  const total = summaries.length
+  const submitted = summaries.filter(s => s.status === 'submitted').length
+  const unfinished = total - submitted
+  const rate = total ? Math.round(submitted / total * 100) : 0
+  if (rate === 0)   return (lang as any).ssSpkWaitingStart
+  if (rate < 50)    return formatTpl((lang as any).ssSpkRemindUnfinishedTpl, { n: unfinished })
+  if (rate < 100)   return (lang as any).ssSpkProgressHalf
+  return (lang as any).ssSpkAllDone
+}
+
+export function useClassSummary(opts: UseClassSummaryOpts) {
+  // ─── list 数据 ─────────────────────────────────────────
+  const summaries = ref<ClassStudentSummary[]>([])
+  const loading = ref(false)
+  const error = ref<string | null>(null)
+  let fetchToken = 0
+  let refetchTimer: number | null = null
+
+  async function fetchClassSummary() {
+    if (!opts.configId.value) {
+      error.value = 'CONFIG_MISSING'
+      summaries.value = []
+      return
+    }
+    if (!opts.studentArray.value.length) return
+
+    const token = ++fetchToken
+    loading.value = true
+    try {
+      const userIds = opts.studentArray.value.map(s => s.userid)
+      const data = await listSpeakingSessionsByConfig(opts.configId.value, userIds)
+      if (token !== fetchToken) return
+      summaries.value = mergeWithRoster(data.summaries, opts.studentArray.value)
+      error.value = null
+    } catch (e) {
+      if (token !== fetchToken) return
+      error.value = 'FETCH_FAILED'
+    } finally {
+      if (token === fetchToken) loading.value = false
+    }
+  }
+
+  function scheduleRefetch() {
+    if (refetchTimer) clearTimeout(refetchTimer)
+    refetchTimer = window.setTimeout(fetchClassSummary, 1000)
+  }
+
+  // ─── AI 总结(混合模式) ───────────────────────────────
+  const aiBackendBullets = ref<[string, string, string] | null>(null)
+  const aiLoading = ref(false)
+  const aiGeneratedAt = ref<string | null>(null)
+  let aiToken = 0
+
+  // bullet 1 永远 live(数字派生)
+  const liveBullet1 = computed(() => {
+    const total = summaries.value.length
+    const done = summaries.value.filter(s => s.status === 'submitted').length
+    const rate = total ? Math.round(done / total * 100) : 0
+    return formatTpl((lang as any).ssSpkCompletedCountTpl, { done, total, rate })
+  })
+
+  // 暴露的 3 条 bullet:第 1 条永远 live,2/3 跟随后端或 fallback
+  const aiBullets = computed<[string, string, string]>(() => {
+    const b1 = liveBullet1.value
+    if (aiBackendBullets.value) {
+      return [b1, aiBackendBullets.value[1], aiBackendBullets.value[2]]
+    }
+    return [b1, frontendRuleBullet2(summaries.value), frontendRuleBullet3(summaries.value)]
+  })
+
+  async function refreshAISummary() {
+    if (!opts.configId.value) return
+    if (!opts.studentArray.value.length) return
+
+    const token = ++aiToken
+    aiLoading.value = true
+    try {
+      const userIds = opts.studentArray.value.map(s => s.userid)
+      const data: ClassSummaryResponse = await generateClassSummary(
+        opts.configId.value, userIds, opts.locale.value,
+      )
+      if (token !== aiToken) return
+      aiBackendBullets.value = data.bullets
+      aiGeneratedAt.value = data.generatedAt
+    } catch (e) {
+      if (token !== aiToken) return
+      // 失败保持上次值;若从未成功,aiBullets 自动用前端 fallback
+    } finally {
+      if (token === aiToken) aiLoading.value = false
+    }
+  }
+
+  // ─── 生命周期 ─────────────────────────────────────────
+  onMounted(() => {
+    fetchClassSummary()
+    refreshAISummary()
+  })
+  onUnmounted(() => {
+    if (refetchTimer) clearTimeout(refetchTimer)
+    fetchToken++
+    aiToken++
+  })
+
+  return {
+    // list 数据
+    summaries, loading, error, fetchClassSummary, scheduleRefetch,
+    // AI 总结
+    aiBullets, aiLoading, aiGeneratedAt, refreshAISummary,
+  }
+}

+ 63 - 1
src/views/Student/index.vue

@@ -107,6 +107,17 @@
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
           <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :cid="props.cid" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <SpeakingClassPanel
+            v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
+            ref="speakingPanelRef"
+            :configId="currentSlideConfigId"
+            :slideIndex="slideIndex"
+            :studentArray="studentArray"
+            :courseId="props.courseid"
+            :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale"
+            :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
+            :style="{ margin: '0 auto' }"
+          />
 
 
           <div class="slide-bottom" v-if="!isFullscreen">
@@ -422,6 +433,7 @@ import { WebsocketProvider } from 'y-websocket'
 import { Refresh } from '@icon-park/vue-next'
 import answerTheResult from './components/answerTheResult.vue'
 import choiceQuestionDetailDialog from './components/choiceQuestionDetailDialog.vue'
+import SpeakingClassPanel from './components/SpeakingClassPanel/index.vue'
 import aiChat from './components/aiChat.vue'
 
 // 生成标准 UUID v4 格式(36位,符合 [0-9a-fA-F-] 格式)
@@ -566,6 +578,39 @@ const visibleQA = ref(false)
 const visibleChoice = ref(false)
 const visibleAI = ref(false)
 const choiceQuestionDetailDialogOpenList = ref<number[]>([])
+const speakingPanelRef = ref<InstanceType<typeof SpeakingClassPanel> | null>(null)
+
+provide('notifySpeakingProgress', (status: 'active' | 'completed', payload: { configId: string; sessionId: string }) => {
+  if (props.type !== '2') return // 只有学生客户端发广播
+  sendMessage({
+    type: 'speaking_session_updated',
+    courseid: props.courseid,
+    slideIndex: slideIndex.value,
+    userid: props.userid,
+    status,
+    ...payload,
+  })
+})
+
+provide('recordSpeakingStart', async (sessionId: string) => {
+  // 老师端 (type=1) 与学生端 (type=2) 都允许写作业记录,方便老师自测/学生提交
+  if (!sessionId || !props.courseid || !props.userid) return
+  try {
+    await api.submitWork({
+      uid: props.userid as string,
+      cid: props.courseid as string,
+      stage: '0',
+      task: String(slideIndex.value),
+      tool: '0',
+      atool: '77',
+      content: sessionId,
+      type: '21',
+    })
+  }
+  catch (err) {
+    console.error('[speaking] recordSpeakingStart failed:', err)
+  }
+})
 
 // 提供给子组件使用
 provide('choiceQuestionDetailDialogOpenList', choiceQuestionDetailDialogOpenList)
@@ -998,6 +1043,16 @@ const currentSlideHasIframe = computed(() => {
   return elementList.value.some(element => element.type === ElementTypes.FRAME)
 })
 
+const currentSlideToolType = computed(() => {
+  const frame = elementList.value.find(element => element.type === ElementTypes.FRAME)
+  return Number((frame as any)?.toolType) || 0
+})
+const currentSlideConfigId = computed(() => {
+  if (currentSlideToolType.value !== 77) return ''
+  const frame = elementList.value.find(element => element.type === ElementTypes.FRAME)
+  return (frame as any)?.url || ''
+})
+
 // 检测当前幻灯片是否包含B站视频
 const currentSlideHasBilibiliVideo = computed(() => {
   return elementList.value.some(element => 
@@ -2652,7 +2707,7 @@ const getCourseDetail = async () => {
               let slideContent = ''
               if (slide.elements) {
                 // 提取文本内容(包括普通文本和形状文本)
-                const allTextElements = slide.elements.filter((element: any) => 
+                const allTextElements = slide.elements.filter((element: any) =>
                   element.type === 'text' || (element.type === 'shape' && element.text?.content)
                 )
                 if (allTextElements.length > 0) {
@@ -3326,6 +3381,13 @@ const getMessages = (msgObj: any) => {
       }
     }, 1000)
   }
+
+  // 处理英语口语状态更新 - 学生开始/完成对话时,刷新班级答题面板
+  if (props.type == '1' && msgObj.type === 'speaking_session_updated' && msgObj.courseid === props.courseid) {
+    console.log('收到英语口语状态更新,触发面板刷新')
+    speakingPanelRef.value?.scheduleRefetch?.()
+  }
+
   // 计时器消息 - 学生与老师端实时显示
   if (msgObj.type === 'timer_start' && msgObj.courseid === props.courseid) {
     applyTimerStart(msgObj.payload)

+ 2 - 2
src/views/components/ThumbnailSlide/ThumbnailElement.vue

@@ -18,7 +18,7 @@
 
 <script lang="ts" setup>
 import { computed } from 'vue'
-import { ElementTypes, type PPTElement } from '@/types/slides'
+import { ElementTypes, type PPTElement, type Slide } from '@/types/slides'
 
 import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
 import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
@@ -35,7 +35,7 @@ const props = defineProps<{
   elementInfo: PPTElement
   elementIndex: number
   scale: number
-  slide: number
+  slide: Slide
 }>()
 
 const currentElementComponent = computed<unknown>(() => {

+ 13 - 5
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -37,6 +37,12 @@
           controls
           :style="{ width: '100%', height: '100%', objectFit: 'contain' }"
         ></video>
+        <!-- 英语口语预览(type 77):使用 Vue 组件直接渲染,url 字段即 configId -->
+        <TopicDiscussionPreview
+          v-else-if="Number(elementInfo.toolType) === 77 && !isThumbnail && isVisible"
+          :configId="elementInfo.url"
+          :style="{ width: '100%', height: '100%' }"
+        />
         <!-- B站视频类型(type 75):使用 iframe -->
         <iframe
           v-else-if="elementInfo.toolType === 75 && !isThumbnail && isVisible"
@@ -96,11 +102,12 @@
   </div>
 </template>
   
-<script lang="ts" setup>
-import type { PropType } from 'vue'
-import type { PPTFrameElement } from '@/types/slides'
-import { lang } from '@/main'
-import { ref, watch, nextTick } from 'vue'
+  <script lang="ts" setup>
+import type { PropType } from "vue";
+import type { PPTFrameElement } from "@/types/slides";
+import { lang } from "@/main";
+import TopicDiscussionPreview from "@/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue";
+import { ref, watch, nextTick } from "vue";
 import { computed } from 'vue'
 
 const props = defineProps({
@@ -165,6 +172,7 @@ const getTypeLabel = (type: number): string => {
     74: 'ssVideo',
     75: lang.lang == 'cn' ? 'ssBiliVideo' : 'ssYouTube',
     76: 'ssCreateSpace',
+    77: "ssEnglishSpeakingTool",
   }
   const key = typeMap[type]
   return (key ? lang[key] : lang.ssUnknown) as string

+ 41 - 13
src/views/components/element/FrameElement/index.vue

@@ -12,8 +12,9 @@
       class="rotate-wrapper"
       :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
     >
-      <div 
-        class="element-content" 
+      <div
+        class="element-content"
+        :class="{ 'is-interactive-widget': isSpeakingTool }"
         v-contextmenu="contextmenus"
         @mousedown="$event => handleSelectElement($event)"
         @touchstart="$event => handleSelectElement($event)"
@@ -27,6 +28,12 @@
           controls
           :style="{ width: '100%', height: '100%', objectFit: 'contain' }"
         ></video>
+        <!-- 英语口语预览(type 77):使用 Vue 组件直接渲染,url 字段即 configId -->
+        <TopicDiscussionPreview
+          v-else-if="isSpeakingTool"
+          :configId="elementInfo.url"
+          :style="{ width: '100%', height: '100%' }"
+        />
         <!-- B站视频类型(type 75):使用 iframe -->
         <iframe 
           v-else-if="elementInfo.toolType === 75"
@@ -60,16 +67,21 @@
           allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
         ></iframe>
 
-        <div class="drag-handler top"></div>
-        <div class="drag-handler bottom"></div>
-        <div class="drag-handler left"></div>
-        <div class="drag-handler right"></div>
+        <!-- type 77(英语口语预览)是一个需要内部交互的 Vue 组件:
+             mask / drag-handler 会挡住 wheel 事件导致内部滚不了,
+             所以对 77 不渲染这两层,选中/拖动走外层 .element-content 的 mousedown。 -->
+        <template v-if="!isSpeakingTool">
+          <div class="drag-handler top"></div>
+          <div class="drag-handler bottom"></div>
+          <div class="drag-handler left"></div>
+          <div class="drag-handler right"></div>
 
-        <div class="mask" 
-          v-if="handleElementId !== elementInfo.id"
-          @mousedown="$event => handleSelectElement($event, false)"
-          @touchstart="$event => handleSelectElement($event, false)"
-        ></div>
+          <div class="mask"
+            v-if="handleElementId !== elementInfo.id"
+            @mousedown="$event => handleSelectElement($event, false)"
+            @touchstart="$event => handleSelectElement($event, false)"
+          ></div>
+        </template>
       </div>
     </div>
   </div>
@@ -79,9 +91,11 @@
 import type { PropType } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore } from '@/store'
+import { useSpeakingStore } from '@/store/speaking'
 import type { PPTFrameElement } from '@/types/slides'
+import TopicDiscussionPreview from '@/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue'
 import type { ContextmenuItem } from '@/components/Contextmenu/types'
-import { ref, watch } from 'vue'
+import { computed, ref, watch } from 'vue'
 
 const props = defineProps({
   elementInfo: {
@@ -99,6 +113,12 @@ const props = defineProps({
 })
 
 const { handleElementId } = storeToRefs(useMainStore())
+const speakingStore = useSpeakingStore()
+
+// 英语口语预览工具(FRAME + toolType 77):产品上占满整页、不可拖动、不可缩放/旋转。
+// 这个 flag 在 template 里决定是否渲染 mask/drag-handler + interactive-widget class,
+// 在 handleSelectElement 里决定是否禁止 drag + 是否弹配置面板。
+const isSpeakingTool = computed(() => Number(props.elementInfo.toolType) === 77)
 
 const iframeKey = ref(0)
 
@@ -110,7 +130,11 @@ watch(() => props.elementInfo.url, (newUrl, oldUrl) => {
 
 const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
   e.stopPropagation()
-  props.selectElement(e, props.elementInfo, canMove)
+  // 口语工具:强制 canMove=false 跳过 useSelectElement 的 dragElement 分支,禁止拖动。
+  // 点击后仍弹出/切换配置面板(业务需求)。
+  const finalCanMove = isSpeakingTool.value ? false : canMove
+  props.selectElement(e, props.elementInfo, finalCanMove)
+  if (isSpeakingTool.value) speakingStore.openConfigPanel()
 }
 </script>
 
@@ -126,6 +150,10 @@ const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
   overflow: hidden;
   position: absolute;
 }
+// 英语口语预览(type 77)不可拖动,光标维持默认(让内部按钮/输入的 cursor 能生效)
+.element-content.is-interactive-widget {
+  cursor: default;
+}
 .element-content iframe{
   width: 100%;
   height: 100%;

+ 95 - 2
src/views/lang/cn.json

@@ -256,7 +256,6 @@
   "ssViewQ": "查看题目",
   "ssViewRes": "查看结果",
   "ssParticipants": "参与人数",
-  "ssAccuracy": "正确率",
   "ssCorrectAns": "正确答案",
   "ssUnsubStu": "未提交人员",
   "ssSubStu": "已提交人员",
@@ -684,6 +683,8 @@
   "ssPage": "页面",
   "ssInteractiveWebpage": "交互网页",
   "ssMultimedia": "多媒体",
+  "ssEnglish": "英语",
+  "ssAddEnglish": "添加英语",
   "ssAddTemplatePage": "添加模版页面",
   "ssTitlePage": "标题页",
   "ssImagePage": "图片页",
@@ -757,5 +758,97 @@
   "ssSubStu2":"已提交学生",
   "ssUnsubStu2":"未提交学生",
   "ssViewSubmitStatus2":"点击查看提交情况",
-  "ssEnterQuestion":"请输入你的问题..."
+  "ssEnterQuestion":"请输入你的问题...",
+  "ssEnglishSubject": "英语学科",
+  "ssSpeaking": "口语",
+  "ssListening": "听力",
+  "ssReading": "阅读",
+  "ssWriting": "写作",
+  "ssAddExercise": "添加练习",
+  "ssSmartRecommend": "智能推荐",
+  "ssManualCreate": "手动创建",
+  "ssBackToRecommend": "返回推荐",
+  "ssTopicDiscussion": "话题讨论",
+  "ssDiscussionTopic": "讨论话题",
+  "ssEnterTopic": "输入讨论话题...",
+  "ssLearningGoals": "学习目标",
+  "ssPracticeVocabulary": "练习词汇",
+  "ssPracticeSentences": "练习句型",
+  "ssAddWord": "输入单词...",
+  "ssAddSentence": "输入句型...",
+  "ssBatchPaste": "批量粘贴",
+  "ssPracticeMode": "练习方式",
+  "ssTimeLimit": "限制时长",
+  "ssRoundLimit": "限制轮次",
+  "ssMinute": "分钟",
+  "ssRound": "轮",
+  "ssShowReport": "展示学习报告",
+  "ssAdvancedConfig": "高级配置",
+  "ssAssistance": "辅助功能",
+  "ssShowEnglish": "显示英文",
+  "ssTaskHint": "任务提示",
+  "ssStutterHint": "卡顿提示",
+  "ssEvalDetails": "评估详情",
+  "ssEvalDimensions": "评估维度",
+  "ssAccuracy": "准确度",
+  "ssFluency": "流畅度",
+  "ssCompleteness": "完整度",
+  "ssRhythm": "节奏韵律",
+  "ssScoreMode": "评分方式",
+  "ssLetterScore": "字母评分 (A-D)",
+  "ssNumericScore": "量化评分 (0-100)",
+  "ssCommentOnly": "仅展示评语",
+  "ssApplyConfig": "应用配置",
+  "ssAll": "所有",
+  "ssBasicPractice": "基础跟练",
+  "ssWordReading": "单词跟读",
+  "ssSentenceReading": "句子跟读",
+  "ssArticleReading": "文章朗读",
+  "ssSpeechPractice": "演讲练习",
+  "ssFreeDialogue": "自由对话",
+  "ssCasualChat": "轻松闲聊",
+  "ssMockInterview": "模拟访谈",
+  "ssScenarioTask": "情境任务",
+  "ssDifficultyBeginner": "初级",
+  "ssDifficultyIntermediate": "中级",
+  "ssDifficultyAdvanced": "中高级",
+  "ssRoundsUnit": "轮",
+  "ssMinutesUnit": "分钟",
+  "ssEnglishSpeakingTool": "英语口语",
+  "ssNoRecommendations": "暂无推荐数据",
+  "ssBatchPasteTitle": "批量粘贴",
+  "ssBatchPasteVocab": "词汇(每行一个)",
+  "ssBatchPasteSentences": "句型(每行一个)",
+  "ssFunctionDeveloping": "功能开发中",
+  "ssSpkFilterAll": "全部",
+  "ssSpkFilterSubmitted": "已完成",
+  "ssSpkFilterUnsubmitted": "进行中",
+  "ssSpkFilterNotStarted": "未开始",
+  "ssSpkSortTimeAsc": "时间正序",
+  "ssSpkSortTimeDesc": "时间倒序",
+  "ssSpkSortNameAsc": "姓名 A→Z",
+  "ssSpkAISummary": "AI 总结",
+  "ssSpkRefresh": "刷新",
+  "ssSpkConfigMissing": "该幻灯片未配置英语口语",
+  "ssSpkLoadFailed": "加载班级数据失败",
+  "ssSpkRetry": "重试",
+  "ssSpkReportTitleSuffix": "的学习报告",
+  "ssSpkRecordTitleSuffix": "的对话记录",
+  "ssSpkInProgressBadge": "进行中",
+  "ssSpkNotStartedTitle": "学生尚未开始",
+  "ssSpkNotStartedMsg": "该学生尚未开始练习",
+  "ssSpkNotStartedAck": "知道了",
+  "ssSpkActiveEmpty": "学生刚开始练习,暂无对话内容",
+  "ssSpkReportGenerating": "报告生成中…",
+  "ssSpkAvgScoreTpl": "平均分 {avg} 分,最高 {max} 分",
+  "ssSpkWaitingFirst": "等待第一位学生完成对话",
+  "ssSpkWaitingStart": "等待学生开始练习",
+  "ssSpkRemindUnfinishedTpl": "还有 {n} 位同学未完成,可以提醒一下",
+  "ssSpkProgressHalf": "进度过半,继续保持",
+  "ssSpkAllDone": "全员完成,可适当增加难度",
+  "ssSpkCompletedCountTpl": "已完成 {done}/{total} 人 ({rate}%)",
+  "ssSpkAIGenerating": "AI 生成中…",
+  "ssSpkJustNow": "刚刚生成",
+  "ssSpkSecondsAgoTpl": "{n} 秒前生成",
+  "ssSpkMinutesAgoTpl": "{n} 分钟前生成"
 }

+ 95 - 2
src/views/lang/en.json

@@ -684,6 +684,8 @@
   "ssPage": "Page",
   "ssInteractiveWebpage": "Interactive Webpage",
   "ssMultimedia": "Multimedia",
+  "ssEnglish": "English",
+  "ssAddEnglish": "Add English",
   "ssAddTemplatePage": "Add Template Page",
   "ssTitlePage": "Title Page",
   "ssImagePage": "Image Page",
@@ -752,10 +754,101 @@
   "ssViewKeywordCloud": "Click to view keyword cloud",
   "ssKeyword": "Keyword",
   "ssKeywordCloud": "Word Cloud",
-  "ssGeneratingContent": "Generating Content",
   "ssSubmitStatus":"Submission Status",
   "ssSubStu2":"Submitted Students",
   "ssUnsubStu2":"Unsubmitted Students",
   "ssViewSubmitStatus2":"Click to view submission status",
-  "ssEnterQuestion":"Please enter your question..."
+  "ssEnterQuestion":"Please enter your question...",
+  "ssGeneratingContent": "Generating Content",
+  "ssEnglishSubject": "English",
+  "ssSpeaking": "Speaking",
+  "ssListening": "Listening",
+  "ssReading": "Reading",
+  "ssWriting": "Writing",
+  "ssAddExercise": "Add Exercise",
+  "ssSmartRecommend": "Smart Recommend",
+  "ssManualCreate": "Manual Create",
+  "ssBackToRecommend": "Back to Recommend",
+  "ssTopicDiscussion": "Topic Discussion",
+  "ssDiscussionTopic": "Discussion Topic",
+  "ssEnterTopic": "Enter discussion topic...",
+  "ssLearningGoals": "Learning Goals",
+  "ssPracticeVocabulary": "Vocabulary",
+  "ssPracticeSentences": "Sentences",
+  "ssAddWord": "Enter word...",
+  "ssAddSentence": "Enter sentence...",
+  "ssBatchPaste": "Batch Paste",
+  "ssPracticeMode": "Practice Mode",
+  "ssTimeLimit": "Time Limit",
+  "ssRoundLimit": "Round Limit",
+  "ssMinute": "min",
+  "ssRound": "rounds",
+  "ssShowReport": "Show Learning Report",
+  "ssAdvancedConfig": "Advanced Settings",
+  "ssAssistance": "Assistance",
+  "ssShowEnglish": "Show English",
+  "ssTaskHint": "Task Hint",
+  "ssStutterHint": "Stutter Hint",
+  "ssEvalDetails": "Evaluation Details",
+  "ssEvalDimensions": "Evaluation Dimensions",
+  "ssFluency": "Fluency",
+  "ssCompleteness": "Completeness",
+  "ssRhythm": "Rhythm",
+  "ssScoreMode": "Score Mode",
+  "ssLetterScore": "Letter Score (A-D)",
+  "ssNumericScore": "Numeric Score (0-100)",
+  "ssCommentOnly": "Comment Only",
+  "ssApplyConfig": "Apply Config",
+  "ssAll": "All",
+  "ssBasicPractice": "Basic Practice",
+  "ssWordReading": "Word Reading",
+  "ssSentenceReading": "Sentence Reading",
+  "ssArticleReading": "Article Reading",
+  "ssSpeechPractice": "Speech Practice",
+  "ssFreeDialogue": "Free Dialogue",
+  "ssCasualChat": "Casual Chat",
+  "ssMockInterview": "Mock Interview",
+  "ssScenarioTask": "Scenario Task",
+  "ssDifficultyBeginner": "Beginner",
+  "ssDifficultyIntermediate": "Intermediate",
+  "ssDifficultyAdvanced": "Upper-Intermediate",
+  "ssRoundsUnit": "rounds",
+  "ssMinutesUnit": "min",
+  "ssEnglishSpeakingTool": "English Speaking",
+  "ssNoRecommendations": "No recommendations available",
+  "ssBatchPasteTitle": "Batch Paste",
+  "ssBatchPasteVocab": "Vocabulary (one per line)",
+  "ssBatchPasteSentences": "Sentences (one per line)",
+  "ssFunctionDeveloping": "Under Development",
+  "ssSpkFilterAll": "All",
+  "ssSpkFilterSubmitted": "Done",
+  "ssSpkFilterUnsubmitted": "In Progress",
+  "ssSpkFilterNotStarted": "Not Started",
+  "ssSpkSortTimeAsc": "Time ↑",
+  "ssSpkSortTimeDesc": "Time ↓",
+  "ssSpkSortNameAsc": "Name A→Z",
+  "ssSpkAISummary": "AI Summary",
+  "ssSpkRefresh": "Refresh",
+  "ssSpkConfigMissing": "No speaking config on this slide",
+  "ssSpkLoadFailed": "Failed to load class data",
+  "ssSpkRetry": "Retry",
+  "ssSpkReportTitleSuffix": "'s Report",
+  "ssSpkRecordTitleSuffix": "'s Dialogue",
+  "ssSpkInProgressBadge": "In Progress",
+  "ssSpkNotStartedTitle": "Student Not Started",
+  "ssSpkNotStartedMsg": "This student has not started yet",
+  "ssSpkNotStartedAck": "OK",
+  "ssSpkActiveEmpty": "Just started, no dialogue yet",
+  "ssSpkReportGenerating": "Generating report…",
+  "ssSpkAvgScoreTpl": "Avg {avg}, Top {max}",
+  "ssSpkWaitingFirst": "Waiting for first student",
+  "ssSpkWaitingStart": "Waiting for students to start",
+  "ssSpkRemindUnfinishedTpl": "{n} unfinished — give a nudge",
+  "ssSpkProgressHalf": "Halfway there — keep going",
+  "ssSpkAllDone": "All done — try harder topic",
+  "ssSpkCompletedCountTpl": "Done {done}/{total} ({rate}%)",
+  "ssSpkAIGenerating": "Generating…",
+  "ssSpkJustNow": "Just now",
+  "ssSpkSecondsAgoTpl": "{n}s ago",
+  "ssSpkMinutesAgoTpl": "{n}m ago"
 }

+ 94 - 1
src/views/lang/hk.json

@@ -684,6 +684,8 @@
   "ssPage": "頁面",
   "ssInteractiveWebpage": "交互網頁",
   "ssMultimedia": "Media",
+  "ssEnglish": "英語",
+  "ssAddEnglish": "添加英語",
   "ssAddTemplatePage": "添加模版頁面",
   "ssTitlePage": "標題頁",
   "ssImagePage": "圖片頁",
@@ -757,5 +759,96 @@
   "ssSubStu2":"已提交學生",
   "ssUnsubStu2":"未提交學生",
   "ssViewSubmitStatus2":"點擊查看提交狀態",
-  "ssEnterQuestion":"請輸入你的問題..."
+  "ssEnterQuestion":"請輸入你的問題...",
+  "ssEnglishSubject": "英語學科",
+  "ssSpeaking": "口語",
+  "ssListening": "聽力",
+  "ssReading": "閱讀",
+  "ssWriting": "寫作",
+  "ssAddExercise": "添加練習",
+  "ssSmartRecommend": "智能推薦",
+  "ssManualCreate": "手動創建",
+  "ssBackToRecommend": "返回推薦",
+  "ssTopicDiscussion": "話題討論",
+  "ssDiscussionTopic": "討論話題",
+  "ssEnterTopic": "輸入討論話題...",
+  "ssLearningGoals": "學習目標",
+  "ssPracticeVocabulary": "練習詞彙",
+  "ssPracticeSentences": "練習句型",
+  "ssAddWord": "輸入單詞...",
+  "ssAddSentence": "輸入句型...",
+  "ssBatchPaste": "批量粘貼",
+  "ssPracticeMode": "練習方式",
+  "ssTimeLimit": "限制時長",
+  "ssRoundLimit": "限制輪次",
+  "ssMinute": "分鐘",
+  "ssRound": "輪",
+  "ssShowReport": "展示學習報告",
+  "ssAdvancedConfig": "高級配置",
+  "ssAssistance": "輔助功能",
+  "ssShowEnglish": "顯示英文",
+  "ssTaskHint": "任務提示",
+  "ssStutterHint": "卡頓提示",
+  "ssEvalDetails": "評估詳情",
+  "ssEvalDimensions": "評估維度",
+  "ssFluency": "流暢度",
+  "ssCompleteness": "完整度",
+  "ssRhythm": "節奏韻律",
+  "ssScoreMode": "評分方式",
+  "ssLetterScore": "字母評分 (A-D)",
+  "ssNumericScore": "量化評分 (0-100)",
+  "ssCommentOnly": "僅展示評語",
+  "ssApplyConfig": "應用配置",
+  "ssAll": "所有",
+  "ssBasicPractice": "基礎跟練",
+  "ssWordReading": "單詞跟讀",
+  "ssSentenceReading": "句子跟讀",
+  "ssArticleReading": "文章朗讀",
+  "ssSpeechPractice": "演講練習",
+  "ssFreeDialogue": "自由對話",
+  "ssCasualChat": "輕鬆閒聊",
+  "ssMockInterview": "模擬訪談",
+  "ssScenarioTask": "情境任務",
+  "ssDifficultyBeginner": "初級",
+  "ssDifficultyIntermediate": "中級",
+  "ssDifficultyAdvanced": "中高級",
+  "ssRoundsUnit": "輪",
+  "ssMinutesUnit": "分鐘",
+  "ssEnglishSpeakingTool": "英語口語",
+  "ssNoRecommendations": "暫無推薦數據",
+  "ssBatchPasteTitle": "批量粘貼",
+  "ssBatchPasteVocab": "詞彙(每行一個)",
+  "ssBatchPasteSentences": "句型(每行一個)",
+  "ssFunctionDeveloping": "功能開發中",
+  "ssSpkFilterAll": "全部",
+  "ssSpkFilterSubmitted": "已完成",
+  "ssSpkFilterUnsubmitted": "進行中",
+  "ssSpkFilterNotStarted": "未開始",
+  "ssSpkSortTimeAsc": "時間正序",
+  "ssSpkSortTimeDesc": "時間倒序",
+  "ssSpkSortNameAsc": "姓名 A→Z",
+  "ssSpkAISummary": "AI 總結",
+  "ssSpkRefresh": "刷新",
+  "ssSpkConfigMissing": "該幻燈片未配置英語口語",
+  "ssSpkLoadFailed": "加載班級數據失敗",
+  "ssSpkRetry": "重試",
+  "ssSpkReportTitleSuffix": "的學習報告",
+  "ssSpkRecordTitleSuffix": "的對話記錄",
+  "ssSpkInProgressBadge": "進行中",
+  "ssSpkNotStartedTitle": "學生尚未開始",
+  "ssSpkNotStartedMsg": "該學生尚未開始練習",
+  "ssSpkNotStartedAck": "知道了",
+  "ssSpkActiveEmpty": "學生剛開始練習,暫無對話內容",
+  "ssSpkReportGenerating": "報告生成中…",
+  "ssSpkAvgScoreTpl": "平均分 {avg} 分,最高 {max} 分",
+  "ssSpkWaitingFirst": "等待第一位學生完成對話",
+  "ssSpkWaitingStart": "等待學生開始練習",
+  "ssSpkRemindUnfinishedTpl": "還有 {n} 位同學未完成,可以提醒一下",
+  "ssSpkProgressHalf": "進度過半,繼續保持",
+  "ssSpkAllDone": "全員完成,可適當增加難度",
+  "ssSpkCompletedCountTpl": "已完成 {done}/{total} 人 ({rate}%)",
+  "ssSpkAIGenerating": "AI 生成中…",
+  "ssSpkJustNow": "剛剛生成",
+  "ssSpkSecondsAgoTpl": "{n} 秒前生成",
+  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成"
 }

+ 5 - 0
vite.config.ts

@@ -2,16 +2,21 @@ import { fileURLToPath, URL } from 'node:url'
 
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
+import VueInspector from 'vite-plugin-vue-inspector'
 
 // https://vitejs.dev/config/
 export default defineConfig({
   base: '',
   plugins: [
     vue(),
+    VueInspector(),
   ],
   server: {
     host: '0.0.0.0',
     port: 5173,
+    headers: {
+      'Origin-Agent-Cluster': '?0',
+    },
     proxy: {
       '/api': {
         target: 'https://server.pptist.cn',