#1 for-test

Fermé
lihongjun veut fusionner 137 commits à partir de jack/beta vers jack/feat/english-speaking-for-merge
100 fichiers modifiés avec 32903 ajouts et 1001 suppressions
  1. 6 0
      .env
  2. 8 0
      .env.example
  3. 5 0
      .gitignore
  4. 4 0
      env.d.ts
  5. 11967 0
      package-lock.json
  6. 4 0
      package.json
  7. 26 0
      public/pcm-recorder-worklet.js
  8. 3524 0
      public/webcodecs-worker.js
  9. 126 0
      scripts/test-dialogue-stream-fallback.mjs
  10. 64 0
      scripts/test-speaking-api-config.mjs
  11. 9 6
      src/App.vue
  12. BIN
      src/assets/img/tool_photo.png
  13. BIN
      src/assets/img/tool_vote.png
  14. 5 2
      src/assets/styles/prosemirror.scss
  15. 1156 413
      src/components/CollapsibleToolbar/componets/aiChat.vue
  16. 180 0
      src/components/CollapsibleToolbar/componets/aiWeb.vue
  17. 5 1
      src/components/CollapsibleToolbar/index.vue
  18. 643 128
      src/components/CollapsibleToolbar/index2.vue
  19. 1649 0
      src/components/CollapsibleToolbar/index22.vue
  20. 1 0
      src/components/ColorButton.vue
  21. 143 0
      src/components/Contextmenu2/MenuContent.vue
  22. 80 0
      src/components/Contextmenu2/index.vue
  23. 14 0
      src/components/Contextmenu2/types.ts
  24. 25 23
      src/components/CreateCourseDialog.vue
  25. 27 2
      src/components/FileInput.vue
  26. 6 4
      src/components/Switch.vue
  27. 38 0
      src/components/TextColorButton2.vue
  28. 1 0
      src/configs/hotkey.ts
  29. 1 0
      src/hooks/useCopyAndPasteElement.ts
  30. 2 1
      src/hooks/useCreateElement.ts
  31. 12 1
      src/hooks/useGlobalHotkey.ts
  32. 87 32
      src/hooks/useImport.ts
  33. 46 1
      src/hooks/usePasteEvent.ts
  34. 1 0
      src/i18n/lang.ts
  35. 64 0
      src/plugins/directive/contextmenu2.ts
  36. 2 0
      src/plugins/directive/index.ts
  37. 4 0
      src/plugins/icon.ts
  38. 1 1
      src/services/config.ts
  39. 16 3
      src/services/course.ts
  40. 105 0
      src/services/speaking.ts
  41. 22 1
      src/store/main.ts
  42. 168 0
      src/store/speaking.ts
  43. 5 4
      src/tools/aiChat.ts
  44. 342 0
      src/types/englishSpeaking.ts
  45. 3 1
      src/types/slides.ts
  46. 22 0
      src/utils/clipboard.ts
  47. 162 0
      src/utils/components/messageInstruction.vue
  48. 4 0
      src/utils/confirmDialog.ts
  49. 137 18
      src/utils/prosemirror/schema/marks.ts
  50. 111 52
      src/utils/prosemirror/schema/nodes.ts
  51. 455 0
      src/views/Editor/Canvas/ChildPageDisplay.vue
  52. 11 2
      src/views/Editor/Canvas/EditableElement.vue
  53. 8 1
      src/views/Editor/Canvas/Operate/index.vue
  54. 15 0
      src/views/Editor/Canvas/index.vue
  55. 6 2
      src/views/Editor/CanvasTool/WebpageInput.vue
  56. 245 5
      src/views/Editor/CanvasTool/index2.vue
  57. 172 0
      src/views/Editor/EnglishSpeaking/SpeakingPanel.vue
  58. 239 0
      src/views/Editor/EnglishSpeaking/components/AIGenerationForm.vue
  59. 123 0
      src/views/Editor/EnglishSpeaking/components/CreationModeSwitch.vue
  60. 116 0
      src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue
  61. 145 0
      src/views/Editor/EnglishSpeaking/components/ExerciseTypeGrid.vue
  62. 123 0
      src/views/Editor/EnglishSpeaking/components/RecommendCard.vue
  63. 62 0
      src/views/Editor/EnglishSpeaking/components/UnitInfoCard.vue
  64. 170 0
      src/views/Editor/EnglishSpeaking/composables/useAudioPlayer.ts
  65. 300 0
      src/views/Editor/EnglishSpeaking/composables/useAudioRecorder.ts
  66. 615 0
      src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts
  67. 1082 0
      src/views/Editor/EnglishSpeaking/configs/TopicDiscussionConfig.vue
  68. 113 0
      src/views/Editor/EnglishSpeaking/data/curriculum.json
  69. 52 0
      src/views/Editor/EnglishSpeaking/data/topicDiscussionTasks.json
  70. 63 0
      src/views/Editor/EnglishSpeaking/layers/Layer1Home.vue
  71. 336 0
      src/views/Editor/EnglishSpeaking/layers/Layer2Speaking.vue
  72. 374 0
      src/views/Editor/EnglishSpeaking/preview/DetailedReport.vue
  73. 1936 0
      src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue
  74. 336 0
      src/views/Editor/EnglishSpeaking/preview/OverallReport.vue
  75. 155 0
      src/views/Editor/EnglishSpeaking/preview/StudentPreview.vue
  76. 490 0
      src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue
  77. 678 0
      src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
  78. 320 0
      src/views/Editor/EnglishSpeaking/services/llmService.ts
  79. 18 0
      src/views/Editor/EnglishSpeaking/services/speakingApiConfig.ts
  80. 60 0
      src/views/Editor/EnglishSpeaking/services/speechService.ts
  81. 80 0
      src/views/Editor/Thumbnails/ThumbnailChildPage.vue
  82. 193 45
      src/views/Editor/Thumbnails/index2.vue
  83. 1 0
      src/views/Editor/Toolbar/ElementStylePanel/index.vue
  84. 59 0
      src/views/Editor/Toolbar/common/ElementFlip2.vue
  85. 476 0
      src/views/Editor/Toolbar/common/RichTextBase2.vue
  86. 5 2
      src/views/Editor/index.vue
  87. 11 5
      src/views/Editor/index3.vue
  88. 117 0
      src/views/Student/components/SpeakingClassPanel/AISummary.vue
  89. 68 0
      src/views/Student/components/SpeakingClassPanel/NotStartedTip.vue
  90. 99 0
      src/views/Student/components/SpeakingClassPanel/SortMenu.vue
  91. 98 0
      src/views/Student/components/SpeakingClassPanel/StudentGrid.vue
  92. 200 0
      src/views/Student/components/SpeakingClassPanel/StudentReportModal.vue
  93. 252 0
      src/views/Student/components/SpeakingClassPanel/index.vue
  94. 31 0
      src/views/Student/components/SpeakingClassPanel/types.ts
  95. 173 0
      src/views/Student/components/SpeakingClassPanel/useClassSummary.ts
  96. 21 4
      src/views/Student/components/aiChat.vue
  97. 8 3
      src/views/Student/components/answerTheResult.vue
  98. 627 104
      src/views/Student/components/choiceQuestionDetailDialog.vue
  99. 4 1
      src/views/Student/components/selectUserDialog.vue
  100. 529 133
      src/views/Student/index.vue

+ 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
+}

+ 11967 - 0
package-lock.json

@@ -0,0 +1,11967 @@
+{
+  "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",
+        "jszip": "^3.10.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.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "license": "(MIT OR GPL-3.0-or-later)",
+      "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,
+      "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.npmjs.org/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"
+      }
+    }
+  }
+}

+ 4 - 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",
@@ -28,6 +30,7 @@
     "hfmath": "^0.0.2",
     "html-to-image": "^1.11.13",
     "html2canvas": "^1.4.1",
+    "jszip": "^3.10.1",
     "katex": "^0.16.22",
     "lodash": "^4.17.21",
     "markdown-it": "^14.1.1",
@@ -88,6 +91,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)

+ 3524 - 0
public/webcodecs-worker.js

@@ -0,0 +1,3524 @@
+"use strict";
+var EncoderWorkerGlobal = (() => {
+  var __create = Object.create;
+  var __defProp = Object.defineProperty;
+  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+  var __getOwnPropNames = Object.getOwnPropertyNames;
+  var __getProtoOf = Object.getPrototypeOf;
+  var __hasOwnProp = Object.prototype.hasOwnProperty;
+  var __commonJS = (cb, mod) => function __require() {
+    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+  };
+  var __copyProps = (to, from, except, desc) => {
+    if (from && typeof from === "object" || typeof from === "function") {
+      for (let key of __getOwnPropNames(from))
+        if (!__hasOwnProp.call(to, key) && key !== except)
+          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+    }
+    return to;
+  };
+  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
+    // If the importer is in node compatibility mode or this is not an ESM
+    // file that has been converted to a CommonJS file using a Babel-
+    // compatible transform (i.e. "__esModule" has not been set), then set
+    // "default" to the CommonJS "module.exports" for node compatibility.
+    isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
+    mod
+  ));
+
+  // node_modules/webm-muxer/build/webm-muxer.js
+  var require_webm_muxer = __commonJS({
+    "node_modules/webm-muxer/build/webm-muxer.js"(exports, module) {
+      "use strict";
+      var WebMMuxer2 = (() => {
+        var __defProp3 = Object.defineProperty;
+        var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
+        var __getOwnPropNames2 = Object.getOwnPropertyNames;
+        var __hasOwnProp3 = Object.prototype.hasOwnProperty;
+        var __pow2 = Math.pow;
+        var __export = (target, all) => {
+          for (var name in all)
+            __defProp3(target, name, { get: all[name], enumerable: true });
+        };
+        var __copyProps2 = (to, from, except, desc) => {
+          if (from && typeof from === "object" || typeof from === "function") {
+            for (let key of __getOwnPropNames2(from))
+              if (!__hasOwnProp3.call(to, key) && key !== except)
+                __defProp3(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
+          }
+          return to;
+        };
+        var __toCommonJS = (mod) => __copyProps2(__defProp3({}, "__esModule", { value: true }), mod);
+        var __accessCheck2 = (obj, member, msg) => {
+          if (!member.has(obj))
+            throw TypeError("Cannot " + msg);
+        };
+        var __privateGet2 = (obj, member, getter) => {
+          __accessCheck2(obj, member, "read from private field");
+          return getter ? getter.call(obj) : member.get(obj);
+        };
+        var __privateAdd2 = (obj, member, value) => {
+          if (member.has(obj))
+            throw TypeError("Cannot add the same private member more than once");
+          member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
+        };
+        var __privateSet2 = (obj, member, value, setter) => {
+          __accessCheck2(obj, member, "write to private field");
+          setter ? setter.call(obj, value) : member.set(obj, value);
+          return value;
+        };
+        var __privateMethod2 = (obj, member, method) => {
+          __accessCheck2(obj, member, "access private method");
+          return method;
+        };
+        var main_exports = {};
+        __export(main_exports, {
+          default: () => main_default
+        });
+        var EBMLFloat32 = class {
+          constructor(value) {
+            this.value = value;
+          }
+        };
+        var EBMLFloat64 = class {
+          constructor(value) {
+            this.value = value;
+          }
+        };
+        var WriteTarget = class {
+          constructor() {
+            this.pos = 0;
+            this.helper = new Uint8Array(8);
+            this.helperView = new DataView(this.helper.buffer);
+            this.offsets = /* @__PURE__ */ new WeakMap();
+            this.dataOffsets = /* @__PURE__ */ new WeakMap();
+          }
+          writeFloat32(value) {
+            this.helperView.setFloat32(0, value, false);
+            this.write(this.helper.subarray(0, 4));
+          }
+          writeFloat64(value) {
+            this.helperView.setFloat64(0, value, false);
+            this.write(this.helper);
+          }
+          writeUnsignedInt(value, width = measureUnsignedInt(value)) {
+            let pos = 0;
+            switch (width) {
+              case 6:
+                this.helperView.setUint8(pos++, value / __pow2(2, 40) | 0);
+              case 5:
+                this.helperView.setUint8(pos++, value / __pow2(2, 32) | 0);
+              case 4:
+                this.helperView.setUint8(pos++, value >> 24);
+              case 3:
+                this.helperView.setUint8(pos++, value >> 16);
+              case 2:
+                this.helperView.setUint8(pos++, value >> 8);
+              case 1:
+                this.helperView.setUint8(pos++, value);
+                break;
+              default:
+                throw new Error("Bad UINT size " + width);
+            }
+            this.write(this.helper.subarray(0, pos));
+          }
+          writeEBMLVarInt(value, width = measureEBMLVarInt(value)) {
+            let pos = 0;
+            switch (width) {
+              case 1:
+                this.helperView.setUint8(pos++, 1 << 7 | value);
+                break;
+              case 2:
+                this.helperView.setUint8(pos++, 1 << 6 | value >> 8);
+                this.helperView.setUint8(pos++, value);
+                break;
+              case 3:
+                this.helperView.setUint8(pos++, 1 << 5 | value >> 16);
+                this.helperView.setUint8(pos++, value >> 8);
+                this.helperView.setUint8(pos++, value);
+                break;
+              case 4:
+                this.helperView.setUint8(pos++, 1 << 4 | value >> 24);
+                this.helperView.setUint8(pos++, value >> 16);
+                this.helperView.setUint8(pos++, value >> 8);
+                this.helperView.setUint8(pos++, value);
+                break;
+              case 5:
+                this.helperView.setUint8(pos++, 1 << 3 | value / __pow2(2, 32) & 7);
+                this.helperView.setUint8(pos++, value >> 24);
+                this.helperView.setUint8(pos++, value >> 16);
+                this.helperView.setUint8(pos++, value >> 8);
+                this.helperView.setUint8(pos++, value);
+                break;
+              case 6:
+                this.helperView.setUint8(pos++, 1 << 2 | value / __pow2(2, 40) & 3);
+                this.helperView.setUint8(pos++, value / __pow2(2, 32) | 0);
+                this.helperView.setUint8(pos++, value >> 24);
+                this.helperView.setUint8(pos++, value >> 16);
+                this.helperView.setUint8(pos++, value >> 8);
+                this.helperView.setUint8(pos++, value);
+                break;
+              default:
+                throw new Error("Bad EBML VINT size " + width);
+            }
+            this.write(this.helper.subarray(0, pos));
+          }
+          writeString(str) {
+            this.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
+          }
+          writeEBML(data) {
+            var _a, _b;
+            if (data instanceof Uint8Array) {
+              this.write(data);
+            } else if (Array.isArray(data)) {
+              for (let elem of data) {
+                this.writeEBML(elem);
+              }
+            } else {
+              this.offsets.set(data, this.pos);
+              this.writeUnsignedInt(data.id);
+              if (Array.isArray(data.data)) {
+                let sizePos = this.pos;
+                let sizeSize = (_a = data.size) != null ? _a : 4;
+                this.seek(this.pos + sizeSize);
+                let startPos = this.pos;
+                this.dataOffsets.set(data, startPos);
+                this.writeEBML(data.data);
+                let size = this.pos - startPos;
+                let endPos = this.pos;
+                this.seek(sizePos);
+                this.writeEBMLVarInt(size, sizeSize);
+                this.seek(endPos);
+              } else if (typeof data.data === "number") {
+                let size = (_b = data.size) != null ? _b : measureUnsignedInt(data.data);
+                this.writeEBMLVarInt(size);
+                this.writeUnsignedInt(data.data, size);
+              } else if (typeof data.data === "string") {
+                this.writeEBMLVarInt(data.data.length);
+                this.writeString(data.data);
+              } else if (data.data instanceof Uint8Array) {
+                this.writeEBMLVarInt(data.data.byteLength, data.size);
+                this.write(data.data);
+              } else if (data.data instanceof EBMLFloat32) {
+                this.writeEBMLVarInt(4);
+                this.writeFloat32(data.data.value);
+              } else if (data.data instanceof EBMLFloat64) {
+                this.writeEBMLVarInt(8);
+                this.writeFloat64(data.data.value);
+              }
+            }
+          }
+        };
+        var measureUnsignedInt = (value) => {
+          if (value < 1 << 8) {
+            return 1;
+          } else if (value < 1 << 16) {
+            return 2;
+          } else if (value < 1 << 24) {
+            return 3;
+          } else if (value < __pow2(2, 32)) {
+            return 4;
+          } else if (value < __pow2(2, 40)) {
+            return 5;
+          } else {
+            return 6;
+          }
+        };
+        var measureEBMLVarInt = (value) => {
+          if (value < (1 << 7) - 1) {
+            return 1;
+          } else if (value < (1 << 14) - 1) {
+            return 2;
+          } else if (value < (1 << 21) - 1) {
+            return 3;
+          } else if (value < (1 << 28) - 1) {
+            return 4;
+          } else if (value < __pow2(2, 35) - 1) {
+            return 5;
+          } else if (value < __pow2(2, 42) - 1) {
+            return 6;
+          } else {
+            throw new Error("EBML VINT size not supported " + value);
+          }
+        };
+        var ArrayBufferWriteTarget = class extends WriteTarget {
+          constructor() {
+            super();
+            this.buffer = new ArrayBuffer(__pow2(2, 16));
+            this.bytes = new Uint8Array(this.buffer);
+          }
+          ensureSize(size) {
+            let newLength = this.buffer.byteLength;
+            while (newLength < size)
+              newLength *= 2;
+            if (newLength === this.buffer.byteLength)
+              return;
+            let newBuffer = new ArrayBuffer(newLength);
+            let newBytes = new Uint8Array(newBuffer);
+            newBytes.set(this.bytes, 0);
+            this.buffer = newBuffer;
+            this.bytes = newBytes;
+          }
+          write(data) {
+            this.ensureSize(this.pos + data.byteLength);
+            this.bytes.set(data, this.pos);
+            this.pos += data.byteLength;
+          }
+          seek(newPos) {
+            this.pos = newPos;
+          }
+          finalize() {
+            this.ensureSize(this.pos);
+            return this.buffer.slice(0, this.pos);
+          }
+        };
+        var FILE_CHUNK_SIZE = __pow2(2, 24);
+        var MAX_CHUNKS_AT_ONCE2 = 2;
+        var FileSystemWritableFileStreamWriteTarget = class extends WriteTarget {
+          constructor(stream) {
+            super();
+            this.chunks = [];
+            this.stream = stream;
+          }
+          write(data) {
+            this.writeDataIntoChunks(data, this.pos);
+            this.flushChunks();
+            this.pos += data.byteLength;
+          }
+          writeDataIntoChunks(data, position) {
+            let chunkIndex = this.chunks.findIndex((x) => x.start <= position && position < x.start + FILE_CHUNK_SIZE);
+            if (chunkIndex === -1)
+              chunkIndex = this.createChunk(position);
+            let chunk = this.chunks[chunkIndex];
+            let relativePosition = position - chunk.start;
+            let toWrite = data.subarray(0, Math.min(FILE_CHUNK_SIZE - relativePosition, data.byteLength));
+            chunk.data.set(toWrite, relativePosition);
+            let section = {
+              start: relativePosition,
+              end: relativePosition + toWrite.byteLength
+            };
+            insertSectionIntoFileChunk(chunk, section);
+            if (chunk.written[0].start === 0 && chunk.written[0].end === FILE_CHUNK_SIZE) {
+              chunk.shouldFlush = true;
+            }
+            if (this.chunks.length > MAX_CHUNKS_AT_ONCE2) {
+              for (let i = 0; i < this.chunks.length - 1; i++) {
+                this.chunks[i].shouldFlush = true;
+              }
+              this.flushChunks();
+            }
+            if (toWrite.byteLength < data.byteLength) {
+              this.writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
+            }
+          }
+          createChunk(includesPosition) {
+            let start = Math.floor(includesPosition / FILE_CHUNK_SIZE) * FILE_CHUNK_SIZE;
+            let chunk = {
+              start,
+              data: new Uint8Array(FILE_CHUNK_SIZE),
+              written: [],
+              shouldFlush: false
+            };
+            this.chunks.push(chunk);
+            this.chunks.sort((a, b) => a.start - b.start);
+            return this.chunks.indexOf(chunk);
+          }
+          flushChunks(force = false) {
+            for (let i = 0; i < this.chunks.length; i++) {
+              let chunk = this.chunks[i];
+              if (!chunk.shouldFlush && !force)
+                continue;
+              for (let section of chunk.written) {
+                this.stream.write({
+                  type: "write",
+                  data: chunk.data.subarray(section.start, section.end),
+                  position: chunk.start + section.start
+                });
+              }
+              this.chunks.splice(i--, 1);
+            }
+          }
+          seek(newPos) {
+            this.pos = newPos;
+          }
+          finalize() {
+            this.flushChunks(true);
+          }
+        };
+        var insertSectionIntoFileChunk = (chunk, section) => {
+          let low = 0;
+          let high = chunk.written.length - 1;
+          let index = -1;
+          while (low <= high) {
+            let mid = Math.floor(low + (high - low + 1) / 2);
+            if (chunk.written[mid].start <= section.start) {
+              low = mid + 1;
+              index = mid;
+            } else {
+              high = mid - 1;
+            }
+          }
+          chunk.written.splice(index + 1, 0, section);
+          if (index === -1 || chunk.written[index].end < section.start)
+            index++;
+          while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
+            chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
+            chunk.written.splice(index + 1, 1);
+          }
+        };
+        var VIDEO_TRACK_NUMBER = 1;
+        var AUDIO_TRACK_NUMBER = 2;
+        var VIDEO_TRACK_TYPE = 1;
+        var AUDIO_TRACK_TYPE = 2;
+        var MAX_CHUNK_LENGTH_MS = __pow2(2, 15);
+        var CODEC_PRIVATE_MAX_SIZE = __pow2(2, 12);
+        var APP_NAME = "https://github.com/Vanilagy/webm-muxer";
+        var SEGMENT_SIZE_BYTES = 6;
+        var CLUSTER_SIZE_BYTES = 5;
+        var _target4, _options2, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _lastVideoTimestamp, _lastAudioTimestamp, _colorSpace, _finalized2, _validateOptions2, validateOptions_fn2, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _createInternalChunk, createInternalChunk_fn, _writeSimpleBlock, writeSimpleBlock_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized2, ensureNotFinalized_fn2;
+        var WebMMuxer3 = class {
+          constructor(options) {
+            __privateAdd2(this, _validateOptions2);
+            __privateAdd2(this, _createFileHeader);
+            __privateAdd2(this, _writeEBMLHeader);
+            __privateAdd2(this, _createSeekHead);
+            __privateAdd2(this, _createSegmentInfo);
+            __privateAdd2(this, _createTracks);
+            __privateAdd2(this, _createSegment);
+            __privateAdd2(this, _createCues);
+            __privateAdd2(this, _segmentDataOffset);
+            __privateAdd2(this, _writeVideoDecoderConfig);
+            __privateAdd2(this, _fixVP9ColorSpace);
+            __privateAdd2(this, _createInternalChunk);
+            __privateAdd2(this, _writeSimpleBlock);
+            __privateAdd2(this, _writeCodecPrivate);
+            __privateAdd2(this, _createNewCluster);
+            __privateAdd2(this, _finalizeCurrentCluster);
+            __privateAdd2(this, _ensureNotFinalized2);
+            __privateAdd2(this, _target4, void 0);
+            __privateAdd2(this, _options2, void 0);
+            __privateAdd2(this, _segment, void 0);
+            __privateAdd2(this, _segmentInfo, void 0);
+            __privateAdd2(this, _seekHead, void 0);
+            __privateAdd2(this, _tracksElement, void 0);
+            __privateAdd2(this, _segmentDuration, void 0);
+            __privateAdd2(this, _colourElement, void 0);
+            __privateAdd2(this, _videoCodecPrivate, void 0);
+            __privateAdd2(this, _audioCodecPrivate, void 0);
+            __privateAdd2(this, _cues, void 0);
+            __privateAdd2(this, _currentCluster, void 0);
+            __privateAdd2(this, _currentClusterTimestamp, void 0);
+            __privateAdd2(this, _duration, 0);
+            __privateAdd2(this, _videoChunkQueue, []);
+            __privateAdd2(this, _audioChunkQueue, []);
+            __privateAdd2(this, _lastVideoTimestamp, 0);
+            __privateAdd2(this, _lastAudioTimestamp, 0);
+            __privateAdd2(this, _colorSpace, void 0);
+            __privateAdd2(this, _finalized2, false);
+            __privateMethod2(this, _validateOptions2, validateOptions_fn2).call(this, options);
+            __privateSet2(this, _options2, options);
+            if (options.target === "buffer") {
+              __privateSet2(this, _target4, new ArrayBufferWriteTarget());
+            } else {
+              __privateSet2(this, _target4, new FileSystemWritableFileStreamWriteTarget(options.target));
+            }
+            __privateMethod2(this, _createFileHeader, createFileHeader_fn).call(this);
+          }
+          addVideoChunk(chunk, meta, timestamp) {
+            let data = new Uint8Array(chunk.byteLength);
+            chunk.copyTo(data);
+            this.addVideoChunkRaw(data, chunk.type, timestamp != null ? timestamp : chunk.timestamp, meta);
+          }
+          addVideoChunkRaw(data, type, timestamp, meta) {
+            __privateMethod2(this, _ensureNotFinalized2, ensureNotFinalized_fn2).call(this);
+            if (!__privateGet2(this, _options2).video)
+              throw new Error("No video track declared.");
+            if (meta)
+              __privateMethod2(this, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn).call(this, meta);
+            let internalChunk = __privateMethod2(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER);
+            if (__privateGet2(this, _options2).video.codec === "V_VP9")
+              __privateMethod2(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, internalChunk);
+            __privateSet2(this, _lastVideoTimestamp, internalChunk.timestamp);
+            while (__privateGet2(this, _audioChunkQueue).length > 0 && __privateGet2(this, _audioChunkQueue)[0].timestamp <= internalChunk.timestamp) {
+              let audioChunk = __privateGet2(this, _audioChunkQueue).shift();
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, audioChunk);
+            }
+            if (!__privateGet2(this, _options2).audio || internalChunk.timestamp <= __privateGet2(this, _lastAudioTimestamp)) {
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk);
+            } else {
+              __privateGet2(this, _videoChunkQueue).push(internalChunk);
+            }
+          }
+          addAudioChunk(chunk, meta, timestamp) {
+            let data = new Uint8Array(chunk.byteLength);
+            chunk.copyTo(data);
+            this.addAudioChunkRaw(data, chunk.type, timestamp != null ? timestamp : chunk.timestamp, meta);
+          }
+          addAudioChunkRaw(data, type, timestamp, meta) {
+            __privateMethod2(this, _ensureNotFinalized2, ensureNotFinalized_fn2).call(this);
+            if (!__privateGet2(this, _options2).audio)
+              throw new Error("No audio track declared.");
+            let internalChunk = __privateMethod2(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER);
+            __privateSet2(this, _lastAudioTimestamp, internalChunk.timestamp);
+            while (__privateGet2(this, _videoChunkQueue).length > 0 && __privateGet2(this, _videoChunkQueue)[0].timestamp <= internalChunk.timestamp) {
+              let videoChunk = __privateGet2(this, _videoChunkQueue).shift();
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, videoChunk);
+            }
+            if (!__privateGet2(this, _options2).video || internalChunk.timestamp <= __privateGet2(this, _lastVideoTimestamp)) {
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk);
+            } else {
+              __privateGet2(this, _audioChunkQueue).push(internalChunk);
+            }
+            if (meta == null ? void 0 : meta.decoderConfig) {
+              __privateMethod2(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet2(this, _audioCodecPrivate), meta.decoderConfig.description);
+            }
+          }
+          finalize() {
+            while (__privateGet2(this, _videoChunkQueue).length > 0)
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet2(this, _videoChunkQueue).shift());
+            while (__privateGet2(this, _audioChunkQueue).length > 0)
+              __privateMethod2(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet2(this, _audioChunkQueue).shift());
+            __privateMethod2(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this);
+            __privateGet2(this, _target4).writeEBML(__privateGet2(this, _cues));
+            let endPos = __privateGet2(this, _target4).pos;
+            let segmentSize = __privateGet2(this, _target4).pos - __privateGet2(this, _segmentDataOffset, segmentDataOffset_get);
+            __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(__privateGet2(this, _segment)) + 4);
+            __privateGet2(this, _target4).writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
+            __privateGet2(this, _segmentDuration).data = new EBMLFloat64(__privateGet2(this, _duration));
+            __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(__privateGet2(this, _segmentDuration)));
+            __privateGet2(this, _target4).writeEBML(__privateGet2(this, _segmentDuration));
+            __privateGet2(this, _seekHead).data[0].data[1].data = __privateGet2(this, _target4).offsets.get(__privateGet2(this, _cues)) - __privateGet2(this, _segmentDataOffset, segmentDataOffset_get);
+            __privateGet2(this, _seekHead).data[1].data[1].data = __privateGet2(this, _target4).offsets.get(__privateGet2(this, _segmentInfo)) - __privateGet2(this, _segmentDataOffset, segmentDataOffset_get);
+            __privateGet2(this, _seekHead).data[2].data[1].data = __privateGet2(this, _target4).offsets.get(__privateGet2(this, _tracksElement)) - __privateGet2(this, _segmentDataOffset, segmentDataOffset_get);
+            __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(__privateGet2(this, _seekHead)));
+            __privateGet2(this, _target4).writeEBML(__privateGet2(this, _seekHead));
+            __privateGet2(this, _target4).seek(endPos);
+            __privateSet2(this, _finalized2, true);
+            if (__privateGet2(this, _target4) instanceof ArrayBufferWriteTarget) {
+              return __privateGet2(this, _target4).finalize();
+            } else if (__privateGet2(this, _target4) instanceof FileSystemWritableFileStreamWriteTarget) {
+              __privateGet2(this, _target4).finalize();
+            }
+            return null;
+          }
+        };
+        _target4 = /* @__PURE__ */ new WeakMap();
+        _options2 = /* @__PURE__ */ new WeakMap();
+        _segment = /* @__PURE__ */ new WeakMap();
+        _segmentInfo = /* @__PURE__ */ new WeakMap();
+        _seekHead = /* @__PURE__ */ new WeakMap();
+        _tracksElement = /* @__PURE__ */ new WeakMap();
+        _segmentDuration = /* @__PURE__ */ new WeakMap();
+        _colourElement = /* @__PURE__ */ new WeakMap();
+        _videoCodecPrivate = /* @__PURE__ */ new WeakMap();
+        _audioCodecPrivate = /* @__PURE__ */ new WeakMap();
+        _cues = /* @__PURE__ */ new WeakMap();
+        _currentCluster = /* @__PURE__ */ new WeakMap();
+        _currentClusterTimestamp = /* @__PURE__ */ new WeakMap();
+        _duration = /* @__PURE__ */ new WeakMap();
+        _videoChunkQueue = /* @__PURE__ */ new WeakMap();
+        _audioChunkQueue = /* @__PURE__ */ new WeakMap();
+        _lastVideoTimestamp = /* @__PURE__ */ new WeakMap();
+        _lastAudioTimestamp = /* @__PURE__ */ new WeakMap();
+        _colorSpace = /* @__PURE__ */ new WeakMap();
+        _finalized2 = /* @__PURE__ */ new WeakMap();
+        _validateOptions2 = /* @__PURE__ */ new WeakSet();
+        validateOptions_fn2 = function(options) {
+          if (options.type && options.type !== "webm" && options.type !== "matroska") {
+            throw new Error(`Invalid type: ${options.type}`);
+          }
+        };
+        _createFileHeader = /* @__PURE__ */ new WeakSet();
+        createFileHeader_fn = function() {
+          __privateMethod2(this, _writeEBMLHeader, writeEBMLHeader_fn).call(this);
+          __privateMethod2(this, _createSeekHead, createSeekHead_fn).call(this);
+          __privateMethod2(this, _createSegmentInfo, createSegmentInfo_fn).call(this);
+          __privateMethod2(this, _createTracks, createTracks_fn).call(this);
+          __privateMethod2(this, _createSegment, createSegment_fn).call(this);
+          __privateMethod2(this, _createCues, createCues_fn).call(this);
+        };
+        _writeEBMLHeader = /* @__PURE__ */ new WeakSet();
+        writeEBMLHeader_fn = function() {
+          var _a;
+          let ebmlHeader = { id: 440786851, data: [
+            { id: 17030, data: 1 },
+            { id: 17143, data: 1 },
+            { id: 17138, data: 4 },
+            { id: 17139, data: 8 },
+            { id: 17026, data: (_a = __privateGet2(this, _options2).type) != null ? _a : "webm" },
+            { id: 17031, data: 2 },
+            { id: 17029, data: 2 }
+          ] };
+          __privateGet2(this, _target4).writeEBML(ebmlHeader);
+        };
+        _createSeekHead = /* @__PURE__ */ new WeakSet();
+        createSeekHead_fn = function() {
+          const kaxCues = new Uint8Array([28, 83, 187, 107]);
+          const kaxInfo = new Uint8Array([21, 73, 169, 102]);
+          const kaxTracks = new Uint8Array([22, 84, 174, 107]);
+          let seekHead = { id: 290298740, data: [
+            { id: 19899, data: [
+              { id: 21419, data: kaxCues },
+              { id: 21420, size: 5, data: 0 }
+            ] },
+            { id: 19899, data: [
+              { id: 21419, data: kaxInfo },
+              { id: 21420, size: 5, data: 0 }
+            ] },
+            { id: 19899, data: [
+              { id: 21419, data: kaxTracks },
+              { id: 21420, size: 5, data: 0 }
+            ] }
+          ] };
+          __privateSet2(this, _seekHead, seekHead);
+        };
+        _createSegmentInfo = /* @__PURE__ */ new WeakSet();
+        createSegmentInfo_fn = function() {
+          let segmentDuration = { id: 17545, data: new EBMLFloat64(0) };
+          __privateSet2(this, _segmentDuration, segmentDuration);
+          let segmentInfo = { id: 357149030, data: [
+            { id: 2807729, data: 1e6 },
+            { id: 19840, data: APP_NAME },
+            { id: 22337, data: APP_NAME },
+            segmentDuration
+          ] };
+          __privateSet2(this, _segmentInfo, segmentInfo);
+        };
+        _createTracks = /* @__PURE__ */ new WeakSet();
+        createTracks_fn = function() {
+          let tracksElement = { id: 374648427, data: [] };
+          __privateSet2(this, _tracksElement, tracksElement);
+          if (__privateGet2(this, _options2).video) {
+            __privateSet2(this, _videoCodecPrivate, { id: 236, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) });
+            let colourElement = { id: 21936, data: [
+              { id: 21937, data: 2 },
+              { id: 21946, data: 2 },
+              { id: 21947, data: 2 },
+              { id: 21945, data: 0 }
+            ] };
+            __privateSet2(this, _colourElement, colourElement);
+            tracksElement.data.push({ id: 174, data: [
+              { id: 215, data: VIDEO_TRACK_NUMBER },
+              { id: 29637, data: VIDEO_TRACK_NUMBER },
+              { id: 131, data: VIDEO_TRACK_TYPE },
+              { id: 134, data: __privateGet2(this, _options2).video.codec },
+              __privateGet2(this, _videoCodecPrivate),
+              __privateGet2(this, _options2).video.frameRate ? { id: 2352003, data: 1e9 / __privateGet2(this, _options2).video.frameRate } : null,
+              { id: 224, data: [
+                { id: 176, data: __privateGet2(this, _options2).video.width },
+                { id: 186, data: __privateGet2(this, _options2).video.height },
+                colourElement
+              ] }
+            ].filter(Boolean) });
+          }
+          if (__privateGet2(this, _options2).audio) {
+            __privateSet2(this, _audioCodecPrivate, { id: 236, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) });
+            tracksElement.data.push({ id: 174, data: [
+              { id: 215, data: AUDIO_TRACK_NUMBER },
+              { id: 29637, data: AUDIO_TRACK_NUMBER },
+              { id: 131, data: AUDIO_TRACK_TYPE },
+              { id: 134, data: __privateGet2(this, _options2).audio.codec },
+              __privateGet2(this, _audioCodecPrivate),
+              { id: 225, data: [
+                { id: 181, data: new EBMLFloat32(__privateGet2(this, _options2).audio.sampleRate) },
+                { id: 159, data: __privateGet2(this, _options2).audio.numberOfChannels },
+                __privateGet2(this, _options2).audio.bitDepth ? { id: 25188, data: __privateGet2(this, _options2).audio.bitDepth } : null
+              ].filter(Boolean) }
+            ] });
+          }
+        };
+        _createSegment = /* @__PURE__ */ new WeakSet();
+        createSegment_fn = function() {
+          let segment = { id: 408125543, size: SEGMENT_SIZE_BYTES, data: [
+            __privateGet2(this, _seekHead),
+            __privateGet2(this, _segmentInfo),
+            __privateGet2(this, _tracksElement)
+          ] };
+          __privateSet2(this, _segment, segment);
+          __privateGet2(this, _target4).writeEBML(segment);
+        };
+        _createCues = /* @__PURE__ */ new WeakSet();
+        createCues_fn = function() {
+          __privateSet2(this, _cues, { id: 475249515, data: [] });
+        };
+        _segmentDataOffset = /* @__PURE__ */ new WeakSet();
+        segmentDataOffset_get = function() {
+          return __privateGet2(this, _target4).dataOffsets.get(__privateGet2(this, _segment));
+        };
+        _writeVideoDecoderConfig = /* @__PURE__ */ new WeakSet();
+        writeVideoDecoderConfig_fn = function(meta) {
+          if (meta.decoderConfig) {
+            if (meta.decoderConfig.colorSpace) {
+              let colorSpace = meta.decoderConfig.colorSpace;
+              __privateSet2(this, _colorSpace, colorSpace);
+              __privateGet2(this, _colourElement).data = [
+                { id: 21937, data: {
+                  "rgb": 1,
+                  "bt709": 1,
+                  "bt470bg": 5,
+                  "smpte170m": 6
+                }[colorSpace.matrix] },
+                { id: 21946, data: {
+                  "bt709": 1,
+                  "smpte170m": 6,
+                  "iec61966-2-1": 13
+                }[colorSpace.transfer] },
+                { id: 21947, data: {
+                  "bt709": 1,
+                  "bt470bg": 5,
+                  "smpte170m": 6
+                }[colorSpace.primaries] },
+                { id: 21945, data: [1, 2][Number(colorSpace.fullRange)] }
+              ];
+              let endPos = __privateGet2(this, _target4).pos;
+              __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(__privateGet2(this, _colourElement)));
+              __privateGet2(this, _target4).writeEBML(__privateGet2(this, _colourElement));
+              __privateGet2(this, _target4).seek(endPos);
+            }
+            if (meta.decoderConfig.description) {
+              __privateMethod2(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet2(this, _videoCodecPrivate), meta.decoderConfig.description);
+            }
+          }
+        };
+        _fixVP9ColorSpace = /* @__PURE__ */ new WeakSet();
+        fixVP9ColorSpace_fn = function(chunk) {
+          if (chunk.type !== "key")
+            return;
+          if (!__privateGet2(this, _colorSpace))
+            return;
+          let i = 0;
+          if (readBits(chunk.data, 0, 2) !== 2)
+            return;
+          i += 2;
+          let profile = (readBits(chunk.data, i + 1, i + 2) << 1) + readBits(chunk.data, i + 0, i + 1);
+          i += 2;
+          if (profile === 3)
+            i++;
+          let showExistingFrame = readBits(chunk.data, i + 0, i + 1);
+          i++;
+          if (showExistingFrame)
+            return;
+          let frameType = readBits(chunk.data, i + 0, i + 1);
+          i++;
+          if (frameType !== 0)
+            return;
+          i += 2;
+          let syncCode = readBits(chunk.data, i + 0, i + 24);
+          i += 24;
+          if (syncCode !== 4817730)
+            return;
+          if (profile >= 2)
+            i++;
+          let colorSpaceID = {
+            "rgb": 7,
+            "bt709": 2,
+            "bt470bg": 1,
+            "smpte170m": 3
+          }[__privateGet2(this, _colorSpace).matrix];
+          writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
+        };
+        _createInternalChunk = /* @__PURE__ */ new WeakSet();
+        createInternalChunk_fn = function(data, type, timestamp, trackNumber) {
+          let internalChunk = {
+            data,
+            type,
+            timestamp,
+            trackNumber
+          };
+          return internalChunk;
+        };
+        _writeSimpleBlock = /* @__PURE__ */ new WeakSet();
+        writeSimpleBlock_fn = function(chunk) {
+          let msTime = Math.floor(chunk.timestamp / 1e3);
+          let clusterIsTooLong = chunk.type !== "key" && msTime - __privateGet2(this, _currentClusterTimestamp) >= MAX_CHUNK_LENGTH_MS;
+          if (clusterIsTooLong) {
+            throw new Error(
+              `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.`
+            );
+          }
+          let shouldCreateNewClusterFromKeyFrame = (chunk.trackNumber === VIDEO_TRACK_NUMBER || !__privateGet2(this, _options2).video) && chunk.type === "key" && msTime - __privateGet2(this, _currentClusterTimestamp) >= 1e3;
+          if (!__privateGet2(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame) {
+            __privateMethod2(this, _createNewCluster, createNewCluster_fn).call(this, msTime);
+          }
+          let prelude = new Uint8Array(4);
+          let view2 = new DataView(prelude.buffer);
+          view2.setUint8(0, 128 | chunk.trackNumber);
+          view2.setUint16(1, msTime - __privateGet2(this, _currentClusterTimestamp), false);
+          view2.setUint8(3, Number(chunk.type === "key") << 7);
+          let simpleBlock = { id: 163, data: [
+            prelude,
+            chunk.data
+          ] };
+          __privateGet2(this, _target4).writeEBML(simpleBlock);
+          __privateSet2(this, _duration, Math.max(__privateGet2(this, _duration), msTime));
+        };
+        _writeCodecPrivate = /* @__PURE__ */ new WeakSet();
+        writeCodecPrivate_fn = function(element, data) {
+          let endPos = __privateGet2(this, _target4).pos;
+          __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(element));
+          element = [
+            { id: 25506, size: 4, data: new Uint8Array(data) },
+            { id: 236, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE - 2 - 4 - data.byteLength) }
+          ];
+          __privateGet2(this, _target4).writeEBML(element);
+          __privateGet2(this, _target4).seek(endPos);
+        };
+        _createNewCluster = /* @__PURE__ */ new WeakSet();
+        createNewCluster_fn = function(timestamp) {
+          if (__privateGet2(this, _currentCluster)) {
+            __privateMethod2(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this);
+          }
+          __privateSet2(this, _currentCluster, { id: 524531317, size: CLUSTER_SIZE_BYTES, data: [
+            { id: 231, data: timestamp }
+          ] });
+          __privateGet2(this, _target4).writeEBML(__privateGet2(this, _currentCluster));
+          __privateSet2(this, _currentClusterTimestamp, timestamp);
+          let clusterOffsetFromSegment = __privateGet2(this, _target4).offsets.get(__privateGet2(this, _currentCluster)) - __privateGet2(this, _segmentDataOffset, segmentDataOffset_get);
+          __privateGet2(this, _cues).data.push({ id: 187, data: [
+            { id: 179, data: timestamp },
+            { id: 183, data: [
+              { id: 247, data: VIDEO_TRACK_NUMBER },
+              { id: 241, data: clusterOffsetFromSegment }
+            ] }
+          ] });
+        };
+        _finalizeCurrentCluster = /* @__PURE__ */ new WeakSet();
+        finalizeCurrentCluster_fn = function() {
+          let clusterSize = __privateGet2(this, _target4).pos - __privateGet2(this, _target4).dataOffsets.get(__privateGet2(this, _currentCluster));
+          let endPos = __privateGet2(this, _target4).pos;
+          __privateGet2(this, _target4).seek(__privateGet2(this, _target4).offsets.get(__privateGet2(this, _currentCluster)) + 4);
+          __privateGet2(this, _target4).writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
+          __privateGet2(this, _target4).seek(endPos);
+        };
+        _ensureNotFinalized2 = /* @__PURE__ */ new WeakSet();
+        ensureNotFinalized_fn2 = function() {
+          if (__privateGet2(this, _finalized2)) {
+            throw new Error("Cannot add new video or audio chunks after the file has been finalized.");
+          }
+        };
+        var main_default = WebMMuxer3;
+        var readBits = (bytes2, start, end) => {
+          let result = 0;
+          for (let i = start; i < end; i++) {
+            let byteIndex = Math.floor(i / 8);
+            let byte = bytes2[byteIndex];
+            let bitIndex = 7 - (i & 7);
+            let bit = (byte & 1 << bitIndex) >> bitIndex;
+            result <<= 1;
+            result |= bit;
+          }
+          return result;
+        };
+        var writeBits = (bytes2, start, end, value) => {
+          for (let i = start; i < end; i++) {
+            let byteIndex = Math.floor(i / 8);
+            let byte = bytes2[byteIndex];
+            let bitIndex = 7 - (i & 7);
+            byte &= ~(1 << bitIndex);
+            byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex;
+            bytes2[byteIndex] = byte;
+          }
+        };
+        return __toCommonJS(main_exports);
+      })();
+      WebMMuxer2 = WebMMuxer2.default;
+      if (typeof module === "object" && typeof module.exports === "object") module.exports = WebMMuxer2;
+    }
+  });
+
+  // node_modules/mp4-muxer/build/mp4-muxer.mjs
+  var __defProp2 = Object.defineProperty;
+  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
+  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
+  var __propIsEnum = Object.prototype.propertyIsEnumerable;
+  var __pow = Math.pow;
+  var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
+  var __spreadValues = (a, b) => {
+    for (var prop in b || (b = {}))
+      if (__hasOwnProp2.call(b, prop))
+        __defNormalProp(a, prop, b[prop]);
+    if (__getOwnPropSymbols)
+      for (var prop of __getOwnPropSymbols(b)) {
+        if (__propIsEnum.call(b, prop))
+          __defNormalProp(a, prop, b[prop]);
+      }
+    return a;
+  };
+  var __accessCheck = (obj, member, msg) => {
+    if (!member.has(obj))
+      throw TypeError("Cannot " + msg);
+  };
+  var __privateGet = (obj, member, getter) => {
+    __accessCheck(obj, member, "read from private field");
+    return getter ? getter.call(obj) : member.get(obj);
+  };
+  var __privateAdd = (obj, member, value) => {
+    if (member.has(obj))
+      throw TypeError("Cannot add the same private member more than once");
+    member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
+  };
+  var __privateSet = (obj, member, value, setter) => {
+    __accessCheck(obj, member, "write to private field");
+    setter ? setter.call(obj, value) : member.set(obj, value);
+    return value;
+  };
+  var __privateMethod = (obj, member, method) => {
+    __accessCheck(obj, member, "access private method");
+    return method;
+  };
+  var bytes = new Uint8Array(8);
+  var view = new DataView(bytes.buffer);
+  var u8 = (value) => {
+    return [(value % 256 + 256) % 256];
+  };
+  var u16 = (value) => {
+    view.setUint16(0, value, false);
+    return [bytes[0], bytes[1]];
+  };
+  var i16 = (value) => {
+    view.setInt16(0, value, false);
+    return [bytes[0], bytes[1]];
+  };
+  var u24 = (value) => {
+    view.setUint32(0, value, false);
+    return [bytes[1], bytes[2], bytes[3]];
+  };
+  var u32 = (value) => {
+    view.setUint32(0, value, false);
+    return [bytes[0], bytes[1], bytes[2], bytes[3]];
+  };
+  var u64 = (value) => {
+    view.setUint32(0, Math.floor(value / __pow(2, 32)), false);
+    view.setUint32(4, value, false);
+    return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
+  };
+  var fixed_8_8 = (value) => {
+    view.setInt16(0, __pow(2, 8) * value, false);
+    return [bytes[0], bytes[1]];
+  };
+  var fixed_16_16 = (value) => {
+    view.setInt32(0, __pow(2, 16) * value, false);
+    return [bytes[0], bytes[1], bytes[2], bytes[3]];
+  };
+  var fixed_2_30 = (value) => {
+    view.setInt32(0, __pow(2, 30) * value, false);
+    return [bytes[0], bytes[1], bytes[2], bytes[3]];
+  };
+  var ascii = (text, nullTerminated = false) => {
+    let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
+    if (nullTerminated)
+      bytes2.push(0);
+    return bytes2;
+  };
+  var last = (arr) => {
+    return arr && arr[arr.length - 1];
+  };
+  var intoTimescale = (timeInSeconds, timescale, round = true) => {
+    let value = timeInSeconds * timescale;
+    return round ? Math.round(value) : value;
+  };
+  var rotationMatrix = (rotationInDegrees) => {
+    let theta = rotationInDegrees * (Math.PI / 180);
+    let cosTheta = Math.cos(theta);
+    let sinTheta = Math.sin(theta);
+    return [
+      cosTheta,
+      sinTheta,
+      0,
+      -sinTheta,
+      cosTheta,
+      0,
+      0,
+      0,
+      1
+    ];
+  };
+  var IDENTITY_MATRIX = rotationMatrix(0);
+  var matrixToBytes = (matrix) => {
+    return [
+      fixed_16_16(matrix[0]),
+      fixed_16_16(matrix[1]),
+      fixed_2_30(matrix[2]),
+      fixed_16_16(matrix[3]),
+      fixed_16_16(matrix[4]),
+      fixed_2_30(matrix[5]),
+      fixed_16_16(matrix[6]),
+      fixed_16_16(matrix[7]),
+      fixed_2_30(matrix[8])
+    ];
+  };
+  var box = (type, contents, children) => ({
+    type,
+    contents: contents && new Uint8Array(contents.flat(10)),
+    children
+  });
+  var fullBox = (type, version, flags, contents, children) => box(
+    type,
+    [u8(version), u24(flags), contents != null ? contents : []],
+    children
+  );
+  var ftyp = (holdsHevc) => {
+    if (holdsHevc)
+      return box("ftyp", [
+        ascii("isom"),
+        // Major brand
+        u32(0),
+        // Minor version
+        ascii("iso4"),
+        // Compatible brand 1
+        ascii("hvc1")
+        // Compatible brand 2
+      ]);
+    return box("ftyp", [
+      ascii("isom"),
+      // Major brand
+      u32(0),
+      // Minor version
+      ascii("isom"),
+      // Compatible brand 1
+      ascii("avc1"),
+      // Compatible brand 2
+      ascii("mp41")
+      // Compatible brand 3
+    ]);
+  };
+  var mdat = () => ({ type: "mdat", largeSize: true });
+  var moov = (tracks, creationTime) => box("moov", null, [
+    mvhd(creationTime, tracks),
+    ...tracks.map((x) => trak(x, creationTime))
+  ]);
+  var mvhd = (creationTime, tracks) => {
+    let duration = intoTimescale(Math.max(
+      0,
+      ...tracks.filter((x) => x.samples.length > 0).map((x) => last(x.samples).timestamp + last(x.samples).duration)
+    ), GLOBAL_TIMESCALE);
+    let nextTrackId = Math.max(...tracks.map((x) => x.id)) + 1;
+    return fullBox("mvhd", 0, 0, [
+      u32(creationTime),
+      // Creation time
+      u32(creationTime),
+      // Modification time
+      u32(GLOBAL_TIMESCALE),
+      // Timescale
+      u32(duration),
+      // Duration
+      fixed_16_16(1),
+      // Preferred rate
+      fixed_8_8(1),
+      // Preferred volume
+      Array(10).fill(0),
+      // Reserved
+      matrixToBytes(IDENTITY_MATRIX),
+      // Matrix
+      Array(24).fill(0),
+      // Pre-defined
+      u32(nextTrackId)
+      // Next track ID
+    ]);
+  };
+  var trak = (track, creationTime) => box("trak", null, [
+    tkhd(track, creationTime),
+    mdia(track, creationTime)
+  ]);
+  var tkhd = (track, creationTime) => {
+    let lastSample = last(track.samples);
+    let durationInGlobalTimescale = intoTimescale(
+      lastSample ? lastSample.timestamp + lastSample.duration : 0,
+      GLOBAL_TIMESCALE
+    );
+    return fullBox("tkhd", 0, 3, [
+      u32(creationTime),
+      // Creation time
+      u32(creationTime),
+      // Modification time
+      u32(track.id),
+      // Track ID
+      u32(0),
+      // Reserved
+      u32(durationInGlobalTimescale),
+      // Duration
+      Array(8).fill(0),
+      // Reserved
+      u16(0),
+      // Layer
+      u16(0),
+      // Alternate group
+      fixed_8_8(track.info.type === "audio" ? 1 : 0),
+      // Volume
+      u16(0),
+      // Reserved
+      matrixToBytes(rotationMatrix(track.info.type === "video" ? track.info.rotation : 0)),
+      // Matrix
+      fixed_16_16(track.info.type === "video" ? track.info.width : 0),
+      // Track width
+      fixed_16_16(track.info.type === "video" ? track.info.height : 0)
+      // Track height
+    ]);
+  };
+  var mdia = (track, creationTime) => box("mdia", null, [
+    mdhd(track, creationTime),
+    hdlr(track.info.type === "video" ? "vide" : "soun"),
+    minf(track)
+  ]);
+  var mdhd = (track, creationTime) => {
+    let lastSample = last(track.samples);
+    let localDuration = intoTimescale(
+      lastSample ? lastSample.timestamp + lastSample.duration : 0,
+      track.timescale
+    );
+    return fullBox("mdhd", 0, 0, [
+      u32(creationTime),
+      // Creation time
+      u32(creationTime),
+      // Modification time
+      u32(track.timescale),
+      // Timescale
+      u32(localDuration),
+      // Duration
+      u16(21956),
+      // Language ("und", undetermined)
+      u16(0)
+      // Quality
+    ]);
+  };
+  var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
+    ascii("mhlr"),
+    // Component type
+    ascii(componentSubtype),
+    // Component subtype
+    u32(0),
+    // Component manufacturer
+    u32(0),
+    // Component flags
+    u32(0),
+    // Component flags mask
+    ascii("mp4-muxer-hdlr")
+    // Component name
+  ]);
+  var minf = (track) => box("minf", null, [
+    track.info.type === "video" ? vmhd() : smhd(),
+    dinf(),
+    stbl(track)
+  ]);
+  var vmhd = () => fullBox("vmhd", 0, 1, [
+    u16(0),
+    // Graphics mode
+    u16(0),
+    // Opcolor R
+    u16(0),
+    // Opcolor G
+    u16(0)
+    // Opcolor B
+  ]);
+  var smhd = () => fullBox("smhd", 0, 0, [
+    u16(0),
+    // Balance
+    u16(0)
+    // Reserved
+  ]);
+  var dinf = () => box("dinf", null, [
+    dref()
+  ]);
+  var dref = () => fullBox("dref", 0, 0, [
+    u32(1)
+    // Entry count
+  ], [
+    url()
+  ]);
+  var url = () => fullBox("url ", 0, 1);
+  var stbl = (track) => box("stbl", null, [
+    stsd(track),
+    stts(track),
+    stss(track),
+    stsc(track),
+    stsz(track),
+    stco(track)
+  ]);
+  var stsd = (track) => fullBox("stsd", 0, 0, [
+    u32(1)
+    // Entry count
+  ], [
+    track.info.type === "video" ? videoSampleDescription(
+      VIDEO_CODEC_TO_BOX_NAME[track.info.codec],
+      track
+    ) : soundSampleDescription(
+      AUDIO_CODEC_TO_BOX_NAME[track.info.codec],
+      track
+    )
+  ]);
+  var videoSampleDescription = (compressionType, track) => box(compressionType, [
+    Array(6).fill(0),
+    // Reserved
+    u16(1),
+    // Data reference index
+    u16(0),
+    // Pre-defined
+    u16(0),
+    // Reserved
+    Array(12).fill(0),
+    // Pre-defined
+    u16(track.info.width),
+    // Width
+    u16(track.info.height),
+    // Height
+    u32(4718592),
+    // Horizontal resolution
+    u32(4718592),
+    // Vertical resolution
+    u32(0),
+    // Reserved
+    u16(1),
+    // Frame count
+    Array(32).fill(0),
+    // Compressor name
+    u16(24),
+    // Depth
+    i16(65535)
+    // Pre-defined
+  ], [
+    VIDEO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track)
+  ]);
+  var avcC = (track) => track.codecPrivate && box("avcC", [...track.codecPrivate]);
+  var hvcC = (track) => track.codecPrivate && box("hvcC", [...track.codecPrivate]);
+  var vpcC = (track) => track.codecPrivate && box("vpcC", [...track.codecPrivate]);
+  var av1C = (track) => track.codecPrivate && box("av1C", [...track.codecPrivate]);
+  var soundSampleDescription = (compressionType, track) => box(compressionType, [
+    Array(6).fill(0),
+    // Reserved
+    u16(1),
+    // Data reference index
+    u16(0),
+    // Version
+    u16(0),
+    // Revision level
+    u32(0),
+    // Vendor
+    u16(track.info.numberOfChannels),
+    // Number of channels
+    u16(16),
+    // Sample size (bits)
+    u16(0),
+    // Compression ID
+    u16(0),
+    // Packet size
+    fixed_16_16(track.info.sampleRate)
+    // Sample rate
+  ], [
+    AUDIO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track)
+  ]);
+  var esds = (track) => fullBox("esds", 0, 0, [
+    // https://stackoverflow.com/a/54803118
+    u32(58753152),
+    // TAG(3) = Object Descriptor ([2])
+    u8(32 + track.codecPrivate.byteLength),
+    // length of this OD (which includes the next 2 tags)
+    u16(1),
+    // ES_ID = 1
+    u8(0),
+    // flags etc = 0
+    u32(75530368),
+    // TAG(4) = ES Descriptor ([2]) embedded in above OD
+    u8(18 + track.codecPrivate.byteLength),
+    // length of this ESD
+    u8(64),
+    // MPEG-4 Audio
+    u8(21),
+    // stream type(6bits)=5 audio, flags(2bits)=1
+    u24(0),
+    // 24bit buffer size
+    u32(130071),
+    // max bitrate
+    u32(130071),
+    // avg bitrate
+    u32(92307584),
+    // TAG(5) = ASC ([2],[3]) embedded in above OD
+    u8(track.codecPrivate.byteLength),
+    // length
+    ...track.codecPrivate,
+    u32(109084800),
+    // TAG(6)
+    u8(1),
+    // length
+    u8(2)
+    // data
+  ]);
+  var dOps = (track) => box("dOps", [
+    u8(0),
+    // Version
+    u8(track.info.numberOfChannels),
+    // OutputChannelCount
+    u16(3840),
+    // PreSkip, should be at least 80 milliseconds worth of playback, measured in 48000 Hz samples
+    u32(track.info.sampleRate),
+    // InputSampleRate
+    fixed_8_8(0),
+    // OutputGain
+    u8(0)
+    // ChannelMappingFamily
+  ]);
+  var stts = (track) => {
+    return fullBox("stts", 0, 0, [
+      u32(track.timeToSampleTable.length),
+      // Number of entries
+      track.timeToSampleTable.map((x) => [
+        // Time-to-sample table
+        u32(x.sampleCount),
+        // Sample count
+        u32(x.sampleDelta)
+        // Sample duration
+      ])
+    ]);
+  };
+  var stss = (track) => {
+    if (track.samples.every((x) => x.type === "key"))
+      return null;
+    let keySamples = [...track.samples.entries()].filter(([, sample]) => sample.type === "key");
+    return fullBox("stss", 0, 0, [
+      u32(keySamples.length),
+      // Number of entries
+      keySamples.map(([index]) => u32(index + 1))
+      // Sync sample table
+    ]);
+  };
+  var stsc = (track) => {
+    return fullBox("stsc", 0, 0, [
+      u32(track.compactlyCodedChunkTable.length),
+      // Number of entries
+      track.compactlyCodedChunkTable.map((x) => [
+        // Sample-to-chunk table
+        u32(x.firstChunk),
+        // First chunk
+        u32(x.samplesPerChunk),
+        // Samples per chunk
+        u32(1)
+        // Sample description index
+      ])
+    ]);
+  };
+  var stsz = (track) => fullBox("stsz", 0, 0, [
+    u32(0),
+    // Sample size (0 means non-constant size)
+    u32(track.samples.length),
+    // Number of entries
+    track.samples.map((x) => u32(x.size))
+    // Sample size table
+  ]);
+  var stco = (track) => {
+    if (track.writtenChunks.length > 0 && last(track.writtenChunks).offset >= __pow(2, 32)) {
+      return fullBox("co64", 0, 0, [
+        u32(track.writtenChunks.length),
+        // Number of entries
+        track.writtenChunks.map((x) => u64(x.offset))
+        // Chunk offset table
+      ]);
+    }
+    return fullBox("stco", 0, 0, [
+      u32(track.writtenChunks.length),
+      // Number of entries
+      track.writtenChunks.map((x) => u32(x.offset))
+      // Chunk offset table
+    ]);
+  };
+  var VIDEO_CODEC_TO_BOX_NAME = {
+    "avc": "avc1",
+    "hevc": "hvc1",
+    "vp9": "vp09",
+    "av1": "av01"
+  };
+  var VIDEO_CODEC_TO_CONFIGURATION_BOX = {
+    "avc": avcC,
+    "hevc": hvcC,
+    "vp9": vpcC,
+    "av1": av1C
+  };
+  var AUDIO_CODEC_TO_BOX_NAME = {
+    "aac": "mp4a",
+    "opus": "opus"
+  };
+  var AUDIO_CODEC_TO_CONFIGURATION_BOX = {
+    "aac": esds,
+    "opus": dOps
+  };
+  var ArrayBufferTarget = class {
+    constructor() {
+      this.buffer = null;
+    }
+  };
+  var StreamTarget = class {
+    constructor(onData, onDone, options) {
+      this.onData = onData;
+      this.onDone = onDone;
+      this.options = options;
+    }
+  };
+  var FileSystemWritableFileStreamTarget = class {
+    constructor(stream, options) {
+      this.stream = stream;
+      this.options = options;
+    }
+  };
+  var _helper;
+  var _helperView;
+  var Writer = class {
+    constructor() {
+      this.pos = 0;
+      __privateAdd(this, _helper, new Uint8Array(8));
+      __privateAdd(this, _helperView, new DataView(__privateGet(this, _helper).buffer));
+      this.offsets = /* @__PURE__ */ new WeakMap();
+    }
+    /** Sets the current position for future writes to a new one. */
+    seek(newPos) {
+      this.pos = newPos;
+    }
+    writeU32(value) {
+      __privateGet(this, _helperView).setUint32(0, value, false);
+      this.write(__privateGet(this, _helper).subarray(0, 4));
+    }
+    writeU64(value) {
+      __privateGet(this, _helperView).setUint32(0, Math.floor(value / __pow(2, 32)), false);
+      __privateGet(this, _helperView).setUint32(4, value, false);
+      this.write(__privateGet(this, _helper).subarray(0, 8));
+    }
+    writeAscii(text) {
+      for (let i = 0; i < text.length; i++) {
+        __privateGet(this, _helperView).setUint8(i % 8, text.charCodeAt(i));
+        if (i % 8 === 7)
+          this.write(__privateGet(this, _helper));
+      }
+      if (text.length % 8 !== 0) {
+        this.write(__privateGet(this, _helper).subarray(0, text.length % 8));
+      }
+    }
+    writeBox(box2) {
+      var _a, _b;
+      this.offsets.set(box2, this.pos);
+      if (box2.contents && !box2.children) {
+        this.writeBoxHeader(box2, (_a = box2.size) != null ? _a : box2.contents.byteLength + 8);
+        this.write(box2.contents);
+      } else {
+        let startPos = this.pos;
+        this.writeBoxHeader(box2, 0);
+        if (box2.contents)
+          this.write(box2.contents);
+        if (box2.children) {
+          for (let child of box2.children)
+            if (child)
+              this.writeBox(child);
+        }
+        let endPos = this.pos;
+        let size = (_b = box2.size) != null ? _b : endPos - startPos;
+        this.seek(startPos);
+        this.writeBoxHeader(box2, size);
+        this.seek(endPos);
+      }
+    }
+    writeBoxHeader(box2, size) {
+      this.writeU32(box2.largeSize ? 1 : size);
+      this.writeAscii(box2.type);
+      if (box2.largeSize)
+        this.writeU64(size);
+    }
+    patchBox(box2) {
+      let endPos = this.pos;
+      this.seek(this.offsets.get(box2));
+      this.writeBox(box2);
+      this.seek(endPos);
+    }
+  };
+  _helper = /* @__PURE__ */ new WeakMap();
+  _helperView = /* @__PURE__ */ new WeakMap();
+  var _target;
+  var _buffer;
+  var _bytes;
+  var _ensureSize;
+  var ensureSize_fn;
+  var ArrayBufferTargetWriter = class extends Writer {
+    constructor(target) {
+      super();
+      __privateAdd(this, _ensureSize);
+      __privateAdd(this, _target, void 0);
+      __privateAdd(this, _buffer, new ArrayBuffer(__pow(2, 16)));
+      __privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer)));
+      __privateSet(this, _target, target);
+    }
+    write(data) {
+      __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos + data.byteLength);
+      __privateGet(this, _bytes).set(data, this.pos);
+      this.pos += data.byteLength;
+    }
+    finalize() {
+      __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos);
+      __privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, this.pos);
+    }
+  };
+  _target = /* @__PURE__ */ new WeakMap();
+  _buffer = /* @__PURE__ */ new WeakMap();
+  _bytes = /* @__PURE__ */ new WeakMap();
+  _ensureSize = /* @__PURE__ */ new WeakSet();
+  ensureSize_fn = function(size) {
+    let newLength = __privateGet(this, _buffer).byteLength;
+    while (newLength < size)
+      newLength *= 2;
+    if (newLength === __privateGet(this, _buffer).byteLength)
+      return;
+    let newBuffer = new ArrayBuffer(newLength);
+    let newBytes = new Uint8Array(newBuffer);
+    newBytes.set(__privateGet(this, _bytes), 0);
+    __privateSet(this, _buffer, newBuffer);
+    __privateSet(this, _bytes, newBytes);
+  };
+  var _target2;
+  var _sections;
+  var StreamTargetWriter = class extends Writer {
+    constructor(target) {
+      super();
+      __privateAdd(this, _target2, void 0);
+      __privateAdd(this, _sections, []);
+      __privateSet(this, _target2, target);
+    }
+    write(data) {
+      __privateGet(this, _sections).push({
+        data: data.slice(),
+        start: this.pos
+      });
+      this.pos += data.byteLength;
+    }
+    flush() {
+      if (__privateGet(this, _sections).length === 0)
+        return;
+      let chunks = [];
+      let sorted = [...__privateGet(this, _sections)].sort((a, b) => a.start - b.start);
+      chunks.push({
+        start: sorted[0].start,
+        size: sorted[0].data.byteLength
+      });
+      for (let i = 1; i < sorted.length; i++) {
+        let lastChunk = chunks[chunks.length - 1];
+        let section = sorted[i];
+        if (section.start <= lastChunk.start + lastChunk.size) {
+          lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
+        } else {
+          chunks.push({
+            start: section.start,
+            size: section.data.byteLength
+          });
+        }
+      }
+      for (let chunk of chunks) {
+        chunk.data = new Uint8Array(chunk.size);
+        for (let section of __privateGet(this, _sections)) {
+          if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
+            chunk.data.set(section.data, section.start - chunk.start);
+          }
+        }
+        __privateGet(this, _target2).onData(chunk.data, chunk.start);
+      }
+      __privateGet(this, _sections).length = 0;
+    }
+    finalize() {
+      var _a, _b;
+      (_b = (_a = __privateGet(this, _target2)).onDone) == null ? void 0 : _b.call(_a);
+    }
+  };
+  _target2 = /* @__PURE__ */ new WeakMap();
+  _sections = /* @__PURE__ */ new WeakMap();
+  var DEFAULT_CHUNK_SIZE = __pow(2, 24);
+  var MAX_CHUNKS_AT_ONCE = 2;
+  var _target3;
+  var _chunkSize;
+  var _chunks;
+  var _writeDataIntoChunks;
+  var writeDataIntoChunks_fn;
+  var _insertSectionIntoChunk;
+  var insertSectionIntoChunk_fn;
+  var _createChunk;
+  var createChunk_fn;
+  var _flushChunks;
+  var flushChunks_fn;
+  var ChunkedStreamTargetWriter = class extends Writer {
+    constructor(target) {
+      var _a, _b;
+      super();
+      __privateAdd(this, _writeDataIntoChunks);
+      __privateAdd(this, _insertSectionIntoChunk);
+      __privateAdd(this, _createChunk);
+      __privateAdd(this, _flushChunks);
+      __privateAdd(this, _target3, void 0);
+      __privateAdd(this, _chunkSize, void 0);
+      __privateAdd(this, _chunks, []);
+      __privateSet(this, _target3, target);
+      __privateSet(this, _chunkSize, (_b = (_a = target.options) == null ? void 0 : _a.chunkSize) != null ? _b : DEFAULT_CHUNK_SIZE);
+      if (!Number.isInteger(__privateGet(this, _chunkSize)) || __privateGet(this, _chunkSize) < __pow(2, 10)) {
+        throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.");
+      }
+    }
+    write(data) {
+      __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data, this.pos);
+      __privateMethod(this, _flushChunks, flushChunks_fn).call(this);
+      this.pos += data.byteLength;
+    }
+    finalize() {
+      var _a, _b;
+      __privateMethod(this, _flushChunks, flushChunks_fn).call(this, true);
+      (_b = (_a = __privateGet(this, _target3)).onDone) == null ? void 0 : _b.call(_a);
+    }
+  };
+  _target3 = /* @__PURE__ */ new WeakMap();
+  _chunkSize = /* @__PURE__ */ new WeakMap();
+  _chunks = /* @__PURE__ */ new WeakMap();
+  _writeDataIntoChunks = /* @__PURE__ */ new WeakSet();
+  writeDataIntoChunks_fn = function(data, position) {
+    let chunkIndex = __privateGet(this, _chunks).findIndex((x) => x.start <= position && position < x.start + __privateGet(this, _chunkSize));
+    if (chunkIndex === -1)
+      chunkIndex = __privateMethod(this, _createChunk, createChunk_fn).call(this, position);
+    let chunk = __privateGet(this, _chunks)[chunkIndex];
+    let relativePosition = position - chunk.start;
+    let toWrite = data.subarray(0, Math.min(__privateGet(this, _chunkSize) - relativePosition, data.byteLength));
+    chunk.data.set(toWrite, relativePosition);
+    let section = {
+      start: relativePosition,
+      end: relativePosition + toWrite.byteLength
+    };
+    __privateMethod(this, _insertSectionIntoChunk, insertSectionIntoChunk_fn).call(this, chunk, section);
+    if (chunk.written[0].start === 0 && chunk.written[0].end === __privateGet(this, _chunkSize)) {
+      chunk.shouldFlush = true;
+    }
+    if (__privateGet(this, _chunks).length > MAX_CHUNKS_AT_ONCE) {
+      for (let i = 0; i < __privateGet(this, _chunks).length - 1; i++) {
+        __privateGet(this, _chunks)[i].shouldFlush = true;
+      }
+      __privateMethod(this, _flushChunks, flushChunks_fn).call(this);
+    }
+    if (toWrite.byteLength < data.byteLength) {
+      __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data.subarray(toWrite.byteLength), position + toWrite.byteLength);
+    }
+  };
+  _insertSectionIntoChunk = /* @__PURE__ */ new WeakSet();
+  insertSectionIntoChunk_fn = function(chunk, section) {
+    let low = 0;
+    let high = chunk.written.length - 1;
+    let index = -1;
+    while (low <= high) {
+      let mid = Math.floor(low + (high - low + 1) / 2);
+      if (chunk.written[mid].start <= section.start) {
+        low = mid + 1;
+        index = mid;
+      } else {
+        high = mid - 1;
+      }
+    }
+    chunk.written.splice(index + 1, 0, section);
+    if (index === -1 || chunk.written[index].end < section.start)
+      index++;
+    while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
+      chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
+      chunk.written.splice(index + 1, 1);
+    }
+  };
+  _createChunk = /* @__PURE__ */ new WeakSet();
+  createChunk_fn = function(includesPosition) {
+    let start = Math.floor(includesPosition / __privateGet(this, _chunkSize)) * __privateGet(this, _chunkSize);
+    let chunk = {
+      start,
+      data: new Uint8Array(__privateGet(this, _chunkSize)),
+      written: [],
+      shouldFlush: false
+    };
+    __privateGet(this, _chunks).push(chunk);
+    __privateGet(this, _chunks).sort((a, b) => a.start - b.start);
+    return __privateGet(this, _chunks).indexOf(chunk);
+  };
+  _flushChunks = /* @__PURE__ */ new WeakSet();
+  flushChunks_fn = function(force = false) {
+    for (let i = 0; i < __privateGet(this, _chunks).length; i++) {
+      let chunk = __privateGet(this, _chunks)[i];
+      if (!chunk.shouldFlush && !force)
+        continue;
+      for (let section of chunk.written) {
+        __privateGet(this, _target3).onData(
+          chunk.data.subarray(section.start, section.end),
+          chunk.start + section.start
+        );
+      }
+      __privateGet(this, _chunks).splice(i--, 1);
+    }
+  };
+  var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTargetWriter {
+    constructor(target) {
+      var _a;
+      super(new StreamTarget(
+        (data, position) => target.stream.write({
+          type: "write",
+          data,
+          position
+        }),
+        void 0,
+        { chunkSize: (_a = target.options) == null ? void 0 : _a.chunkSize }
+      ));
+    }
+  };
+  var GLOBAL_TIMESCALE = 1e3;
+  var SUPPORTED_VIDEO_CODECS2 = ["avc", "hevc", "vp9", "av1"];
+  var SUPPORTED_AUDIO_CODECS2 = ["aac", "opus"];
+  var TIMESTAMP_OFFSET = 2082844800;
+  var MAX_CHUNK_DURATION = 0.5;
+  var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset"];
+  var _options;
+  var _writer;
+  var _mdat;
+  var _videoTrack;
+  var _audioTrack;
+  var _creationTime;
+  var _finalized;
+  var _validateOptions;
+  var validateOptions_fn;
+  var _writeHeader;
+  var writeHeader_fn;
+  var _prepareTracks;
+  var prepareTracks_fn;
+  var _generateMpeg4AudioSpecificConfig;
+  var generateMpeg4AudioSpecificConfig_fn;
+  var _addSampleToTrack;
+  var addSampleToTrack_fn;
+  var _validateTimestamp;
+  var validateTimestamp_fn;
+  var _writeCurrentChunk;
+  var writeCurrentChunk_fn;
+  var _maybeFlushStreamingTargetWriter;
+  var maybeFlushStreamingTargetWriter_fn;
+  var _ensureNotFinalized;
+  var ensureNotFinalized_fn;
+  var Muxer = class {
+    constructor(options) {
+      __privateAdd(this, _validateOptions);
+      __privateAdd(this, _writeHeader);
+      __privateAdd(this, _prepareTracks);
+      __privateAdd(this, _generateMpeg4AudioSpecificConfig);
+      __privateAdd(this, _addSampleToTrack);
+      __privateAdd(this, _validateTimestamp);
+      __privateAdd(this, _writeCurrentChunk);
+      __privateAdd(this, _maybeFlushStreamingTargetWriter);
+      __privateAdd(this, _ensureNotFinalized);
+      __privateAdd(this, _options, void 0);
+      __privateAdd(this, _writer, void 0);
+      __privateAdd(this, _mdat, void 0);
+      __privateAdd(this, _videoTrack, null);
+      __privateAdd(this, _audioTrack, null);
+      __privateAdd(this, _creationTime, Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET);
+      __privateAdd(this, _finalized, false);
+      var _a;
+      __privateMethod(this, _validateOptions, validateOptions_fn).call(this, options);
+      this.target = options.target;
+      __privateSet(this, _options, __spreadValues({
+        firstTimestampBehavior: "strict"
+      }, options));
+      if (options.target instanceof ArrayBufferTarget) {
+        __privateSet(this, _writer, new ArrayBufferTargetWriter(options.target));
+      } else if (options.target instanceof StreamTarget) {
+        __privateSet(this, _writer, ((_a = options.target.options) == null ? void 0 : _a.chunked) ? new ChunkedStreamTargetWriter(options.target) : new StreamTargetWriter(options.target));
+      } else if (options.target instanceof FileSystemWritableFileStreamTarget) {
+        __privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target));
+      } else {
+        throw new Error(`Invalid target: ${options.target}`);
+      }
+      __privateMethod(this, _writeHeader, writeHeader_fn).call(this);
+      __privateMethod(this, _prepareTracks, prepareTracks_fn).call(this);
+    }
+    addVideoChunk(sample, meta, timestamp) {
+      let data = new Uint8Array(sample.byteLength);
+      sample.copyTo(data);
+      this.addVideoChunkRaw(data, sample.type, timestamp != null ? timestamp : sample.timestamp, sample.duration, meta);
+    }
+    addVideoChunkRaw(data, type, timestamp, duration, meta) {
+      __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
+      if (!__privateGet(this, _options).video)
+        throw new Error("No video track declared.");
+      __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta);
+    }
+    addAudioChunk(sample, meta, timestamp) {
+      let data = new Uint8Array(sample.byteLength);
+      sample.copyTo(data);
+      this.addAudioChunkRaw(data, sample.type, timestamp != null ? timestamp : sample.timestamp, sample.duration, meta);
+    }
+    addAudioChunkRaw(data, type, timestamp, duration, meta) {
+      __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
+      if (!__privateGet(this, _options).audio)
+        throw new Error("No audio track declared.");
+      __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta);
+    }
+    /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
+    finalize() {
+      if (__privateGet(this, _videoTrack))
+        __privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack));
+      if (__privateGet(this, _audioTrack))
+        __privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack));
+      let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat));
+      let mdatSize = __privateGet(this, _writer).pos - mdatPos;
+      __privateGet(this, _mdat).size = mdatSize;
+      __privateGet(this, _writer).patchBox(__privateGet(this, _mdat));
+      let movieBox = moov([__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean), __privateGet(this, _creationTime));
+      __privateGet(this, _writer).writeBox(movieBox);
+      __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
+      __privateGet(this, _writer).finalize();
+      __privateSet(this, _finalized, true);
+    }
+  };
+  _options = /* @__PURE__ */ new WeakMap();
+  _writer = /* @__PURE__ */ new WeakMap();
+  _mdat = /* @__PURE__ */ new WeakMap();
+  _videoTrack = /* @__PURE__ */ new WeakMap();
+  _audioTrack = /* @__PURE__ */ new WeakMap();
+  _creationTime = /* @__PURE__ */ new WeakMap();
+  _finalized = /* @__PURE__ */ new WeakMap();
+  _validateOptions = /* @__PURE__ */ new WeakSet();
+  validateOptions_fn = function(options) {
+    if (options.video) {
+      if (!SUPPORTED_VIDEO_CODECS2.includes(options.video.codec)) {
+        throw new Error(`Unsupported video codec: ${options.video.codec}`);
+      }
+      if (options.video.rotation !== void 0 && ![0, 90, 180, 270].includes(options.video.rotation)) {
+        throw new Error(`Invalid video rotation: ${options.video.rotation}. Has to be 0, 90, 180 or 270.`);
+      }
+    }
+    if (options.audio && !SUPPORTED_AUDIO_CODECS2.includes(options.audio.codec)) {
+      throw new Error(`Unsupported audio codec: ${options.audio.codec}`);
+    }
+    if (options.firstTimestampBehavior && !FIRST_TIMESTAMP_BEHAVIORS.includes(options.firstTimestampBehavior)) {
+      throw new Error(`Invalid first timestamp behavior: ${options.firstTimestampBehavior}`);
+    }
+  };
+  _writeHeader = /* @__PURE__ */ new WeakSet();
+  writeHeader_fn = function() {
+    var _a;
+    let holdsHevc = ((_a = __privateGet(this, _options).video) == null ? void 0 : _a.codec) === "hevc";
+    __privateGet(this, _writer).writeBox(ftyp(holdsHevc));
+    __privateSet(this, _mdat, mdat());
+    __privateGet(this, _writer).writeBox(__privateGet(this, _mdat));
+    __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
+  };
+  _prepareTracks = /* @__PURE__ */ new WeakSet();
+  prepareTracks_fn = function() {
+    var _a;
+    if (__privateGet(this, _options).video) {
+      __privateSet(this, _videoTrack, {
+        id: 1,
+        info: {
+          type: "video",
+          codec: __privateGet(this, _options).video.codec,
+          width: __privateGet(this, _options).video.width,
+          height: __privateGet(this, _options).video.height,
+          rotation: (_a = __privateGet(this, _options).video.rotation) != null ? _a : 0
+        },
+        timescale: 720,
+        // = lcm(24, 30, 60, 120, 144, 240, 360), so should fit with many framerates
+        codecPrivate: new Uint8Array(0),
+        samples: [],
+        writtenChunks: [],
+        currentChunk: null,
+        firstTimestamp: void 0,
+        lastTimestamp: -1,
+        timeToSampleTable: [],
+        lastTimescaleUnits: null,
+        compactlyCodedChunkTable: []
+      });
+    }
+    if (__privateGet(this, _options).audio) {
+      let guessedCodecPrivate = __privateMethod(this, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn).call(
+        this,
+        2,
+        // Object type for AAC-LC, since it's the most common
+        __privateGet(this, _options).audio.sampleRate,
+        __privateGet(this, _options).audio.numberOfChannels
+      );
+      __privateSet(this, _audioTrack, {
+        id: __privateGet(this, _options).video ? 2 : 1,
+        info: {
+          type: "audio",
+          codec: __privateGet(this, _options).audio.codec,
+          numberOfChannels: __privateGet(this, _options).audio.numberOfChannels,
+          sampleRate: __privateGet(this, _options).audio.sampleRate
+        },
+        timescale: __privateGet(this, _options).audio.sampleRate,
+        codecPrivate: guessedCodecPrivate,
+        samples: [],
+        writtenChunks: [],
+        currentChunk: null,
+        firstTimestamp: void 0,
+        lastTimestamp: -1,
+        timeToSampleTable: [],
+        lastTimescaleUnits: null,
+        compactlyCodedChunkTable: []
+      });
+    }
+  };
+  _generateMpeg4AudioSpecificConfig = /* @__PURE__ */ new WeakSet();
+  generateMpeg4AudioSpecificConfig_fn = function(objectType, sampleRate, numberOfChannels) {
+    let frequencyIndices = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
+    let frequencyIndex = frequencyIndices.indexOf(sampleRate);
+    let channelConfig = numberOfChannels;
+    let configBits = "";
+    configBits += objectType.toString(2).padStart(5, "0");
+    configBits += frequencyIndex.toString(2).padStart(4, "0");
+    if (frequencyIndex === 15)
+      configBits += sampleRate.toString(2).padStart(24, "0");
+    configBits += channelConfig.toString(2).padStart(4, "0");
+    let paddingLength = Math.ceil(configBits.length / 8) * 8;
+    configBits = configBits.padEnd(paddingLength, "0");
+    let configBytes = new Uint8Array(configBits.length / 8);
+    for (let i = 0; i < configBits.length; i += 8) {
+      configBytes[i / 8] = parseInt(configBits.slice(i, i + 8), 2);
+    }
+    return configBytes;
+  };
+  _addSampleToTrack = /* @__PURE__ */ new WeakSet();
+  addSampleToTrack_fn = function(track, data, type, timestamp, duration, meta) {
+    var _a;
+    let timestampInSeconds = timestamp / 1e6;
+    let durationInSeconds = duration / 1e6;
+    if (track.firstTimestamp === void 0)
+      track.firstTimestamp = timestampInSeconds;
+    timestampInSeconds = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, timestampInSeconds, track);
+    track.lastTimestamp = timestampInSeconds;
+    if (!track.currentChunk || timestampInSeconds - track.currentChunk.startTimestamp >= MAX_CHUNK_DURATION) {
+      if (track.currentChunk)
+        __privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, track);
+      track.currentChunk = {
+        startTimestamp: timestampInSeconds,
+        sampleData: [],
+        sampleCount: 0
+      };
+    }
+    track.currentChunk.sampleData.push(data);
+    track.currentChunk.sampleCount++;
+    if ((_a = meta == null ? void 0 : meta.decoderConfig) == null ? void 0 : _a.description) {
+      track.codecPrivate = new Uint8Array(meta.decoderConfig.description);
+    }
+    track.samples.push({
+      timestamp: timestampInSeconds,
+      duration: durationInSeconds,
+      size: data.byteLength,
+      type
+    });
+    if (track.lastTimescaleUnits !== null) {
+      let timescaleUnits = intoTimescale(timestampInSeconds, track.timescale, false);
+      let delta = Math.round(timescaleUnits - track.lastTimescaleUnits);
+      track.lastTimescaleUnits += delta;
+      let lastTableEntry = last(track.timeToSampleTable);
+      if (lastTableEntry.sampleCount === 1) {
+        lastTableEntry.sampleDelta = delta;
+        lastTableEntry.sampleCount++;
+      } else if (lastTableEntry.sampleDelta === delta) {
+        lastTableEntry.sampleCount++;
+      } else {
+        lastTableEntry.sampleCount--;
+        track.timeToSampleTable.push({
+          sampleCount: 2,
+          sampleDelta: delta
+        });
+      }
+    } else {
+      track.lastTimescaleUnits = 0;
+      track.timeToSampleTable.push({
+        sampleCount: 1,
+        sampleDelta: intoTimescale(durationInSeconds, track.timescale)
+      });
+    }
+  };
+  _validateTimestamp = /* @__PURE__ */ new WeakSet();
+  validateTimestamp_fn = function(timestamp, track) {
+    if (__privateGet(this, _options).firstTimestampBehavior === "strict" && track.lastTimestamp === -1 && timestamp !== 0) {
+      throw new Error(
+        `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want.
+
+If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options.
+`
+      );
+    } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") {
+      timestamp -= track.firstTimestamp;
+    }
+    if (timestamp < track.lastTimestamp) {
+      throw new Error(
+        `Timestamps must be monotonically increasing (went from ${track.lastTimestamp * 1e6} to ${timestamp * 1e6}).`
+      );
+    }
+    return timestamp;
+  };
+  _writeCurrentChunk = /* @__PURE__ */ new WeakSet();
+  writeCurrentChunk_fn = function(track) {
+    if (!track.currentChunk)
+      return;
+    track.currentChunk.offset = __privateGet(this, _writer).pos;
+    for (let bytes2 of track.currentChunk.sampleData)
+      __privateGet(this, _writer).write(bytes2);
+    track.currentChunk.sampleData = null;
+    if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.sampleCount) {
+      track.compactlyCodedChunkTable.push({
+        firstChunk: track.writtenChunks.length + 1,
+        // 1-indexed
+        samplesPerChunk: track.currentChunk.sampleCount
+      });
+    }
+    track.writtenChunks.push(track.currentChunk);
+    __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
+  };
+  _maybeFlushStreamingTargetWriter = /* @__PURE__ */ new WeakSet();
+  maybeFlushStreamingTargetWriter_fn = function() {
+    if (__privateGet(this, _writer) instanceof StreamTargetWriter) {
+      __privateGet(this, _writer).flush();
+    }
+  };
+  _ensureNotFinalized = /* @__PURE__ */ new WeakSet();
+  ensureNotFinalized_fn = function() {
+    if (__privateGet(this, _finalized)) {
+      throw new Error("Cannot add new video or audio chunks after the file has been finalized.");
+    }
+  };
+
+  // src/muxers/mp4muxer.ts
+  var Mp4MuxerWrapper = class {
+    constructor(config, postMessageCallback, options) {
+      this.videoConfigured = false;
+      this.audioConfigured = false;
+      this.firstAudioTimestamp = null;
+      this.firstVideoTimestamp = null;
+      this.firstTimestamp = null;
+      this.config = config;
+      this.postMessageToMain = postMessageCallback;
+      let disableAudio = options?.disableAudio ?? false;
+      const videoDisabled = config.width <= 0 || config.height <= 0 || config.videoBitrate <= 0;
+      const audioCodecOption = config.codec?.audio ?? "aac";
+      const supportedAudioCodecsForMp4 = /* @__PURE__ */ new Set(["aac", "mp3"]);
+      if (!supportedAudioCodecsForMp4.has(audioCodecOption)) {
+        console.warn(
+          `MP4 muxer: Audio codec ${audioCodecOption} is not supported. Disabling audio track.`
+        );
+        disableAudio = true;
+      }
+      const muxerAudioCodec = audioCodecOption;
+      const commonMuxerOptions = {};
+      if (!videoDisabled) {
+        const videoCodecOption = config.codec?.video ?? "avc";
+        let muxerVideoCodec;
+        switch (videoCodecOption) {
+          case "hevc":
+            muxerVideoCodec = "hevc";
+            break;
+          case "vp9":
+            muxerVideoCodec = "vp9";
+            break;
+          case "av1":
+            muxerVideoCodec = "av1";
+            break;
+          case "avc":
+          default:
+            muxerVideoCodec = "avc";
+            break;
+        }
+        commonMuxerOptions.video = {
+          codec: muxerVideoCodec,
+          width: config.width,
+          height: config.height
+          // framerate is not directly a muxer option here, but good to have in config
+        };
+      }
+      if (!disableAudio) {
+        commonMuxerOptions.audio = {
+          codec: muxerAudioCodec,
+          sampleRate: config.sampleRate,
+          numberOfChannels: config.channels
+        };
+      }
+      if (config.latencyMode === "realtime") {
+        this.target = new StreamTarget({
+          onData: (chunk, position) => {
+            const chunkCopy = new Uint8Array(chunk.slice(0));
+            const isHeader = position === 0;
+            const message = {
+              type: "dataChunk",
+              chunk: chunkCopy,
+              offset: position,
+              // Use position as offset
+              isHeader,
+              container: "mp4"
+            };
+            this.postMessageToMain(message, [chunkCopy.buffer]);
+          }
+        });
+        this.muxer = new Muxer({
+          target: this.target,
+          ...commonMuxerOptions,
+          fastStart: "fragmented"
+        });
+      } else {
+        this.target = new ArrayBufferTarget();
+        this.muxer = new Muxer({
+          target: this.target,
+          ...commonMuxerOptions,
+          fastStart: "in-memory"
+          // or false, depending on desired behavior for non-realtime
+        });
+      }
+      this.videoConfigured = !videoDisabled;
+      this.audioConfigured = !disableAudio;
+    }
+    addVideoChunk(chunk, meta) {
+      if (!this.videoConfigured) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: "MP4: Video track not configured.",
+            type: "configuration-error" /* ConfigurationError */
+          }
+        });
+        return;
+      }
+      try {
+        let adjustedChunk = chunk;
+        const adjustedMeta = meta;
+        if (this.config.firstTimestampBehavior === "offset" && typeof chunk.timestamp === "number") {
+          if (this.firstVideoTimestamp === null) {
+            this.firstVideoTimestamp = chunk.timestamp;
+            if (this.firstTimestamp === null) {
+              this.firstTimestamp = chunk.timestamp;
+            } else {
+              this.firstTimestamp = Math.min(
+                this.firstTimestamp,
+                chunk.timestamp
+              );
+            }
+          }
+          const newTimestamp = Math.max(
+            0,
+            chunk.timestamp - (this.firstTimestamp || 0)
+          );
+          const data = new Uint8Array(chunk.byteLength);
+          chunk.copyTo(data.buffer);
+          chunk.close?.();
+          adjustedChunk = new EncodedVideoChunk({
+            type: chunk.type,
+            timestamp: newTimestamp,
+            duration: chunk.duration ?? void 0,
+            data: data.buffer
+          });
+        } else if (typeof chunk.timestamp !== "number" && this.config.firstTimestampBehavior === "offset") {
+        }
+        this.muxer.addVideoChunk(adjustedChunk, adjustedMeta);
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `MP4: Error adding video chunk: ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+      }
+    }
+    addAudioChunk(chunk, meta) {
+      if (!this.audioConfigured) {
+        return;
+      }
+      try {
+        let adjustedChunk = chunk;
+        const adjustedMeta = meta;
+        if (this.config.firstTimestampBehavior === "offset" && typeof chunk.timestamp === "number") {
+          if (this.firstAudioTimestamp === null) {
+            this.firstAudioTimestamp = chunk.timestamp;
+            if (this.firstTimestamp === null) {
+              this.firstTimestamp = chunk.timestamp;
+            } else {
+              this.firstTimestamp = Math.min(
+                this.firstTimestamp,
+                chunk.timestamp
+              );
+            }
+          }
+          const newTimestamp = Math.max(
+            0,
+            chunk.timestamp - (this.firstTimestamp || 0)
+          );
+          const data = new Uint8Array(chunk.byteLength);
+          chunk.copyTo(data.buffer);
+          chunk.close?.();
+          adjustedChunk = new EncodedAudioChunk({
+            type: chunk.type,
+            timestamp: newTimestamp,
+            duration: chunk.duration ?? void 0,
+            data: data.buffer
+          });
+        } else if (typeof chunk.timestamp !== "number" && this.config.firstTimestampBehavior === "offset") {
+        }
+        this.muxer.addAudioChunk(adjustedChunk, adjustedMeta);
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `MP4: Error adding audio chunk: ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+      }
+    }
+    finalize() {
+      if (this.config.latencyMode === "realtime") {
+        try {
+          this.muxer.finalize();
+        } catch (e) {
+          this.postMessageToMain({
+            type: "error",
+            errorDetail: {
+              message: `MP4: Error finalizing muxer (realtime): ${e.message}`,
+              type: "muxing-failed" /* MuxingFailed */,
+              stack: e.stack
+            }
+          });
+        }
+        return null;
+      }
+      if (!(this.target instanceof ArrayBufferTarget)) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: "MP4: Muxer target is not ArrayBufferTarget in non-realtime mode.",
+            type: "unknown" /* Unknown */
+          }
+        });
+        return null;
+      }
+      try {
+        this.muxer.finalize();
+        const buffer = this.target.buffer;
+        this.target = new ArrayBufferTarget();
+        return new Uint8Array(buffer);
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `MP4: Error finalizing muxer (non-realtime): ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+        return null;
+      }
+    }
+  };
+
+  // src/muxers/webmmuxer.ts
+  var import_webm_muxer = __toESM(require_webm_muxer(), 1);
+  var CallbackWritableStream = class {
+    constructor(onData) {
+      this.onData = onData;
+      this.position = 0;
+    }
+    write({ data, position }) {
+      this.onData(data, position);
+      this.position = position + data.byteLength;
+    }
+  };
+  var WebMMuxerWrapper = class {
+    constructor(config, postMessageCallback, options) {
+      this.videoConfigured = false;
+      this.audioConfigured = false;
+      this.firstAudioTimestamp = null;
+      this.firstVideoTimestamp = null;
+      this.firstTimestamp = null;
+      this.config = config;
+      this.postMessageToMain = postMessageCallback;
+      let disableAudio = options?.disableAudio ?? false;
+      const videoCodecOption = config.codec?.video ?? "vp9";
+      let muxerVideoCodec;
+      switch (videoCodecOption) {
+        case "vp8":
+          muxerVideoCodec = "V_VP8";
+          break;
+        case "vp9":
+          muxerVideoCodec = "V_VP9";
+          break;
+        case "av1":
+          muxerVideoCodec = "V_AV1";
+          break;
+        default:
+          muxerVideoCodec = "V_VP9";
+          break;
+      }
+      const requestedAudioCodec = config.codec?.audio ?? "opus";
+      let muxerAudioCodec = null;
+      switch (requestedAudioCodec) {
+        case "opus":
+          muxerAudioCodec = "A_OPUS";
+          break;
+        case "vorbis":
+          muxerAudioCodec = "A_VORBIS";
+          break;
+        case "flac":
+          muxerAudioCodec = "A_FLAC";
+          break;
+        default:
+          if (!disableAudio) {
+            console.warn(
+              `WebM muxer: Audio codec ${requestedAudioCodec} is not supported. Disabling audio track.`
+            );
+            disableAudio = true;
+          }
+          break;
+      }
+      const target = config.latencyMode === "realtime" ? new CallbackWritableStream((chunk, position) => {
+        const chunkCopy = new Uint8Array(chunk.slice(0));
+        const isHeader = position === 0;
+        const message = {
+          type: "dataChunk",
+          chunk: chunkCopy,
+          offset: position,
+          isHeader,
+          container: "webm"
+        };
+        this.postMessageToMain(message, [chunkCopy.buffer]);
+      }) : "buffer";
+      const videoDisabled = config.width === 0 || config.height === 0 || config.videoBitrate === 0;
+      const optionsForMuxer = {
+        target
+      };
+      if (!videoDisabled) {
+        optionsForMuxer.video = {
+          codec: muxerVideoCodec,
+          width: config.width,
+          height: config.height,
+          frameRate: config.frameRate
+        };
+      }
+      if (!disableAudio && muxerAudioCodec) {
+        optionsForMuxer.audio = {
+          codec: muxerAudioCodec,
+          numberOfChannels: config.channels,
+          sampleRate: config.sampleRate
+        };
+      }
+      this.muxer = new import_webm_muxer.default(optionsForMuxer);
+      this.videoConfigured = !videoDisabled;
+      this.audioConfigured = !disableAudio;
+    }
+    addVideoChunk(chunk, meta) {
+      if (!this.videoConfigured) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: "WebM: Video track not configured.",
+            type: "configuration-error" /* ConfigurationError */
+          }
+        });
+        return;
+      }
+      try {
+        let adjustedChunk = chunk;
+        const adjustedMeta = meta;
+        if (this.config.firstTimestampBehavior === "offset" && typeof chunk.timestamp === "number") {
+          if (this.firstVideoTimestamp === null) {
+            this.firstVideoTimestamp = chunk.timestamp;
+            if (this.firstTimestamp === null) {
+              this.firstTimestamp = chunk.timestamp;
+            } else {
+              this.firstTimestamp = Math.min(
+                this.firstTimestamp,
+                chunk.timestamp
+              );
+            }
+          }
+          const newTimestamp = Math.max(
+            0,
+            chunk.timestamp - (this.firstTimestamp || 0)
+          );
+          const data = new Uint8Array(chunk.byteLength);
+          chunk.copyTo(data.buffer);
+          chunk.close?.();
+          adjustedChunk = new EncodedVideoChunk({
+            type: chunk.type,
+            timestamp: newTimestamp,
+            duration: chunk.duration ?? void 0,
+            data: data.buffer
+          });
+        }
+        this.muxer.addVideoChunk(adjustedChunk, adjustedMeta);
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `WebM: Error adding video chunk: ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+      }
+    }
+    addAudioChunk(chunk, meta) {
+      if (!this.audioConfigured) return;
+      try {
+        let adjustedChunk = chunk;
+        const adjustedMeta = meta;
+        if (this.config.firstTimestampBehavior === "offset" && typeof chunk.timestamp === "number") {
+          if (this.firstAudioTimestamp === null) {
+            this.firstAudioTimestamp = chunk.timestamp;
+            if (this.firstTimestamp === null) {
+              this.firstTimestamp = chunk.timestamp;
+            } else {
+              this.firstTimestamp = Math.min(
+                this.firstTimestamp,
+                chunk.timestamp
+              );
+            }
+          }
+          const newTimestamp = Math.max(
+            0,
+            chunk.timestamp - (this.firstTimestamp || 0)
+          );
+          const data = new Uint8Array(chunk.byteLength);
+          chunk.copyTo(data.buffer);
+          chunk.close?.();
+          adjustedChunk = new EncodedAudioChunk({
+            type: chunk.type,
+            timestamp: newTimestamp,
+            duration: chunk.duration ?? void 0,
+            data: data.buffer
+          });
+        }
+        this.muxer.addAudioChunk(adjustedChunk, adjustedMeta);
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `WebM: Error adding audio chunk: ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+      }
+    }
+    finalize() {
+      if (this.config.latencyMode === "realtime") {
+        try {
+          this.muxer.finalize();
+        } catch (e) {
+          this.postMessageToMain({
+            type: "error",
+            errorDetail: {
+              message: `WebM: Error finalizing muxer (realtime): ${e.message}`,
+              type: "muxing-failed" /* MuxingFailed */,
+              stack: e.stack
+            }
+          });
+        }
+        return null;
+      }
+      try {
+        const buffer = this.muxer.finalize();
+        if (buffer) return new Uint8Array(buffer);
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: "WebM: Muxer finalized without output in non-realtime mode.",
+            type: "muxing-failed" /* MuxingFailed */
+          }
+        });
+        return null;
+      } catch (e) {
+        this.postMessageToMain({
+          type: "error",
+          errorDetail: {
+            message: `WebM: Error finalizing muxer (non-realtime): ${e.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: e.stack
+          }
+        });
+        return null;
+      }
+    }
+  };
+
+  // src/worker/encoder-worker.ts
+  if (typeof self !== "undefined" && typeof self.addEventListener === "function") {
+    self.addEventListener("error", (event) => {
+      console.error("Unhandled global error in worker. Event:", event);
+      const message = event.message || `Unhandled global error${event.filename ? ` at ${event.filename}` : ""}`;
+      self.postMessage({
+        type: "error",
+        errorDetail: {
+          message,
+          type: "worker-error" /* WorkerError */,
+          stack: event.error?.stack
+        }
+      });
+    });
+  }
+  if (typeof self !== "undefined" && typeof self.addEventListener === "function") {
+    self.addEventListener(
+      "unhandledrejection",
+      (event) => {
+        console.error(
+          "Unhandled promise rejection in worker. Reason:",
+          event.reason
+        );
+        const reason = event.reason;
+        const message = reason instanceof Error ? `Unhandled promise rejection: ${reason.message}` : `Unhandled promise rejection: ${String(reason)}`;
+        self.postMessage({
+          type: "error",
+          errorDetail: {
+            message,
+            type: "worker-error" /* WorkerError */,
+            stack: reason instanceof Error ? reason.stack : void 0
+          }
+        });
+      }
+    );
+  }
+  var getVideoEncoder = () => self.VideoEncoder ?? globalThis.VideoEncoder;
+  var getAudioEncoder = () => self.AudioEncoder ?? globalThis.AudioEncoder;
+  var getAudioData = () => self.AudioData ?? globalThis.AudioData;
+  var EncoderWorker = class {
+    constructor() {
+      this.videoEncoder = null;
+      this.audioEncoder = null;
+      this.muxer = null;
+      this.currentConfig = null;
+      this.processedFrames = 0;
+      this.videoFrameCount = 0;
+      this.isCancelled = false;
+    }
+    postMessageToMainThread(message, transfer) {
+      if (transfer && transfer.length > 0) {
+        self.postMessage(message, transfer);
+      } else {
+        self.postMessage(message);
+      }
+    }
+    defaultAvcCodecString(width, height, frameRate, profile) {
+      const mbPerSec = Math.ceil(width / 16) * Math.ceil(height / 16) * frameRate;
+      let level;
+      if (mbPerSec <= 108e3) level = 31;
+      else if (mbPerSec <= 216e3) level = 32;
+      else if (mbPerSec <= 245760) level = 40;
+      else if (mbPerSec <= 589824) level = 50;
+      else if (mbPerSec <= 983040) level = 51;
+      else level = 52;
+      const chosenProfile = profile ?? (width >= 1280 || height >= 720 ? "high" : "baseline");
+      const profileHex = chosenProfile === "high" ? "64" : chosenProfile === "main" ? "4d" : "42";
+      const levelHex = level.toString(16).padStart(2, "0");
+      return `avc1.${profileHex}00${levelHex}`;
+    }
+    getCodecString(codecType, width, height, frameRate) {
+      switch (codecType) {
+        case "avc":
+          return this.defaultAvcCodecString(width, height, frameRate);
+        case "vp9":
+          return "vp09.00.50.08";
+        case "vp8":
+          return "vp8";
+        case "hevc":
+          return "hvc1";
+        case "av1":
+          return "av01.0.04M.08";
+        default:
+          return codecType;
+      }
+    }
+    async isConfigSupportedWithHwFallback(Ctor, config, label) {
+      let support = await Ctor.isConfigSupported(config);
+      if (support?.supported && support.config) return support.config;
+      const pref = config.hardwareAcceleration;
+      if (pref) {
+        let altPref;
+        if (pref === "prefer-hardware") altPref = "prefer-software";
+        else if (pref === "prefer-software") altPref = "prefer-hardware";
+        if (altPref) {
+          const opposite = { ...config, hardwareAcceleration: altPref };
+          support = await Ctor.isConfigSupported(opposite);
+          if (support?.supported && support.config) {
+            console.warn(
+              `${label}: hardwareAcceleration preference '${pref}' not supported. Using '${altPref}'.`
+            );
+            return support.config;
+          }
+        }
+        const noPref = { ...config };
+        delete noPref.hardwareAcceleration;
+        support = await Ctor.isConfigSupported(noPref);
+        if (support?.supported && support.config) {
+          console.warn(
+            `${label}: hardwareAcceleration preference '${pref}' not supported. Using no preference.`
+          );
+          return support.config;
+        }
+        console.warn(
+          `${label}: Failed to find a supported hardware acceleration configuration for codec ${config.codec}.`
+        );
+      }
+      return null;
+    }
+    postQueueSize() {
+      this.postMessageToMainThread({
+        type: "queueSize",
+        videoQueueSize: this.videoEncoder?.encodeQueueSize ?? 0,
+        audioQueueSize: this.audioEncoder?.encodeQueueSize ?? 0
+      });
+    }
+    async prepareAudioCodec(container, audioDisabled) {
+      if (audioDisabled) {
+        return {
+          audioDisabled: true,
+          selectedCodec: null,
+          finalConfig: null,
+          encoderCtor: null
+        };
+      }
+      const AudioEncoderCtor = getAudioEncoder();
+      if (!AudioEncoderCtor) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: "Worker: AudioEncoder not available",
+            type: "not-supported" /* NotSupported */
+          }
+        });
+        return {
+          audioDisabled: true,
+          selectedCodec: null,
+          finalConfig: null,
+          encoderCtor: null
+        };
+      }
+      const config = this.currentConfig;
+      if (!config) {
+        return {
+          audioDisabled: true,
+          selectedCodec: null,
+          finalConfig: null,
+          encoderCtor: AudioEncoderCtor
+        };
+      }
+      const requestedCodec = config.codec?.audio;
+      const requestedCodecString = config.codecString?.audio;
+      const preference = buildAudioCodecPreference(container, requestedCodec);
+      let attemptedDefaultCodec = false;
+      for (const candidate of preference) {
+        if (candidate === "aac") {
+          attemptedDefaultCodec = true;
+        }
+        const codecString = requestedCodec && requestedCodecString && candidate === requestedCodec ? requestedCodecString : getAudioEncoderCodecStringFromAudioCodec(candidate);
+        const baseConfig = {
+          codec: codecString,
+          sampleRate: config.sampleRate,
+          numberOfChannels: config.channels,
+          bitrate: config.audioBitrate,
+          ...config.audioBitrateMode && {
+            bitrateMode: config.audioBitrateMode
+          },
+          ...config.hardwareAcceleration && {
+            hardwareAcceleration: config.hardwareAcceleration
+          },
+          ...getAudioEncoderConfigOverridesForCodec(
+            candidate,
+            config.audioEncoderConfig
+          )
+        };
+        const support = await this.isConfigSupportedWithHwFallback(
+          AudioEncoderCtor,
+          baseConfig,
+          "AudioEncoder"
+        );
+        if (!support) {
+          if (candidate === "aac" && container === "mp4") {
+            console.warn(
+              "Worker: AAC audio codec is not supported. Falling back to MP3."
+            );
+            const mp3AttemptConfig = {
+              codec: getAudioEncoderCodecStringFromAudioCodec("mp3"),
+              sampleRate: config.sampleRate,
+              numberOfChannels: config.channels,
+              bitrate: config.audioBitrate,
+              ...config.audioBitrateMode && {
+                bitrateMode: config.audioBitrateMode
+              },
+              ...config.hardwareAcceleration && {
+                hardwareAcceleration: config.hardwareAcceleration
+              },
+              ...getAudioEncoderConfigOverridesForCodec(
+                "mp3",
+                config.audioEncoderConfig
+              )
+            };
+            const mp3Support = await this.isConfigSupportedWithHwFallback(
+              AudioEncoderCtor,
+              mp3AttemptConfig,
+              "AudioEncoder"
+            );
+            if (mp3Support) {
+              const resolvedMp3Config = mp3Support;
+              if (resolvedMp3Config.numberOfChannels !== void 0 && resolvedMp3Config.numberOfChannels !== config.channels) {
+                this.postMessageToMainThread({
+                  type: "error",
+                  errorDetail: {
+                    message: `AudioEncoder reported numberOfChannels (${resolvedMp3Config.numberOfChannels}) does not match configured channels (${config.channels}).`,
+                    type: "configuration-error" /* ConfigurationError */
+                  }
+                });
+                return {
+                  audioDisabled: true,
+                  selectedCodec: null,
+                  finalConfig: null,
+                  encoderCtor: AudioEncoderCtor
+                };
+              }
+              if (resolvedMp3Config.sampleRate !== void 0 && resolvedMp3Config.sampleRate !== config.sampleRate) {
+                this.postMessageToMainThread({
+                  type: "error",
+                  errorDetail: {
+                    message: `AudioEncoder reported sampleRate (${resolvedMp3Config.sampleRate}) does not match configured sampleRate (${config.sampleRate}).`,
+                    type: "configuration-error" /* ConfigurationError */
+                  }
+                });
+                return {
+                  audioDisabled: true,
+                  selectedCodec: null,
+                  finalConfig: null,
+                  encoderCtor: AudioEncoderCtor
+                };
+              }
+              if (!isAudioCodecMuxerCompatible(container, "mp3")) {
+                console.warn(
+                  "Worker: Audio codec mp3 is not compatible with MP4 muxer. Audio will be disabled."
+                );
+              } else {
+                console.warn("Worker: Falling back to MP3 for MP4 container.");
+                return {
+                  audioDisabled: false,
+                  selectedCodec: "mp3",
+                  finalConfig: resolvedMp3Config,
+                  encoderCtor: AudioEncoderCtor
+                };
+              }
+            }
+          } else {
+            console.warn(
+              `Worker: Audio codec ${candidate} not supported or config invalid.`
+            );
+          }
+          continue;
+        }
+        const resolvedConfig = support;
+        if (resolvedConfig.numberOfChannels !== void 0 && resolvedConfig.numberOfChannels !== config.channels) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: `AudioEncoder reported numberOfChannels (${resolvedConfig.numberOfChannels}) does not match configured channels (${config.channels}).`,
+              type: "configuration-error" /* ConfigurationError */
+            }
+          });
+          return {
+            audioDisabled: true,
+            selectedCodec: null,
+            finalConfig: null,
+            encoderCtor: AudioEncoderCtor
+          };
+        }
+        if (resolvedConfig.sampleRate !== void 0 && resolvedConfig.sampleRate !== config.sampleRate) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: `AudioEncoder reported sampleRate (${resolvedConfig.sampleRate}) does not match configured sampleRate (${config.sampleRate}).`,
+              type: "configuration-error" /* ConfigurationError */
+            }
+          });
+          return {
+            audioDisabled: true,
+            selectedCodec: null,
+            finalConfig: null,
+            encoderCtor: AudioEncoderCtor
+          };
+        }
+        if (!isAudioCodecMuxerCompatible(container, candidate)) {
+          console.warn(
+            `Worker: Audio codec ${candidate} is not compatible with ${container.toUpperCase()} muxer. Trying fallback codec.`
+          );
+          continue;
+        }
+        if (candidate === "aac") {
+          attemptedDefaultCodec = true;
+          if (container === "mp4" && requestedCodec && requestedCodec !== "aac") {
+            console.warn("Worker: Falling back to AAC for MP4 container.");
+          }
+        }
+        if (container === "mp4" && candidate === "mp3" && attemptedDefaultCodec) {
+          console.warn("Worker: Falling back to MP3 for MP4 container.");
+        }
+        return {
+          audioDisabled: false,
+          selectedCodec: candidate,
+          finalConfig: resolvedConfig,
+          encoderCtor: AudioEncoderCtor
+        };
+      }
+      const defaultCodec = DEFAULT_AUDIO_CODEC_BY_CONTAINER[container];
+      const noCodecMessage = container === "mp4" ? "Worker: No supported audio codec (AAC, MP3) found for MP4 container." : `Worker: No supported audio codec found. Requested: ${requestedCodec ?? "(auto)"}. Tried: ${preference.join(", ")}.`;
+      this.postMessageToMainThread({
+        type: "error",
+        errorDetail: {
+          message: noCodecMessage,
+          type: "not-supported" /* NotSupported */
+        }
+      });
+      console.warn(
+        `Worker: Disabling audio. Consider using ${defaultCodec} for container ${container}.`
+      );
+      return {
+        audioDisabled: true,
+        selectedCodec: null,
+        finalConfig: null,
+        encoderCtor: AudioEncoderCtor
+      };
+    }
+    async initializeEncoders(data) {
+      this.currentConfig = data.config;
+      this.totalFramesToProcess = data.totalFrames;
+      this.processedFrames = 0;
+      this.videoFrameCount = 0;
+      this.isCancelled = false;
+      if (!this.currentConfig) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: "Worker: Configuration is missing.",
+            type: "initialization-failed" /* InitializationFailed */
+          }
+        });
+        return;
+      }
+      let audioDisabled = !this.currentConfig.audioBitrate || this.currentConfig.audioBitrate <= 0 || !this.currentConfig.channels || this.currentConfig.channels <= 0 || !this.currentConfig.sampleRate || this.currentConfig.sampleRate <= 0 || !this.currentConfig.codec?.audio;
+      const containerType = getContainerType(this.currentConfig.container);
+      const audioOriginallyDisabled = audioDisabled;
+      const audioPlan = await this.prepareAudioCodec(
+        containerType,
+        audioDisabled
+      );
+      audioDisabled = audioPlan.audioDisabled;
+      let selectedAudioCodec = audioPlan.selectedCodec;
+      let finalAudioEncoderConfig = audioPlan.finalConfig;
+      const preparedAudioEncoderCtor = audioPlan.encoderCtor;
+      if (audioDisabled) {
+        selectedAudioCodec = null;
+        finalAudioEncoderConfig = null;
+      }
+      if (!audioOriginallyDisabled && audioDisabled) {
+        this.cleanup();
+        return;
+      }
+      const videoDisabled = this.currentConfig.width === 0 || this.currentConfig.height === 0 || this.currentConfig.videoBitrate === 0;
+      let videoCodec = this.currentConfig.codec?.video ?? (this.currentConfig.container === "webm" ? "vp9" : "avc");
+      const requestedVideoCodec = videoCodec;
+      let finalVideoEncoderConfig = null;
+      let resolvedVideoCodecString = null;
+      let VideoEncoderCtor;
+      if (!videoDisabled) {
+        if (this.currentConfig.container === "webm" && (videoCodec === "avc" || videoCodec === "hevc")) {
+          console.warn(
+            `Worker: Video codec ${videoCodec} not compatible with WebM. Switching to VP9.`
+          );
+          videoCodec = "vp9";
+        }
+        resolvedVideoCodecString = (this.currentConfig.codecString?.video && videoCodec === requestedVideoCodec ? this.currentConfig.codecString.video : void 0) ?? (videoCodec === "avc" ? this.defaultAvcCodecString(
+          this.currentConfig.width,
+          this.currentConfig.height,
+          this.currentConfig.frameRate
+        ) : videoCodec === "vp9" ? "vp09.00.50.08" : videoCodec === "vp8" ? "vp8" : videoCodec === "hevc" ? "hvc1" : videoCodec === "av1" ? "av01.0.04M.08" : videoCodec);
+        const videoEncoderConfig = {
+          codec: resolvedVideoCodecString,
+          width: this.currentConfig.width,
+          height: this.currentConfig.height,
+          bitrate: this.currentConfig.videoBitrate,
+          framerate: this.currentConfig.frameRate,
+          ...this.currentConfig.container === "mp4" && videoCodec === "avc" ? { avc: { format: "avc" } } : {},
+          ...this.currentConfig.hardwareAcceleration ? { hardwareAcceleration: this.currentConfig.hardwareAcceleration } : {},
+          ...getVideoEncoderConfigOverridesForCodec(
+            videoCodec,
+            this.currentConfig.videoEncoderConfig
+          )
+        };
+        VideoEncoderCtor = getVideoEncoder();
+        if (!VideoEncoderCtor) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: "Worker: VideoEncoder not available",
+              type: "not-supported" /* NotSupported */
+            }
+          });
+          this.cleanup();
+          return;
+        }
+        const initialSupport = await VideoEncoderCtor.isConfigSupported(videoEncoderConfig);
+        if (initialSupport?.supported && initialSupport.config) {
+          finalVideoEncoderConfig = initialSupport.config;
+        } else {
+          if (videoCodec === "vp9" || videoCodec === "vp8" || videoCodec === "av1") {
+            console.warn(
+              "Worker: Video codec " + videoCodec + " not supported or config invalid. Looking for fallback..."
+            );
+            let fallbackSuccessful = false;
+            if (this.currentConfig.container === "webm") {
+              const webmCodecs = ["vp9", "vp8"];
+              for (const fallbackCodec of webmCodecs) {
+                if (fallbackCodec === videoCodec) continue;
+                console.warn(
+                  `Worker: Trying fallback to ${fallbackCodec} for WebM container.`
+                );
+                const fallbackCodecString = this.getCodecString(
+                  fallbackCodec,
+                  this.currentConfig.width,
+                  this.currentConfig.height,
+                  this.currentConfig.frameRate
+                );
+                const fallbackConfig = {
+                  ...videoEncoderConfig,
+                  codec: fallbackCodecString,
+                  ...getVideoEncoderConfigOverridesForCodec(
+                    fallbackCodec,
+                    this.currentConfig.videoEncoderConfig
+                  )
+                };
+                const support = await this.isConfigSupportedWithHwFallback(
+                  VideoEncoderCtor,
+                  fallbackConfig,
+                  "VideoEncoder"
+                );
+                if (support) {
+                  console.warn(
+                    `Worker: Successfully fell back to ${fallbackCodec} for WebM.`
+                  );
+                  videoCodec = fallbackCodec;
+                  finalVideoEncoderConfig = support;
+                  fallbackSuccessful = true;
+                  break;
+                }
+              }
+              if (!fallbackSuccessful) {
+                this.postMessageToMainThread({
+                  type: "error",
+                  errorDetail: {
+                    message: "Worker: No compatible video codec (VP9, VP8) found for WebM container.",
+                    type: "not-supported" /* NotSupported */
+                  }
+                });
+                this.cleanup();
+                return;
+              }
+            } else {
+              console.warn("Worker: Falling back to AVC for MP4 container.");
+              videoCodec = "avc";
+              const avcCodecString = this.defaultAvcCodecString(
+                this.currentConfig.width,
+                this.currentConfig.height,
+                this.currentConfig.frameRate
+              );
+              const avcConfig = {
+                ...videoEncoderConfig,
+                codec: avcCodecString,
+                ...this.currentConfig.container === "mp4" ? { avc: { format: "avc" } } : {},
+                ...getVideoEncoderConfigOverridesForCodec(
+                  "avc",
+                  this.currentConfig.videoEncoderConfig
+                )
+              };
+              const support = await this.isConfigSupportedWithHwFallback(
+                VideoEncoderCtor,
+                avcConfig,
+                "VideoEncoder"
+              );
+              if (support) {
+                finalVideoEncoderConfig = support;
+                fallbackSuccessful = true;
+              } else {
+                this.postMessageToMainThread({
+                  type: "error",
+                  errorDetail: {
+                    message: "Worker: AVC (H.264) video codec is not supported after fallback.",
+                    type: "not-supported" /* NotSupported */
+                  }
+                });
+                this.cleanup();
+                return;
+              }
+            }
+          } else {
+            const result = await this.isConfigSupportedWithHwFallback(
+              VideoEncoderCtor,
+              videoEncoderConfig,
+              "VideoEncoder"
+            );
+            if (result) {
+              finalVideoEncoderConfig = result;
+            } else {
+              this.postMessageToMainThread({
+                type: "error",
+                errorDetail: {
+                  message: `Worker: Video codec ${videoCodec} config not supported.`,
+                  type: "not-supported" /* NotSupported */
+                }
+              });
+              this.cleanup();
+              return;
+            }
+          }
+        }
+      } else {
+        videoCodec = void 0;
+      }
+      const codecConfigForMuxer = {
+        ...this.currentConfig.codec ?? {},
+        video: videoDisabled ? void 0 : videoCodec,
+        audio: audioDisabled || !selectedAudioCodec ? void 0 : selectedAudioCodec
+      };
+      this.currentConfig.codec = codecConfigForMuxer;
+      try {
+        const MuxerCtor = containerType === "webm" ? WebMMuxerWrapper : Mp4MuxerWrapper;
+        this.muxer = new MuxerCtor(
+          this.currentConfig,
+          this.postMessageToMainThread.bind(this),
+          {
+            disableAudio: audioDisabled
+          }
+        );
+      } catch (e) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Worker: Failed to initialize Muxer: ${e.message}`,
+            type: "initialization-failed" /* InitializationFailed */,
+            stack: e.stack
+          }
+        });
+        this.cleanup();
+        return;
+      }
+      if (!videoDisabled) {
+        try {
+          if (!VideoEncoderCtor) {
+            this.postMessageToMainThread({
+              type: "error",
+              errorDetail: {
+                message: "Worker: VideoEncoder not available",
+                type: "not-supported" /* NotSupported */
+              }
+            });
+            this.cleanup();
+            return;
+          }
+          this.videoEncoder = new VideoEncoderCtor({
+            output: (chunk, meta) => {
+              if (this.isCancelled || !this.muxer) return;
+              this.muxer.addVideoChunk(chunk, meta);
+            },
+            error: (error) => {
+              if (this.isCancelled) return;
+              this.postMessageToMainThread({
+                type: "error",
+                errorDetail: {
+                  message: `VideoEncoder error: ${error.message}`,
+                  type: "video-encoding-error" /* VideoEncodingError */,
+                  stack: error.stack
+                }
+              });
+              this.cleanup();
+            }
+          });
+          if (finalVideoEncoderConfig) {
+            if (this.videoEncoder) {
+              this.videoEncoder.configure(finalVideoEncoderConfig);
+            } else {
+              this.postMessageToMainThread({
+                type: "error",
+                errorDetail: {
+                  message: "Worker: VideoEncoder instance is null after creation.",
+                  type: "initialization-failed" /* InitializationFailed */
+                }
+              });
+              this.cleanup();
+              return;
+            }
+          } else {
+            this.postMessageToMainThread({
+              type: "error",
+              errorDetail: {
+                message: `Worker: VideoEncoder: Failed to find a supported hardware acceleration configuration for codec ${resolvedVideoCodecString ?? "(unknown)"}`,
+                type: "not-supported" /* NotSupported */
+              }
+            });
+            this.cleanup();
+            return;
+          }
+        } catch (e) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: `Worker: Failed to initialize VideoEncoder: ${e.message}`,
+              type: "initialization-failed" /* InitializationFailed */,
+              stack: e.stack
+            }
+          });
+          this.cleanup();
+          return;
+        }
+      }
+      if (!audioDisabled) {
+        if (!selectedAudioCodec || !finalAudioEncoderConfig || !preparedAudioEncoderCtor) {
+          this.cleanup();
+          return;
+        }
+        try {
+          this.audioEncoder = new preparedAudioEncoderCtor({
+            output: (chunk, meta) => {
+              if (this.isCancelled || !this.muxer) return;
+              this.muxer.addAudioChunk(chunk, meta);
+            },
+            error: (error) => {
+              if (this.isCancelled) return;
+              this.postMessageToMainThread({
+                type: "error",
+                errorDetail: {
+                  message: `AudioEncoder error: ${error.message}`,
+                  type: "audio-encoding-error" /* AudioEncodingError */,
+                  stack: error.stack
+                }
+              });
+              this.cleanup();
+            }
+          });
+          if (this.audioEncoder) {
+            this.audioEncoder.configure(finalAudioEncoderConfig);
+          } else {
+            this.postMessageToMainThread({
+              type: "error",
+              errorDetail: {
+                message: "Worker: AudioEncoder instance is null after creation.",
+                type: "initialization-failed" /* InitializationFailed */
+              }
+            });
+            this.cleanup();
+            return;
+          }
+        } catch (e) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: `Worker: Failed to initialize AudioEncoder: ${e.message}`,
+              type: "initialization-failed" /* InitializationFailed */,
+              stack: e.stack
+            }
+          });
+          this.cleanup();
+          return;
+        }
+      }
+      this.postMessageToMainThread({
+        type: "initialized",
+        actualVideoCodec: finalVideoEncoderConfig?.codec,
+        actualAudioCodec: audioDisabled ? null : finalAudioEncoderConfig?.codec
+      });
+      console.warn("Worker: Initialized successfully");
+    }
+    async handleAddVideoFrame(data) {
+      if (this.isCancelled || !this.videoEncoder || !this.currentConfig) return;
+      try {
+        const frame = data.frame;
+        const currentQueueSize = this.videoEncoder.encodeQueueSize;
+        const maxQueueSize = this.currentConfig.maxVideoQueueSize || 30;
+        const strategy = this.currentConfig.backpressureStrategy || "drop";
+        if (currentQueueSize >= maxQueueSize) {
+          if (strategy === "drop") {
+            console.warn(
+              `Video queue full (${currentQueueSize}/${maxQueueSize}), dropping frame`
+            );
+            try {
+              frame.close();
+            } catch (closeErr) {
+              console.warn(
+                "Worker: Ignored error closing dropped VideoFrame",
+                closeErr
+              );
+            }
+            return;
+          } else if (strategy === "wait") {
+            let waitTime = 10;
+            const maxWaitTime = 100;
+            const maxRetries = 5;
+            let attempts = 0;
+            while (this.videoEncoder.encodeQueueSize >= maxQueueSize && attempts < maxRetries) {
+              await new Promise((resolve) => setTimeout(resolve, waitTime));
+              waitTime = Math.min(waitTime * 1.5, maxWaitTime);
+              attempts++;
+            }
+            if (this.videoEncoder.encodeQueueSize >= maxQueueSize) {
+              console.warn(
+                `Video queue still full after waiting, dropping frame`
+              );
+              try {
+                frame.close();
+              } catch (closeErr) {
+                console.warn(
+                  "Worker: Ignored error closing waited VideoFrame",
+                  closeErr
+                );
+              }
+              return;
+            }
+          }
+        }
+        const interval = this.currentConfig.keyFrameInterval;
+        const opts = interval && this.videoFrameCount % interval === 0 ? { keyFrame: true } : void 0;
+        this.videoEncoder.encode(frame, opts);
+        try {
+          frame.close();
+        } catch (closeErr) {
+          console.warn("Worker: Ignored error closing VideoFrame", closeErr);
+        }
+        this.videoFrameCount++;
+        this.processedFrames++;
+        const progressMessage = {
+          type: "progress",
+          processedFrames: this.processedFrames
+        };
+        if (typeof this.totalFramesToProcess !== "undefined") {
+          progressMessage.totalFrames = this.totalFramesToProcess;
+        }
+        this.postMessageToMainThread(progressMessage);
+        this.postQueueSize();
+      } catch (error) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Error encoding video frame: ${error.message}`,
+            type: "video-encoding-error" /* VideoEncodingError */,
+            stack: error.stack
+          }
+        });
+        this.cleanup();
+      }
+    }
+    async handleAddAudioData(data) {
+      if (this.isCancelled || !this.audioEncoder || !this.currentConfig) return;
+      if (data.audio) {
+        const audioData = data.audio;
+        let audioClosed = false;
+        const closeAudioData = (context) => {
+          if (audioClosed) return;
+          try {
+            audioData.close();
+            audioClosed = true;
+          } catch (closeErr) {
+            console.warn(`Worker: Ignored error closing ${context}`, closeErr);
+          }
+        };
+        try {
+          const currentQueueSize = this.audioEncoder.encodeQueueSize;
+          const maxQueueSize = this.currentConfig.maxAudioQueueSize || 30;
+          const strategy = this.currentConfig.backpressureStrategy || "drop";
+          if (currentQueueSize >= maxQueueSize) {
+            if (strategy === "drop") {
+              console.warn(
+                `Audio queue full (${currentQueueSize}/${maxQueueSize}), dropping audio data`
+              );
+              closeAudioData("dropped AudioData");
+              return;
+            } else if (strategy === "wait") {
+              let waitTime = 10;
+              const maxWaitTime = 100;
+              const maxRetries = 5;
+              let attempts = 0;
+              while (this.audioEncoder.encodeQueueSize >= maxQueueSize && attempts < maxRetries) {
+                await new Promise((resolve) => setTimeout(resolve, waitTime));
+                waitTime = Math.min(waitTime * 1.5, maxWaitTime);
+                attempts++;
+              }
+              if (this.audioEncoder.encodeQueueSize >= maxQueueSize) {
+                console.warn(
+                  `Audio queue still full after waiting, dropping audio data`
+                );
+                closeAudioData("waited AudioData");
+                return;
+              }
+            }
+          }
+          this.audioEncoder.encode(audioData);
+          this.postQueueSize();
+        } catch (error) {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: `Error encoding audio data: ${error.message}`,
+              type: "audio-encoding-error" /* AudioEncodingError */,
+              stack: error.stack
+            }
+          });
+          this.cleanup();
+        } finally {
+          closeAudioData("AudioData");
+        }
+        return;
+      }
+      if (!data.audioData || data.audioData.length === 0) return;
+      if (data.audioData.length !== this.currentConfig.channels) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Audio data channel count (${data.audioData.length}) does not match configured channels (${this.currentConfig.channels}).`,
+            type: "configuration-error" /* ConfigurationError */
+          }
+        });
+        return;
+      }
+      const AudioDataCtor = getAudioData();
+      if (!AudioDataCtor) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: "Worker: AudioData not available",
+            type: "not-supported" /* NotSupported */
+          }
+        });
+        this.cleanup();
+        return;
+      }
+      try {
+        const interleaveFloat32Arrays = (planarArrays) => {
+          if (!planarArrays || planarArrays.length === 0) {
+            return new Float32Array(0);
+          }
+          const numChannels = planarArrays.length;
+          const numFrames = Math.min(...planarArrays.map((arr) => arr.length));
+          const interleaved = new Float32Array(numFrames * numChannels);
+          for (let i = 0; i < numFrames; i++) {
+            for (let ch = 0; ch < numChannels; ch++) {
+              interleaved[i * numChannels + ch] = planarArrays[ch][i];
+            }
+          }
+          return interleaved;
+        };
+        const interleavedData = interleaveFloat32Arrays(data.audioData);
+        const audioData = new AudioDataCtor({
+          format: "f32",
+          sampleRate: data.sampleRate,
+          numberOfFrames: data.numberOfFrames,
+          numberOfChannels: data.numberOfChannels,
+          timestamp: data.timestamp,
+          data: interleavedData.buffer
+        });
+        try {
+          this.audioEncoder.encode(audioData);
+          this.postQueueSize();
+        } finally {
+          audioData.close();
+        }
+      } catch (error) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Error encoding audio data: ${error.message}`,
+            type: "audio-encoding-error" /* AudioEncodingError */,
+            stack: error.stack
+          }
+        });
+        this.cleanup();
+      }
+    }
+    async handleFinalize(_message) {
+      if (this.isCancelled) return;
+      try {
+        if (this.videoEncoder) await this.videoEncoder.flush();
+        if (this.audioEncoder) await this.audioEncoder.flush();
+        if (this.muxer) {
+          const uint8ArrayOrNullOutput = this.muxer.finalize();
+          if (uint8ArrayOrNullOutput) {
+            this.postMessageToMainThread(
+              { type: "finalized", output: uint8ArrayOrNullOutput },
+              [uint8ArrayOrNullOutput.buffer]
+            );
+          } else if (this.currentConfig?.latencyMode === "realtime") {
+            this.postMessageToMainThread({ type: "finalized", output: null });
+          } else {
+            this.postMessageToMainThread({
+              type: "error",
+              errorDetail: {
+                message: "Muxer finalized without output in non-realtime mode.",
+                type: "muxing-failed" /* MuxingFailed */
+              }
+            });
+          }
+        } else {
+          this.postMessageToMainThread({
+            type: "error",
+            errorDetail: {
+              message: "Muxer not initialized during finalize.",
+              type: "muxing-failed" /* MuxingFailed */
+            }
+          });
+        }
+      } catch (error) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Error during finalization: ${error.message}`,
+            type: "muxing-failed" /* MuxingFailed */,
+            stack: error.stack
+          }
+        });
+      } finally {
+        this.cleanup();
+      }
+    }
+    handleCancel(_message) {
+      if (this.isCancelled) return;
+      this.isCancelled = true;
+      console.warn("Worker: Received cancel signal.");
+      this.postMessageToMainThread({ type: "cancelled" });
+      this.videoEncoder?.close();
+      this.audioEncoder?.close();
+      this.cleanup();
+    }
+    cleanup() {
+      console.warn("Worker: Cleaning up resources.");
+      if (this.videoEncoder && this.videoEncoder.state !== "closed")
+        this.videoEncoder.close();
+      if (this.audioEncoder && this.audioEncoder.state !== "closed")
+        this.audioEncoder.close();
+      this.videoEncoder = null;
+      this.audioEncoder = null;
+      this.muxer = null;
+      this.currentConfig = null;
+      this.totalFramesToProcess = void 0;
+      this.processedFrames = 0;
+      this.videoFrameCount = 0;
+    }
+    async handleMessage(eventData) {
+      if (this.isCancelled && eventData.type !== "initialize" && eventData.type !== "cancel") {
+        console.warn(
+          `Worker: Ignoring message type '${eventData.type}' because worker is cancelled.`
+        );
+        return;
+      }
+      try {
+        switch (eventData.type) {
+          case "initialize":
+            this.isCancelled = false;
+            this.cleanup();
+            await this.initializeEncoders(eventData);
+            break;
+          case "addVideoFrame":
+            await this.handleAddVideoFrame(eventData);
+            break;
+          case "addAudioData":
+            await this.handleAddAudioData(eventData);
+            break;
+          case "finalize":
+            await this.handleFinalize(eventData);
+            break;
+          case "cancel":
+            this.handleCancel(eventData);
+            break;
+          default:
+            console.warn(
+              "Worker received unknown message type:",
+              eventData.type
+            );
+        }
+      } catch (error) {
+        this.postMessageToMainThread({
+          type: "error",
+          errorDetail: {
+            message: `Unhandled error in worker onmessage: ${error.message}`,
+            type: "unknown" /* Unknown */,
+            stack: error.stack
+          }
+        });
+        this.cleanup();
+      }
+    }
+  };
+  var encoder = new EncoderWorker();
+  self.onmessage = async (event) => {
+    await encoder.handleMessage(event.data);
+  };
+  var DEFAULT_AUDIO_CODEC_BY_CONTAINER = {
+    mp4: "aac",
+    webm: "opus"
+  };
+  var AUDIO_ENCODER_CODEC_MAP = {
+    aac: "mp4a.40.2",
+    opus: "opus",
+    flac: "flac",
+    mp3: "mp3",
+    vorbis: "vorbis",
+    pcm: "pcm",
+    ulaw: "ulaw",
+    alaw: "alaw"
+  };
+  var MUXER_COMPATIBLE_AUDIO = {
+    mp4: /* @__PURE__ */ new Set(["aac", "mp3"]),
+    webm: /* @__PURE__ */ new Set(["opus", "vorbis", "flac"])
+  };
+  function getAudioEncoderCodecStringFromAudioCodec(codec) {
+    return AUDIO_ENCODER_CODEC_MAP[codec] ?? codec;
+  }
+  function getVideoEncoderConfigOverridesForCodec(codec, overrides) {
+    if (!overrides) {
+      return {};
+    }
+    const sanitized = { ...overrides };
+    if (codec !== "avc") {
+      delete sanitized.avc;
+    }
+    if (codec !== "hevc") {
+      delete sanitized.hevc;
+    }
+    return sanitized;
+  }
+  function getAudioEncoderConfigOverridesForCodec(codec, overrides) {
+    if (!overrides) {
+      return {};
+    }
+    const sanitized = { ...overrides };
+    if (codec !== "aac") {
+      delete sanitized.aac;
+    }
+    return sanitized;
+  }
+  function getContainerType(container) {
+    return container === "webm" ? "webm" : "mp4";
+  }
+  function buildAudioCodecPreference(container, requested) {
+    const preference = [];
+    const addCodec = (codec) => {
+      if (!preference.includes(codec)) {
+        preference.push(codec);
+      }
+    };
+    if (requested) {
+      addCodec(requested);
+    }
+    addCodec(DEFAULT_AUDIO_CODEC_BY_CONTAINER[container]);
+    for (const codec of MUXER_COMPATIBLE_AUDIO[container]) {
+      addCodec(codec);
+    }
+    if (container === "mp4") {
+      addCodec("aac");
+      addCodec("mp3");
+    } else {
+      addCodec("opus");
+      addCodec("vorbis");
+      addCodec("flac");
+      addCodec("aac");
+    }
+    return preference;
+  }
+  function isAudioCodecMuxerCompatible(container, codec) {
+    return MUXER_COMPATIBLE_AUDIO[container].has(codec);
+  }
+})();
+//# sourceMappingURL=worker.js.map

+ 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',
+)

+ 9 - 6
src/App.vue

@@ -188,24 +188,27 @@ window.addEventListener('beforeunload', () => {
 .image-preview__toolbar button {
   padding: 8px 12px;
   border: none;
-  background: linear-gradient(180deg, #3a8bff 0%, #2f80ed 100%);
+  // background: linear-gradient(180deg, #ff9300 0%, #2f80ed 100%);
+  background: #ff9300;
   color: #fff;
   border-radius: 10px;
   cursor: pointer;
   transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
-  box-shadow: 0 2px 8px rgba(47, 128, 237, 0.3);
+  // box-shadow: 0 2px 8px rgba(47, 128, 237, 0.3);
 }
 .image-preview__toolbar button:hover {
   transform: translateY(-1px);
-  box-shadow: 0 6px 16px rgba(47, 128, 237, 0.35);
+  // box-shadow: 0 6px 16px rgba(47, 128, 237, 0.35);
 }
 .image-preview__toolbar button:active {
   transform: translateY(0);
-  box-shadow: 0 2px 8px rgba(47, 128, 237, 0.28);
-  background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
+  // box-shadow: 0 2px 8px rgba(47, 128, 237, 0.28);
+  // background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
+  background: #ff9300;
 }
 .image-preview__toolbar button:focus-visible {
-  outline: 2px solid rgba(47, 128, 237, 0.6);
+  // outline: 2px solid rgba(47, 128, 237, 0.6);
+  outline: none;
   outline-offset: 2px;
 }
 .image-preview__stage {

BIN
src/assets/img/tool_photo.png


BIN
src/assets/img/tool_vote.png


+ 5 - 2
src/assets/styles/prosemirror.scss

@@ -20,9 +20,12 @@
     font-size: 0;
     padding: 0;
   }
-  p:first-child {
+  p:first-child:has(br) {
+    font-size: 0;
+  }
+  p:first-child br {
     margin-top: 0;
-    // font-size: 0;
+    font-size: 0;
   }
 
   ul, ol, li {

+ 1156 - 413
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -1,100 +1,212 @@
-<template>
-    <div class="ai-chat-container">
-        <!-- 聊天区域 -->
-        <div class="chat-section" v-if="messages.length > 0" ref="chatSection">
-            <!-- 消息列表 -->
-            <div v-for="(message, index) in messages" :key="index" class="chat-message">
-                <div class="message-content user-message chat" v-if="message.content">
-                    <div v-html="message.content"></div>
-                    <!-- 显示上传的文件 -->
-                    <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
-                        <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
-                            <span>{{ file.title }}</span>
-                        </div>
-                    </div>
-                </div>
-                <div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
-                    <div v-if="message.aiContent" v-html="message.aiContent"></div>
-                    <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
-                        viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
-                        <circle cx="4" cy="12" r="3" fill="currentColor">
-                            <animate id="svgSpinners3DotsBounce0" attributeName="cy"
-                                begin="0;svgSpinners3DotsBounce1.end+0.25s" calcMode="spline" dur="0.6s"
-                                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
-                        <circle cx="12" cy="12" r="3" fill="currentColor">
-                            <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline"
-                                dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
-                        <circle cx="20" cy="12" r="3" fill="currentColor">
-                            <animate id="svgSpinners3DotsBounce1" attributeName="cy"
-                                begin="svgSpinners3DotsBounce0.begin+0.2s" calcMode="spline" dur="0.6s"
-                                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
+<template>
+  <div class="ai-chat-container">
+    <div class="message-quick-box">
+      <div class="message-quick-box-item" :class="{ 'active': isQuickActions.includes('quick') }">
+        <button type="button" class="coco-collapse-trigger" @click="toggleQuickActions('quick')">
+          <span class="coco-collapse-icon soft-orange">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
+              stroke-linecap="round">
+              <path d="M4 7h16"></path>
+              <path d="M4 12h10"></path>
+              <path d="M4 17h7"></path>
+            </svg>
+          </span>
+          <span class="coco-collapse-copy">
+            <span class="coco-collapse-title">{{ lang.ssAiChatQuickTitle }}</span>
+            <span class="coco-collapse-subtitle" id="cocoQuickSectionHint">{{ lang.ssAiChatQuickSubtitle }}</span>
+          </span>
+          <span class="coco-collapse-chevron">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <polyline points="6 9 12 15 18 9"></polyline>
+            </svg>
+          </span>
+        </button>
+        <div class="coco-collapse-body" v-if="isQuickActions.includes('quick')">
+          <div class="coco-quick-tabs">
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'page' }"
+              @click="setCocoQuickTab('page')">{{ lang.ssAiChatQuickTabPage }}</button>
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'course' }"
+              @click="setCocoQuickTab('course')">{{ lang.ssAiChatQuickTabCourse }}</button>
+          </div>
+          <div class="coco-quick-list">
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
+              @click="sendQuickAction(lang.ssAiChatQuickAction3)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenChoicesTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenChoicesDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <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="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button>
+
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
+              @click="sendQuickAction(lang.ssAiChatQuickAction4)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenWebTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenWebDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <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="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button>
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'"
+              @click="sendQuickAction(lang.ssAiChatQuickAction5)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickRecommendToolsTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickRecommendToolsDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <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="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button>
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'" @click="sendQuickAction(lang.ssBatchInteractiveWeb)">
+                <span class="coco-quick-action-copy">
+                    <span class="coco-quick-action-title">{{ lang.ssAiChatQuickBatchGenToolsTitle }}</span>
+                    <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickBatchGenToolsDesc }}</span>
+                </span>
+                <span class="coco-quick-action-arrow">
+                    <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="M5 12h14"></path>
+                        <path d="M13 5l7 7-7 7"></path>
                     </svg>
-                    <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }" v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
-                        @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm}}</button>
-                </div>
+                </span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 聊天区域 -->
+    <div class="chat-section" ref="chatSection">
+      <!-- 消息列表 -->
+      <div v-for="(message, index) in messages" :key="index" class="chat-message">
+        <div class="message-content user-message chat" v-if="message.content">
+          <div v-html="message.content"></div>
+          <!-- 显示上传的文件 -->
+          <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
+            <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
+              <span>{{ file.title }}</span>
             </div>
+          </div>
         </div>
-        <!-- 输入区域 -->
-        <div class="input-section">
-            <div class="input-wrapper">
-                <div class="file-box" v-show="files.length">
-                    <div v-for="(file, index) in files" :key="index" class="file-item">
-                        <span class="file-name">{{ file.title }}</span>
-                        <button class="remove-file-btn" @click="removeFile(index)">
-                            <svg 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>
-                                <line x1="6" y1="6" x2="18" y2="18"></line>
-                            </svg>
-                        </button>
-                    </div>
-                </div>
-                <textarea class="ai-input"
-                    :placeholder="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
-                    v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
-                <div class="input-actions">
-                    <FileInput accept="*" @change="handleFileUpload" >
-                        <button class="attach-btn">
-                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                                <path
-                                    d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
-                                </path>
-                            </svg>
-                        </button>
-                    </FileInput>
-                    <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
-                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
-                            stroke-linecap="round" stroke-linejoin="round">
-                            <line x1="22" y1="2" x2="11" y2="13"></line>
-                            <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
-                        </svg>
-                    </button>
-                    <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
-                        <svg width="32" height="32" viewBox="0 0 32 32"
-                            fill="none" xmlns="http://www.w3.org/2000/svg">
-                            <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
-                            <path
-                                d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
-                                fill="white" fill-opacity="0.9"></path>
-                        </svg>
-                    </button>
-                </div>
+        <div class="message-content ai-message chat" :class="{'loading-state': message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'}" v-if="message.aiContent || message.chatloading">
+          <div class="submenu-icon loading-icon" v-show="message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
+              <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
+            </svg>
+          </div>
+          <div v-if="message.aiContent" v-html="message.aiContent"></div>
+          <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
+            viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
+            <circle cx="4" cy="12" r="3" fill="currentColor">
+              <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
+                calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+            <circle cx="12" cy="12" r="3" fill="currentColor">
+              <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
+                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+            <circle cx="20" cy="12" r="3" fill="currentColor">
+              <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
+                calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+          </svg>
+          <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
+            v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_interactive_web' && message.jsonData?.gType !== 'trigger_tool_recommend' && message.jsonData?.gType !== 'trigger_batch_tool' && !message.chatloading && message.aiContent"
+            @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
+          <button type="button" class="coco-interactive-web-card"
+            v-if="message.jsonData?.gType === 'generate_interactive_web' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
+            <div class="submenu-icon loading-icon" v-show="create_app_loading">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
+                <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
+              </svg>
+            </div>
+            <div class="coco-interactive-web-card-head">
+              <div class="coco-interactive-web-card-title">{{ lang.ssInteractiveWebTitle }}</div>
+              <!-- <span class="coco-interactive-web-card-status">待确认</span> -->
             </div>
-                    <!-- 输入时的快捷操作弹出 -->
-        <div class="quick-actions-popup" v-if="showQuickActions">
-            <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
+            <div class="coco-interactive-web-card-desc">{{ lang.ssInteractiveWebDesc }}</div>
+            <div class="coco-interactive-web-card-foot">
+              <span class="coco-interactive-web-card-action">{{ lang.ssOpenWorkspace }}</span>
+            </div>
+          </button>
         </div>
+      </div>
+    </div>
+    <!-- 输入区域 -->
+    <div class="input-section">
+      <div class="input-wrapper">
+        <div class="file-box" v-show="files.length">
+          <div v-for="(file, index) in files" :key="index" class="file-item">
+            <span class="file-name">{{ file.title }}</span>
+            <button class="remove-file-btn" @click="removeFile(index)">
+              <svg 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>
+                <line x1="6" y1="6" x2="18" y2="18"></line>
+              </svg>
+            </button>
+          </div>
         </div>
-        <!-- 初始状态 -->
-        <div class="initial-state" v-if="messages.length === 0">
-            <!-- 快捷操作 -->
-            <div class="quick-actions">
-                <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
-            </div>
+        <textarea class="ai-input" :placeholder="lang.ssAiChatShortcut" v-model="inputText"
+          @keyup.enter.exact="sendMessage" rows="5" />
+        <div class="input-actions">
+          <FileInput accept="*" @change="handleFileUpload">
+            <button class="attach-btn">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path
+                  d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
+                </path>
+              </svg>
+            </button>
+          </FileInput>
+          <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+              stroke-linejoin="round">
+              <line x1="22" y1="2" x2="11" y2="13"></line>
+              <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
+            </svg>
+          </button>
+          <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
+            <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
+              <path
+                d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
+                fill="white" fill-opacity="0.9"></path>
+            </svg>
+          </button>
         </div>
+      </div>
+      <!-- 输入时的快捷操作弹出 -->
+      <div class="quick-actions-popup" v-if="showQuickActions">
+        <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
+          @click="sendQuickAction(action)">{{ action }}</button>
+      </div>
     </div>
+    <!-- 初始状态 -->
+    <div class="initial-state" v-if="false">
+      <!-- 快捷操作 -->
+      <div class="quick-actions">
+        <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
+          @click="sendQuickAction(action)">{{ action }}</button>
+      </div>
+    </div>
+
+    <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" @add="addAiWeb" :is-loading="isLoading" />
+
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -107,52 +219,58 @@ import { getWorkPageId } from '@/services/course'
 import FileInput from '@/components/FileInput2.vue'
 import axios from '@/services/config'
 import message from '@/utils/message'
+import AiWeb from './aiWeb.vue'
 
 interface ChatMessage {
-    uid?: string
-    role: 'ai' | 'user'
-    content?: string
-    aiContent?: string
-    oldContent?: string
-    loading?: boolean
-    chatloading?: boolean
-    gLoading?: boolean
-    rawContent?: string
-    timestamp?: Date
-    like?: boolean
-    unlike?: boolean
-    isTyping?: boolean
-    AI?: string
-    isShowSynchronization?: boolean
-    filename?: string
-    is_mind_map?: boolean
-    sourceFiles?: Array<{
-        title: string
-        id?: string
-        url?: string
+  uid?: string
+  role: 'ai' | 'user'
+  content?: string
+  aiContent?: string
+  oldContent?: string
+  loading?: boolean
+  chatloading?: boolean
+  gLoading?: boolean
+  rawContent?: string
+  timestamp?: Date
+  like?: boolean
+  unlike?: boolean
+  isTyping?: boolean
+  AI?: string
+  isShowSynchronization?: boolean
+  filename?: string
+  is_mind_map?: boolean
+  sourceFiles?: Array<{
+    title: string
+    id?: string
+    url?: string
+  }>
+  jsonData?: {
+    gType?: string
+    isGenerate?: boolean
+    headUrl?: string
+    assistantName?: string
+    files?: Array<{
+      title: string
+      id?: string
+      url?: string
     }>
-    jsonData?: {
-        gType?: string
-        isGenerate?: boolean
-        headUrl?: string
-        assistantName?: string
-        files?: Array<{
-            title: string
-            id?: string
-            url?: string
-        }>
-        sourceArray?: Array<{
-            text?: string
-            id?: string
-            title?: string
-        }>
-    }
+    sourceArray?: Array<{
+      text?: string
+      id?: string
+      title?: string
+    }>
+    aiContent?: string
+  }
 }
 
 const props = withDefaults(defineProps<{
-    userid?: string | null
+  userid?: string | null
+  userJson?: any
+  courseTitle?: string | null
 }>(), {
   userid: null,
+  userJson: null,
+  courseTitle: null,
 })
 
 
@@ -170,11 +288,14 @@ const files = ref<Array<{ title: string; id?: string | null; url?: string; isPro
 const quickActions = [
   lang.ssAiChatQuickAction1,
   lang.ssAiChatQuickAction2,
+  lang.ssAiChatQuickAction6,
+  lang.ssAiChatQuickAction7,
 ]
 
 // 监听输入变化,当输入"/"时显示快捷操作
 watch(inputText, (newValue) => {
-  if (messages.value.length > 0 && newValue === '/') {
+  // messages.value.length > 0 && 
+  if (newValue === '/') {
     showQuickActions.value = true
   }
   else if (newValue !== '/') {
@@ -242,7 +363,7 @@ const stopMessage = () => {
 const handleFileUpload = async (files2: File[]) => {
   const maxSize = 10 * 1024 * 1024 // 10MB
   const uploadPromises = []
-  
+
   for (let i = 0; i < files2.length; i++) {
     const file = files2[i]
     if (file.size > maxSize) {
@@ -281,10 +402,10 @@ const handleFileUpload = async (files2: File[]) => {
         files.value.splice(fileIndex, 1)
       }
     })
-    
+
     uploadPromises.push(uploadPromise)
   }
-  
+
   // 等待所有文件上传完成
   await Promise.allSettled(uploadPromises)
 }
@@ -318,7 +439,7 @@ const gType = ref('chat')
 
 const sendAction = async (action: string) => {
   const md = new MarkdownIt()
-  if (gType.value === 'chat') {
+  if (gType.value === 'chat' || gType.value === 'generate_interactive_web' || gType.value === 'trigger_tool_recommend' || gType.value === 'trigger_batch_tool') {
     const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
     noStreamController.value = result
     console.log(result)
@@ -336,12 +457,69 @@ const sendAction = async (action: string) => {
       gType.value = 'chat'
     }
 
-    // generate_qa // generate_choice_question
-    if (gType.value !== 'chat') {
+    // generate_qa // generate_choice_question // generate_photo // generate_poll
+    if (gType.value !== 'chat' && gType.value !== 'generate_interactive_web' && gType.value !== 'trigger_batch_tool') {
+      messages.value.at(-1).jsonData = {
+        gType: gType.value,
+        isGenerate: false
+      }
+    }
+    else if (gType.value === 'generate_interactive_web') {
+      messages.value.at(-1).jsonData = {
+        gType: gType.value,
+        isGenerate: false
+      }
+      messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
+      messages.value.at(-1).loading = false
+      messages.value.at(-1).chatloading = false
+      chatLoading.value = false
+      return
+    }
+    else if (gType.value === 'trigger_batch_tool') {
       messages.value.at(-1).jsonData = {
         gType: gType.value,
         isGenerate: false
       }
+      messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
+      // messages.value.at(-1).loading = false
+      // messages.value.at(-1).chatloading = false
+      // chatLoading.value = false
+      // return
+    }
+
+    if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
+      gType.value = 'generate_interactive_web'
+      messages.value.at(-1).jsonData = {
+        gType: 'generate_interactive_web',
+        isGenerate: false
+      }
+      messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
+      messages.value.at(-1).loading = false
+      chatLoading.value = false
+      messages.value.at(-1).chatloading = false
+
+      return
+    }
+
+    if (content.includes(lang.ssAiChatQuickAction5)) {
+      gType.value = 'trigger_tool_recommend'
+      messages.value.at(-1).jsonData = {
+        gType: 'trigger_tool_recommend',
+        isGenerate: false
+      }
+    }
+
+    if (content.includes(lang.ssBatchInteractiveWeb)) {
+      gType.value = 'trigger_batch_tool'
+      messages.value.at(-1).jsonData = {
+        gType: 'trigger_batch_tool',
+        isGenerate: false
+      }
+      messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
+      // messages.value.at(-1).loading = false
+      // chatLoading.value = false
+      // messages.value.at(-1).chatloading = false
+      return
     }
   }
   else {
@@ -350,8 +528,62 @@ const sendAction = async (action: string) => {
       isGenerate: false
     }
   }
-
-
+  const getTypeLabel = (type?: number) => {
+    const typeMap: Record<number, string> = {
+      45: lang.ssChoiceQ,
+      15: lang.ssQATest,
+      72: lang.ssAiApp,
+      73: lang.ssHPage,
+      81: lang.ssHPage,
+      74: lang.ssVideo,
+      75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+      76: lang.ssCreative,
+      77: lang.ssEnglishSpeakingTool,
+      78: lang.ssVote,
+      79: lang.ssPhoto,
+    }
+    return typeMap[type || 0] || lang.ssUnknown
+  }
+  // 提取所有页面中 type 为 'text' 的元素的纯文本内容
+  const allTextContents = slidesStore.slides
+    .map((slide: any, index: number) => {
+      const elements = slide.elements.filter((element: any) => {
+        if (element.type === 'text' || (element.type === 'shape' && element.text && element.text.content)) {
+          return true
+        }
+        // 如果 element.type 包含 frame 且 tooltype 是对应的工具,输出 lang.ssInteract(对应工具)
+        if (element.type === 'frame' && element.toolType) {
+          const toolLabel = getTypeLabel(element.toolType)
+          if (toolLabel !== lang.ssUnknown) {
+            return true
+          }
+        }
+        return false
+      })
+      const pageContents = elements.map((textElement: any) => {
+        let content = ''
+        if (textElement.type === 'shape') {
+          // 创建一个临时元素来解析 HTML 并提取纯文本
+          const tempElement = document.createElement('div')
+          tempElement.innerHTML = textElement.text.content
+          content = tempElement.textContent || tempElement.innerText || ''
+        }
+        else if (textElement.type === 'frame' && textElement.toolType) {
+          // 如果是 frame 类型且有 toolType,输出交互工具标签
+          content = lang.ssInteract + `(${getTypeLabel(textElement.toolType)})`
+        }
+        else {
+          // 创建一个临时元素来解析 HTML 并提取纯文本
+          const tempElement = document.createElement('div')
+          tempElement.innerHTML = textElement.content
+          content = tempElement.textContent || tempElement.innerText || ''
+        }
+        return content
+      }).filter((content: string) => content.trim() !== '')
+      
+      const pageContent = pageContents.length > 0 ? pageContents.join(' ') : '无文本内容'
+      return `第${index + 1}页的内容:${pageContent}`
+    }) || []
 
   // 提取当前页面中 type 为 'text' 的元素的纯文本内容
   const textContents = slidesStore.currentSlide?.elements
@@ -367,16 +599,66 @@ const sendAction = async (action: string) => {
       const tempElement = document.createElement('div')
       tempElement.innerHTML = textElement.content
       return tempElement.textContent || tempElement.innerText || ''
-      
+
 
     })
     .filter(content => content.trim() !== '') || []
+
+  console.log('allTextContents', allTextContents)
   console.log('textContents', textContents)
-  const prompt = `
-  #当前页面内容 ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
+  let prompt = ''
+  let agentid = agentid2.value
+
+
+  if (gType.value === 'trigger_tool_recommend') {
+    agentid = agentid3.value
+    prompt = `
+  #课程标题(course_title): ${props.courseTitle || ''}
+  #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
+  #query: ${action}
+  `
+  }
+  else if (gType.value === 'trigger_batch_tool') {
+    agentid = agentid4.value
+    prompt = `
+  #课程标题(course_title): ${props.courseTitle || ''}
+  #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
   #query: ${action}
   `
-  chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
+  }
+  else {
+    prompt = `
+  #课程标题(course_title): ${props.courseTitle || ''}
+  #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
+  #query: ${action}
+  `
+  }
+
+  if (gType.value === 'trigger_batch_tool') {
+    chat_stream(prompt, agentid, props.userid || '', lang.lang, async (event) => {
+      if (event.type === 'message') {
+        messages.value.at(-1).jsonData.aiContent = md.render(event.data)
+        messages.value.at(-1).loading = false
+        
+      }
+      else if (event.type === 'messageEnd') {
+        messages.value.at(-1).jsonData.aiContent = md.render(event.data)
+        console.log('event.data', event.data)
+        generate(messages.value.at(-1))
+
+      }
+    }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
+      streamController.value = controller
+    }).catch(err => {
+      chatLoading.value = false
+      console.log('err', err)
+      stopMessage()
+    })
+    return
+
+  }
+  
+  chat_stream(prompt, agentid, props.userid || '', lang.lang, (event) => {
     if (event.type === 'message') {
       messages.value.at(-1).aiContent = md.render(event.data)
 
@@ -426,7 +708,7 @@ const generate = (message: ChatMessage) => {
     const prompt = [
       {
         role: 'user',
-        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}`,
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
       },
     ]
     chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
@@ -447,7 +729,7 @@ const generate = (message: ChatMessage) => {
     const prompt = [
       {
         role: 'user',
-        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}`,
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
       },
     ]
     chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
@@ -463,6 +745,79 @@ const generate = (message: ChatMessage) => {
       })
     })
   }
+  else if (message.jsonData?.gType === 'generate_poll') {
+    console.log(message.jsonData?.gType)
+    const prompt = [
+      {
+        role: 'user',
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成投票的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[],"userAnswer":[]]}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
+      },
+    ]
+    chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
+      console.log('投票', JSON.parse(res))
+      gType.value = 'chat'
+      setPageId(78, res).then(res => {
+        const baseUrl = setUrl()
+        const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${78}`
+        createSlide()
+        createFrameElement(url, 78)
+        message.gLoading = false
+        message.jsonData.isGenerate = true
+      })
+    })
+  }
+  else if (message.jsonData?.gType === 'generate_photo') {
+    console.log(message.jsonData?.gType)
+    const prompt = [
+      {
+        role: 'user',
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成拍照的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[]}。`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
+      },
+    ]
+    chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
+      console.log('拍照', JSON.parse(res))
+      gType.value = 'chat'
+      setPageId(79, res).then(res => {
+        const baseUrl = setUrl()
+        const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${79}`
+        createSlide()
+        createFrameElement(url, 79)
+        message.gLoading = false
+        message.jsonData.isGenerate = true
+      })
+    })
+  }
+  else if (message.jsonData?.gType === 'trigger_batch_tool') {
+    console.log(message.jsonData?.gType)
+    const prompt = [
+      {
+        role: 'user',
+        content: `这是用户输入的内容:“${message.jsonData.aiContent}”,根据用户输入的内容,取出里面candidates的值。输出一个json格式的回复`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
+      },
+    ]
+    chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
+      console.log('批量生成工具', JSON.parse(res))
+      gType.value = 'chat'
+      messages.value.at(-1).chatloading = false
+      messages.value.push({
+        role: 'ai',
+        aiContent: lang.ssBatchInteractiveWebDesc2,
+        jsonData: {
+          gType: 'trigger_batch_tool'
+        }
+      })
+      chatLoading.value = false
+      const array = JSON.parse(res).candidates
+      for (let i = 0; i < array.length; i++) {
+        const item = array[i]
+        const pageIndex = item.page_index - 1
+        if (pageIndex >= 0 && pageIndex < slidesStore.slides.length) {
+          const slide = slidesStore.slides[pageIndex]
+          slidesStore.updateSlide({ toolsArray: item.tools }, slide.id)
+        }
+      }
+    })
+  }
 }
 
 const setPageId = async (tool: any, json: any) => {
@@ -482,7 +837,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
     const timestamp = Date.now()
     const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
     const baseName = file.name.slice(0, -(finalExtension.length + 1))
-    
+
     formData.append(
       'file',
       new File([file], `${baseName}${timestamp}.${finalExtension}`)
@@ -504,7 +859,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
         signal: signal
       }
     )
-    
+
     console.log(res)
     return res
   }
@@ -521,390 +876,778 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
 
 const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
 const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
+const agentid3 = ref('caaa43ab-41a5-466a-94bc-74e0c9265068')// 互动工具推荐
+const agentid4 = ref('b41bc650-9861-468c-a624-b52269dfff22')// 批量生成工具
+
+const isQuickActions = ref<string[]>([])
+const toggleQuickActions = (section: string) => {
+  if (isQuickActions.value.includes(section)) {
+    isQuickActions.value = isQuickActions.value.filter((item: string) => item !== section)
+  }
+  else {
+    isQuickActions.value.push(section)
+  }
+}
 
+const cocoQuickTab = ref('page')
+const setCocoQuickTab = (tab: string) => {
+  cocoQuickTab.value = tab
+}
+
+// AI Web 相关状态
+const create_app_loading = ref(false)
+const webId = ref('')
+const showAiWebModal = ref(false)
+const isEdit = ref(false)
+import { storeToRefs } from 'pinia'
+import _ from 'lodash'
+const { currentSlide } = storeToRefs(slidesStore)
+
+const handle_add_aiWeb = _.throttle(async () => {
+  create_app_loading.value = true
+  try {
+    const elements = currentSlide.value?.elements || []
+    console.log(elements)
+    const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
+    
+    if (existingWebElement && existingWebElement.webId) {
+      webId.value = existingWebElement.webId
+      create_app_loading.value = false
+      showAiWebModal.value = true
+      isEdit.value = true
+      return
+    }
+
+    const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
+      userid: props.userid || '',
+      username: props.userJson.name || '',
+      muti_name: lang.ssAddWebH5,
+      description: lang.ssAddWebH5,
+      headUrl: 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F51741770599274.svg',
+      organizeid: props.userJson.organizeid || '',
+      content: ''
+    })
+    console.log(res)
+    webId.value = res.id || ''
+
+    create_app_loading.value = false
+    showAiWebModal.value = true
+  }
+  catch (error) {
+    console.log(error)
+    create_app_loading.value = false
+  }
+}, 3000)
+
+const isLoading = ref(false)
+import useImport from '@/hooks/useImport'
+const { uploadFileToS3 } = useImport()
+
+const addAiWeb = async (code: string) => {
+  if (!code) return
+  const file = new File([code], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  if (isEdit.value) {
+    const elements = currentSlide.value?.elements || []
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 73))
+    if (frameElement) {
+      slidesStore.updateElement({
+        id: frameElement.id,
+        props: { url: url }
+      })
+    }
+  }
+  else {
+    createSlide()
+    createFrameElement(url, 73, webId.value)
+  }
+  
+  isLoading.value = false
+  showAiWebModal.value = false
+  webId.value = ''
+  isEdit.value = false
+}
 
 onMounted(() => {
   session_name.value = uuidv4()
   getAgentModel(agentid1.value)
   getAgentModel(agentid2.value)
+  getAgentModel(agentid3.value)
+  getAgentModel(agentid4.value)
 })
 </script>
 
 <style lang="scss" scoped>
 .ai-chat-container {
-    width: 100%;
-    height: 100%;
-    display: flex;
-    flex-direction: column;
-    padding: 16px;
-    gap: 16px;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  gap: 16px;
 }
 
 .input-section {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
-    position: relative;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  position: relative;
 }
 
 .input-wrapper {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    background: #fafbfc;
-    border: 1.5px solid #e5e7eb;
-    border-radius: 8px;
-    padding: 8px 12px;
-    min-height: 120px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  background: #fafbfc;
+  border: 1.5px solid #e5e7eb;
+  border-radius: 8px;
+  padding: 8px 12px;
+  min-height: 120px;
 }
 
 .ai-input {
-    flex: 1;
-    border: none;
-    background: transparent;
-    font-size: 14px;
-    color: #374151;
-    outline: none;
-    resize: none;
-    min-height: 80px;
-
-    &::placeholder {
-        color: #9CA3AF;
-    }
+  flex: 1;
+  border: none;
+  background: transparent;
+  font-size: 14px;
+  color: #374151;
+  outline: none;
+  resize: none;
+  min-height: 80px;
+
+  &::placeholder {
+    color: #9CA3AF;
+  }
 }
 
 .input-actions {
-    display: flex;
-    // justify-content: space-between;
-    align-items: center;
-    margin-top: 8px;
+  display: flex;
+  // justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
 }
 
 .attach-btn {
-    width: 32px;
-    height: 32px;
-    background: none;
-    border: none;
-    cursor: pointer;
-    padding: 4px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.3s ease;
-    color: #6b7280;
-
-    svg {
-        width: 20px;
-        height: 20px;
-    }
+  width: 32px;
+  height: 32px;
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+  color: #6b7280;
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
 
-    &:hover {
-        background: #FFF4E5;
-        color: #F78B22;
-        border-radius: 4px;
-    }
+  &:hover {
+    background: #FFF4E5;
+    color: #F78B22;
+    border-radius: 4px;
+  }
 
-    input[type="file"] {
-        display: none;
-    }
+  input[type="file"] {
+    display: none;
+  }
 }
 
 .file-box {
-    margin-bottom: 8px;
-    min-height: 24px;
-    max-height: 70px;
-    overflow-y: auto;
+  margin-bottom: 8px;
+  min-height: 24px;
+  max-height: 70px;
+  overflow-y: auto;
 
-    .file-item {
-        display: flex;
-        align-items: center;
-        background: #f5f5f5;
-        padding: 4px 8px;
-        border-radius: 4px;
-        margin-bottom: 4px;
-
-        .file-name {
-            flex: 1;
-            font-size: 12px;
-            color: #374151;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-        }
-
-        .remove-file-btn {
-            background: none;
-            border: none;
-            cursor: pointer;
-            color: #9CA3AF;
-            font-size: 12px;
-            padding: 2px;
-            margin-left: 8px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
+  .file-item {
+    display: flex;
+    align-items: center;
+    background: #f5f5f5;
+    padding: 4px 8px;
+    border-radius: 4px;
+    margin-bottom: 4px;
+
+    .file-name {
+      flex: 1;
+      font-size: 12px;
+      color: #374151;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
 
-            svg {
-                width: 14px;
-                height: 14px;
-            }
+    .remove-file-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #9CA3AF;
+      font-size: 12px;
+      padding: 2px;
+      margin-left: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      svg {
+        width: 14px;
+        height: 14px;
+      }
 
-            &:hover {
-                color: #EF4444;
-            }
-        }
+      &:hover {
+        color: #EF4444;
+      }
     }
+  }
 }
 
 .send-btn {
-    margin-left: auto;
-    width: 32px;
-    height: 32px;
-    border: none;
-    background: #FF9300;
-    color: #fff;
-    border-radius: 50%;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.3s ease;
+  margin-left: auto;
+  width: 32px;
+  height: 32px;
+  border: none;
+  background: #FF9300;
+  color: #fff;
+  border-radius: 50%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
 
-    svg {
-        width: 20px;
-        height: 20px;
-    }
+  &:hover {
+    background: #E68A00;
+  }
 
-    &:hover {
-        background: #E68A00;
-    }
+  &.stop {
+    background: unset;
+    width: auto;
 
-    &.stop {
-        background: unset;
-        width: auto;
-        svg {
-            width: 32px;
-            height: 32px;
-        }
+    svg {
+      width: 32px;
+      height: 32px;
     }
+  }
 }
 
 .quick-actions {
-    // display: flex;
-    // flex-direction: column;
-    // gap: 8px;
-
-    .quick-action-btn {
-        padding: 5px 10px;
-        border: 1px solid #f7c58f;
-        background: #FFF9F2;
-        color: #6b4a1f;
-        border-radius: 16px;
-        font-size: 12px;
-        cursor: pointer;
-        text-align: left;
-        display: block;
-
-        &:hover {
-            border-color: #F78B22;
-            background: #FFF4E5;
-            color: #111827;
-        }
+  // display: flex;
+  // flex-direction: column;
+  // gap: 8px;
+
+  .quick-action-btn {
+    padding: 5px 10px;
+    border: 1px solid #f7c58f;
+    background: #FFF9F2;
+    color: #6b4a1f;
+    border-radius: 16px;
+    font-size: 12px;
+    cursor: pointer;
+    text-align: left;
+    display: block;
 
-        +.quick-action-btn {
-            margin-top: 8px;
-        }
+    &:hover {
+      border-color: #F78B22;
+      background: #FFF4E5;
+      color: #111827;
+    }
+
+    +.quick-action-btn {
+      margin-top: 8px;
     }
+  }
 }
 
 
 
 .chat-section {
-    // flex: 1;
-    height: calc(100% - 155px);
-    overflow-y: auto;
-    display: flex;
-    flex-direction: column;
-    // gap: 16px;
-    padding-right: 8px;
-
-    &::-webkit-scrollbar {
-        width: 6px;
-    }
+  // flex: 1;
+  height: calc(100% - 155px);
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  // gap: 16px;
+  padding-right: 8px;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
 
-    &::-webkit-scrollbar-track {
-        background: #F3F4F6;
-        border-radius: 3px;
-    }
+  &::-webkit-scrollbar-track {
+    background: #F3F4F6;
+    border-radius: 3px;
+  }
 
-    &::-webkit-scrollbar-thumb {
-        background: #D1D5DB;
-        border-radius: 3px;
+  &::-webkit-scrollbar-thumb {
+    background: #D1D5DB;
+    border-radius: 3px;
 
-        &:hover {
-            background: #9CA3AF;
-        }
+    &:hover {
+      background: #9CA3AF;
     }
+  }
 }
 
 .chat-message {
-    max-width: 100%;
-    margin-bottom: 10px;
+  max-width: 100%;
+  margin-bottom: 10px;
+  display: flex;
+  flex-direction: column;
+
+  .message-content {
     display: flex;
     flex-direction: column;
+    border-radius: 8px;
+    padding: 8px 10px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #374151;
+    width: fit-content;
+    position: relative;
+
+    +.message-content {
+      margin-top: 10px;
+    }
+
+    &.loading-state {
+      opacity: 0.7;
+      cursor: not-allowed;
+      pointer-events: none;
+
+      // .submenu-icon {
+      //   animation: pulse 1s infinite;
+      // }
 
-    .message-content {
+      .spin {
+        animation: spin 1s linear infinite;
+      }
+
+      .submenu-icon {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        background: #00000026;
+        top: 0;
+        left: 0;
         display: flex;
-        flex-direction: column;
-        border-radius: 8px;
-        padding: 8px 10px;
-        font-size: 14px;
-        line-height: 1.5;
-        color: #374151;
-        width: fit-content;
+        align-items: center;
+        justify-content: center;
 
-        +.message-content {
-            margin-top: 10px;
+        svg {
+          height: 30px;
+          color: #fff;
         }
+      }
     }
+  }
 }
 
 .message-content {
-    word-break: break-word;
+  word-break: break-word;
 
-    &.ai-message {
-        align-self: flex-start;
-        background: #fafbfc;
-        border: 1.5px solid #e5e7eb;
-        border-bottom-left-radius: 2px;
+  &.ai-message {
+    align-self: flex-start;
+    background: #fafbfc;
+    border: 1.5px solid #e5e7eb;
+    border-bottom-left-radius: 2px;
 
-        &>svg {
-            width: 17px;
-            height: 17px;
-        }
+    &>svg {
+      width: 17px;
+      height: 17px;
     }
+  }
 }
 
 .message-content {
-    &.user-message {
-        align-self: flex-end;
-        background: #FFF4E5;
-        border: 1.5px solid #F78B22;
-        border-bottom-right-radius: 2px;
-    }
+  &.user-message {
+    align-self: flex-end;
+    background: #FFF4E5;
+    border: 1.5px solid #F78B22;
+    border-bottom-right-radius: 2px;
+  }
 }
 
 .initial-state {
-    display: flex;
-    flex-direction: column;
-    justify-content: flex-start;
-    align-items: flex-start;
-    // padding: 24px;
-    gap: 16px;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  // padding: 24px;
+  gap: 16px;
 }
 
 
 .confirm-btn {
-    margin-top: 10px;
-    padding: 6px 15px;
-    background: #FF9300;
-    color: white;
+  margin-top: 10px;
+  padding: 6px 15px;
+  background: #FF9300;
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  cursor: pointer;
+  margin-left: auto;
+  transition: all 0.3s ease;
+
+  &:hover {
+    background: #E68A00;
+  }
+
+  &.disabled {
+    background: #9CA3AF;
+    cursor: not-allowed;
+  }
+}
+
+ul {
+  margin: 8px 0;
+  padding-left: 20px;
+
+  li {
+    margin: 4px 0;
+  }
+}
+
+.quick-actions-popup {
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  right: 0;
+  background: white;
+  border: 1px solid #E5E7EB;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  padding: 8px;
+  z-index: 100;
+  margin-bottom: 8px;
+
+  .quick-action-btn {
+    width: 100%;
+    text-align: left;
+    padding: 10px 12px;
+    background: white;
     border: none;
-    border-radius: 8px;
+    border-radius: 6px;
     font-size: 14px;
+    color: #374151;
     cursor: pointer;
-    margin-left: auto;
-    transition: all 0.3s ease;
+    transition: all 0.2s ease;
 
     &:hover {
-        background: #E68A00;
-    }
-    &.disabled {
-        background: #9CA3AF;
-        cursor: not-allowed;
+      background: #fff4e5;
     }
+  }
 }
 
-ul {
-    margin: 8px 0;
-    padding-left: 20px;
+.message-quick-box {
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+  gap: 10px;
+
+  .message-quick-box-item {
+    box-shadow: rgba(17, 24, 39, 0.04) 0px 10px 30px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: rgb(239, 229, 216);
+    border-image: initial;
+    border-radius: 18px;
+    background: rgba(255, 255, 255, 0.96);
+    overflow: hidden;
 
-    li {
-        margin: 4px 0;
+
+    .coco-collapse-trigger {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+      text-align: left;
+      gap: 12px;
+      border-width: initial;
+      border-style: none;
+      border-color: initial;
+      border-image: initial;
+      background: transparent;
+      padding: 12px 14px;
+
+      &:hover {
+        background: rgb(255, 251, 245);
+      }
     }
-}
 
-.quick-actions-popup {
-    position: absolute;
-    bottom: 100%;
-    left: 0;
-    right: 0;
-    background: white;
-    border: 1px solid #E5E7EB;
-    border-radius: 8px;
-    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-    padding: 8px;
-    z-index: 100;
-    margin-bottom: 8px;
+    .coco-collapse-icon.soft-orange {
+      color: rgb(154, 91, 17);
+      background: rgb(255, 240, 219);
+    }
 
-    .quick-action-btn {
-        width: 100%;
-        text-align: left;
-        padding: 10px 12px;
-        background: white;
-        border: none;
-        border-radius: 6px;
+    .coco-collapse-icon {
+      width: 34px;
+      height: 34px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      border-radius: 12px;
+    }
+
+    .coco-collapse-copy {
+      min-width: 0px;
+      display: flex;
+      flex-direction: column;
+      flex: 1 1 0%;
+      gap: 2px;
+
+      .coco-collapse-title {
         font-size: 14px;
-        color: #374151;
-        cursor: pointer;
-        transition: all 0.2s ease;
+        font-weight: 700;
+        color: rgb(17, 24, 39);
+      }
+
+      .coco-collapse-subtitle {
+        font-size: 12px;
+        color: rgb(139, 115, 86);
+        line-height: 1.5;
+      }
+    }
+
+    .coco-collapse-chevron {
+      color: rgb(156, 163, 175);
+      flex-shrink: 0;
+
+      svg {
+        transition: transform 0.2s;
+      }
+    }
+
+    &.active {
+      .coco-collapse-chevron svg {
+        transform: rotate(180deg);
+      }
+    }
 
-        &:hover {
-            background: #fff4e5;
+    .coco-collapse-body {
+      padding: 0px 14px 14px;
+
+      .coco-quick-tabs {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0px, 1fr));
+        margin-bottom: 12px;
+        gap: 8px;
+
+        .coco-quick-tab {
+          height: 34px;
+          color: rgb(124, 92, 56);
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+          border-radius: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(234, 223, 206);
+          border-image: initial;
+          background: rgb(250, 247, 242);
+          transition: 0.2s;
+
+          &.active {
+            color: rgb(154, 91, 17);
+            box-shadow: rgba(247, 139, 34, 0.12) 0px 8px 18px;
+            border-color: rgba(247, 139, 34, 0.48);
+            background: rgb(255, 244, 229);
+          }
         }
+      }
+
+      .coco-quick-list {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+
+        .coco-quick-action {
+          width: 100%;
+          display: flex;
+          align-items: flex-start;
+          cursor: pointer;
+          text-align: left;
+          gap: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(239, 229, 216);
+          border-image: initial;
+          background: rgb(255, 253, 250);
+          border-radius: 14px;
+          padding: 12px;
+          transition: 0.2s;
+
+          .coco-quick-action-copy {
+            min-width: 0px;
+            display: flex;
+            flex-direction: column;
+            flex: 1 1 0%;
+            gap: 4px;
+
+            .coco-quick-action-title {
+              font-size: 13px;
+              font-weight: 700;
+              color: rgb(17, 24, 39);
+              line-height: 1.4;
+            }
+
+
+            .coco-quick-action-desc {
+              font-size: 12px;
+              line-height: 1.6;
+              color: rgb(139, 115, 86);
+            }
+          }
+
+          .coco-quick-action-arrow {
+            color: rgb(212, 163, 115);
+            flex-shrink: 0;
+            padding-top: 2px;
+          }
+        }
+
+      }
+
     }
+  }
+}
+
+.coco-interactive-web-card {
+  width: 100%;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  cursor: pointer;
+  border-width: 1px;
+  border-style: solid;
+  border-color: rgb(247, 197, 143);
+  border-image: initial;
+  border-radius: 14px;
+  background: linear-gradient(rgb(255, 250, 243) 0%, rgb(255, 255, 255) 100%);
+  padding: 12px 14px;
+  gap: 8px;
+  transition: 0.2s;
+  position: relative;
+  overflow: hidden;
+
+  &:hover {
+    box-shadow: rgba(247, 139, 34, 0.1) 0px 10px 24px;
+    transform: translateY(-1px);
+    border-color: rgb(247, 139, 34);
+  }
+
+  &.loading-state {
+    opacity: 0.7;
+    cursor: not-allowed;
+    pointer-events: none;
+
+    // .submenu-icon {
+    //   animation: pulse 1s infinite;
+    // }
+
+    .spin {
+      animation: spin 1s linear infinite;
+    }
+
+    .submenu-icon {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      background: #00000026;
+      top: 0;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      svg {
+        height: 30px;
+        color: #fff;
+      }
+    }
+  }
+
+  .coco-interactive-web-card-head,
+  .coco-interactive-web-card-foot {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 10px;
+
+    .coco-interactive-web-card-title {
+      min-width: 0px;
+      font-size: 14px;
+      font-weight: 700;
+      color: rgb(17, 24, 39);
+      line-height: 1.4;
+      flex: 1 1 0%;
+    }
+
+    .coco-interactive-web-card-action {
+      margin-left: auto;
+      font-size: 12px;
+      font-weight: 700;
+      color: rgb(240, 120, 21);
+    }
+  }
+
+  .coco-interactive-web-card-desc {
+    font-size: 12px;
+    color: rgb(107, 114, 128);
+    line-height: 1.6;
+  }
 }
 </style>
 
 <style>
 .chat table {
-    text-align: center;
-    border-spacing: 0;
-    border-left: 1px solid #000;
-    border-bottom: 1px solid #000;
+  text-align: center;
+  border-spacing: 0;
+  border-left: 1px solid #000;
+  border-bottom: 1px solid #000;
 }
 
 .chat table td,
 .chat table th {
-    border-top: 1px solid #000;
-    border-right: 1px solid #000;
-    padding: 10px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #000;
+  padding: 10px;
 }
 
 .message-files {
-    margin-top: 8px;
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
+  margin-top: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
 }
 
 .message-file-item {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    font-size: 12px;
-    color: #6b7280;
-    background: #f3f4f6;
-    padding: 4px 8px;
-    border-radius: 4px;
-    max-width: 200px;
-    overflow: hidden;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #6b7280;
+  background: #f3f4f6;
+  padding: 4px 8px;
+  border-radius: 4px;
+  max-width: 200px;
+  overflow: hidden;
 }
 
 .message-file-item span {
-    flex: 1;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 </style>

+ 180 - 0
src/components/CollapsibleToolbar/componets/aiWeb.vue

@@ -0,0 +1,180 @@
+<template>
+  <div v-if="visible" class="aiweb-modal-overlay" @click.self="closeModal">
+    <div class="aiweb-modal">
+      <div class="aiweb-modal-header">
+        <span class="aiweb-modal-title">{{ lang.ssWebEditor }}</span>
+        <button class="aiweb-modal-close" @click="closeModal">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M18 6L6 18"></path>
+            <path d="M6 6l12 12"></path>
+          </svg>
+        </button>
+      </div>
+      <div class="aiweb-modal-body">
+        <iframe :src="iframeUrl" frameborder="0" class="aiweb-iframe" ref="iframeRef"></iframe>
+      </div>
+      <div class="aiweb-modal-footer">
+        <button class="aiweb-btn aiweb-btn-close" @click="closeModal">{{ lang.ssClose }}</button>
+        <button class="aiweb-btn aiweb-btn-confirm" @click="handleConfirm" :disabled="isLoading">
+          {{ isLoading ? lang.ssCreating : lang.ssConfirmAdd }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { lang } from '@/main'
+
+const props = defineProps<{
+  visible: boolean
+  webId: string
+  isLoading: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'close'): void
+  (e: 'add', webCode: string): void
+}>()
+
+const iframeUrl = computed(() => {
+  let url = 'https://beta.app.cocorobo.cn'
+  if (window.location.href.includes('beta')) {
+    url = 'https://beta.app.cocorobo.cn'
+  }
+  else if (lang.lang === 'cn') {
+    url = 'https://app.cocorobo.cn'
+  }
+  else if (lang.lang === 'hk') {
+    url = 'https://app.cocorobo.hk'
+  }
+  else if (lang.lang === 'en') {
+    url = 'https://app.cocorobo.com'
+  }
+  if (!props.webId) return ''
+  return `${url}/#/web?id=${props.webId}&create=false&isPPT=true`
+})
+
+const closeModal = () => {
+  emit('close')
+}
+
+const iframeRef = ref<HTMLIFrameElement>()
+
+const handleConfirm = () => {
+  const webCode = iframeRef.value?.contentWindow?.ReturnWebCode() as string || ''
+  console.log(webCode)
+  if (!webCode) return
+  console.log('确定按钮被点击')
+  emit('add', webCode)
+}
+</script>
+
+<style lang="scss" scoped>
+.aiweb-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.aiweb-modal {
+  width: 90%;
+  max-width: 1200px;
+  height: 90vh;
+  background-color: #fff;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.aiweb-modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-modal-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.aiweb-modal-close {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 8px;
+  color: #666;
+  transition: color 0.2s;
+
+  &:hover {
+    color: #333;
+  }
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
+}
+
+.aiweb-modal-body {
+  height: calc(100% - 140px);
+}
+
+.aiweb-modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 20px;
+  border-top: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-btn {
+  padding: 8px 20px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid transparent;
+}
+
+.aiweb-btn-close {
+  background-color: #fff;
+  border-color: #d9d9d9;
+  color: #666;
+
+  &:hover {
+    background-color: #f5f5f5;
+    border-color: #bfbfbf;
+  }
+}
+
+.aiweb-btn-confirm {
+  background-color: #1890ff;
+  border-color: #1890ff;
+  color: #fff;
+
+  &:hover {
+    background-color: #40a9ff;
+    border-color: #40a9ff;
+  }
+}
+
+.aiweb-iframe {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 5 - 1
src/components/CollapsibleToolbar/index.vue

@@ -283,9 +283,13 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
-    75: lang.ssBiliVideo,
+    75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
+    77: lang.ssEnglishSpeakingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type || 0] || lang.ssUnknown
 }

Fichier diff supprimé car celui-ci est trop grand
+ 643 - 128
src/components/CollapsibleToolbar/index2.vue


+ 1649 - 0
src/components/CollapsibleToolbar/index22.vue

@@ -0,0 +1,1649 @@
+<template>
+  <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
+    <div class="toolbar-content" v-show="!isCollapsed">
+      <div class="sidebar-content">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }" @click="toggleSubmenu('cocoai')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
+            <path d="M2 17l10 5 10-5"></path>
+            <path d="M2 12l10 5 10-5"></path>
+          </svg>
+          <span class="item-label">Coco AI</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M12.8332 1.83398H5.49984C5.01361 1.83398 4.54729 2.02714 4.20347 2.37096C3.85966 2.71477 3.6665 3.18109 3.6665 3.66732V18.334C3.6665 18.8202 3.85966 19.2865 4.20347 19.6303C4.54729 19.9742 5.01361 20.1673 5.49984 20.1673H16.4998C16.9861 20.1673 17.4524 19.9742 17.7962 19.6303C18.14 19.2865 18.3332 18.8202 18.3332 18.334V7.33398L12.8332 1.83398Z"
+                stroke="currentColor" stroke-width="1.83333" />
+              <path id="Vector_2" d="M12.8335 1.83398V7.33398H18.3335" stroke="currentColor" stroke-width="1.83333" />
+            </g>
+          </svg>
+          <span class="item-label">{{ lang.ssPage }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }"
+          @click="toggleSubmenu('interactive')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Frame">
+              <path id="Vector"
+                d="M4.44727 14.6738V14.5449L5.84805 13.041H6.02422L9.43164 16.2766V7.46797L9.4875 7.41211H11.9969L12.0098 7.425V13.5137H16.7234L16.8781 13.6684V18.9707H18.5969V12.7789L17.6086 11.7949H13.5996V6.62578L12.7531 5.7793H8.73125L7.8418 6.66875V12.6844L6.60859 11.4512H5.23789L2.85742 13.9863V15.2324L6.35508 18.9707H8.60234L4.44727 14.6738Z"
+                fill="currentColor" />
+              <path id="Vector_2"
+                d="M2.49219 2.79297V10.5273H6.40234V8.76562H4.25391V4.55469H17.7461V8.76562H15.0391V10.5273H19.4219V2.79297H2.49219Z"
+                fill="currentColor" />
+            </g>
+          </svg>
+          <span class="item-label">{{ lang.ssInteract }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'aiapp' }" @click="toggleSubmenu('aiapp')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <rect x="3" y="3" width="7" height="7" />
+            <rect x="14" y="3" width="7" height="7" />
+            <rect x="14" y="14" width="7" height="7" />
+            <rect x="3" y="14" width="7" height="7" />
+          </svg>
+          <span class="item-label">{{ lang.ssAiApp }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'h5page' }" @click="toggleSubmenu('h5page')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10" />
+            <path d="M2 12h20" />
+            <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
+          </svg>
+          <span class="item-label">{{ lang.ssInteractiveWebpage }}</span>
+        </div>
+        <!-- <div class="sidebar-item" @click="handleToolClick('video')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <rect x="3" y="4" width="18" height="16" rx="2" ry="2" />
+            <polygon points="10 9 16 12 10 15 10 9" />
+          </svg>
+          <span class="item-label">{{ lang.ssVideo }}</span>
+        </div> -->
+        <!-- <div class="sidebar-item" @click="handleToolClick('creative')">
+          <svg class="item-icon" 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="16" />
+            <line x1="8" y1="12" x2="16" y2="12" />
+          </svg>
+          <span class="item-label">{{ lang.ssCreative }}</span>
+        </div> -->
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'multimedia' }"
+          @click="toggleSubmenu('multimedia')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M20.1668 17.4167C20.1668 17.9029 19.9737 18.3692 19.6299 18.713C19.286 19.0568 18.8197 19.25 18.3335 19.25H3.66683C3.1806 19.25 2.71428 19.0568 2.37047 18.713C2.02665 18.3692 1.8335 17.9029 1.8335 17.4167V4.58333C1.8335 4.0971 2.02665 3.63079 2.37047 3.28697C2.71428 2.94315 3.1806 2.75 3.66683 2.75H8.25016L10.0835 5.5H18.3335C18.8197 5.5 19.286 5.69315 19.6299 6.03697C19.9737 6.38079 20.1668 6.8471 20.1668 7.33333V17.4167Z"
+                stroke="currentColor" stroke-width="1.83333" />
+            </g>
+          </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' }">
+      <div class="submenu-title" style="margin-bottom: 0;">
+        <div class="title">Coco AI</div>
+        <div class="close-icon" @click="toggleSubmenu('cocoai')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-content">
+        <AiChat :userid="props.userid" />
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddTemplatePage }}</div>
+        <div class="close-icon" @click="toggleSubmenu('page')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('titlepage')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector" d="M8 14V8H40V14M18 40H30M24 8V40" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssTitlePage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ImagePage')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M38 6H10C7.79086 6 6 7.79086 6 10V38C6 40.2091 7.79086 42 10 42H38C40.2091 42 42 40.2091 42 38V10C42 7.79086 40.2091 6 38 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M17 20C18.6569 20 20 18.6569 20 17C20 15.3431 18.6569 14 17 14C15.3431 14 14 15.3431 14 17C14 18.6569 15.3431 20 17 20Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M42 30L32 20L10 42" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssImagePage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ContentPage')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M28 4H12C10.9391 4 9.92172 4.42143 9.17157 5.17157C8.42143 5.92172 8 6.93913 8 8V40C8 41.0609 8.42143 42.0783 9.17157 42.8284C9.92172 43.5786 10.9391 44 12 44H36C37.0609 44 38.0783 43.5786 38.8284 42.8284C39.5786 42.0783 40 41.0609 40 40V16L28 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M32 26H16" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M32 34H16" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssContentPage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ImageTextPage')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M18 6H8C6.89543 6 6 6.89543 6 8V40C6 41.1046 6.89543 42 8 42H18C19.1046 42 20 41.1046 20 40V8C20 6.89543 19.1046 6 18 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M40 6H30C28.8954 6 28 6.89543 28 8V40C28 41.1046 28.8954 42 30 42H40C41.1046 42 42 41.1046 42 40V8C42 6.89543 41.1046 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssImageTextPage }}</span>
+        </div>
+      </div>
+      <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
+        @change="handleFileUpload">
+        <div class="submenu-upload">
+          <div class="submenu-icon">
+            <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M24.5 17.5V22.1667C24.5 22.7855 24.2542 23.379 23.8166 23.8166C23.379 24.2542 22.7855 24.5 22.1667 24.5H5.83333C5.21449 24.5 4.621 24.2542 4.18342 23.8166C3.74583 23.379 3.5 22.7855 3.5 22.1667V17.5"
+                  stroke="currentColor" stroke-width="2.33333" />
+                <path id="Vector_2" d="M19.8334 9.33333L14.0001 3.5L8.16675 9.33333" stroke="currentColor"
+                  stroke-width="2.33333" />
+                <path id="Vector_3" d="M14 3.5V17.5" stroke="currentColor" stroke-width="2.33333" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadPPT }}</span>
+        </div>
+      </FileInput>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'interactive' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddInteractiveTool }}</div>
+        <div class="close-icon" @click="toggleSubmenu('interactive')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <transition name="fade" mode="out-in">
+        <div class="submenu-panel" v-if="!hoveredTool">
+          <svg key="svg" width="120" height="80" viewBox="0 0 120 80" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M102 15H18C13.5817 15 10 18.5817 10 23V57C10 61.4183 13.5817 65 18 65H102C106.418 65 110 61.4183 110 57V23C110 18.5817 106.418 15 102 15Z"
+                fill="#FFFAF0" stroke="#FF9300" stroke-width="2" />
+              <path id="Vector_2"
+                d="M30 34C32.2091 34 34 32.2091 34 30C34 27.7909 32.2091 26 30 26C27.7909 26 26 27.7909 26 30C26 32.2091 27.7909 34 30 34Z"
+                fill="#FF9300" />
+              <path id="Vector_3"
+                d="M97 27H43C41.3431 27 40 28.3431 40 30C40 31.6569 41.3431 33 43 33H97C98.6569 33 100 31.6569 100 30C100 28.3431 98.6569 27 97 27Z"
+                fill="#FFD9A8" />
+              <path id="Vector_4"
+                d="M30 49C32.2091 49 34 47.2091 34 45C34 42.7909 32.2091 41 30 41C27.7909 41 26 42.7909 26 45C26 47.2091 27.7909 49 30 49Z"
+                fill="#D1D5DB" />
+              <path id="Vector_5"
+                d="M82 42H43C41.3431 42 40 43.3431 40 45C40 46.6569 41.3431 48 43 48H82C83.6569 48 85 46.6569 85 45C85 43.3431 83.6569 42 82 42Z"
+                fill="#E5E7EB" />
+            </g>
+          </svg>
+          <div class="detail">{{ lang.ssSelectToolCreateInteractive }}</div>
+        </div>
+        <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'vote'" key="vote" :src="toolVote" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'photo'" key="photo" :src="toolPhoto" alt="">
+      </transition>
+      <div class="submenu-item-box">
+        <div class="submenu-item" @click="handleToolClick('choice')" @mouseenter="hoveredTool = 'choice'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10" />
+            <path d="M12 16v-4m0-4h.01" />
+          </svg>
+          <span class="submenu-label">{{ lang.ssChoiceQ }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('qa')" @mouseenter="hoveredTool = 'qa'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" 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>
+          <span class="submenu-label">{{ lang.ssQandA }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <polyline points="9 11 12 14 22 4"></polyline>
+              <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+          </svg>
+          <span class="submenu-label">{{ lang.ssVote }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+              <circle cx="12" cy="13" r="4"></circle>
+          </svg>
+          <span class="submenu-label">{{ lang.ssPhoto }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" 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="16"/>
+            <line x1="8" y1="12" x2="16" y2="12"/>
+          </svg>
+          <span class="submenu-label">{{ lang.ssCreative }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'aiapp' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddAIApp }}</div>
+        <div class="close-icon" @click="toggleSubmenu('aiapp')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('aiapp')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M18 6H8C6.89543 6 6 6.89543 6 8V18C6 19.1046 6.89543 20 8 20H18C19.1046 20 20 19.1046 20 18V8C20 6.89543 19.1046 6 18 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M40 6H30C28.8954 6 28 6.89543 28 8V18C28 19.1046 28.8954 20 30 20H40C41.1046 20 42 19.1046 42 18V8C42 6.89543 41.1046 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M40 28H30C28.8954 28 28 28.8954 28 30V40C28 41.1046 28.8954 42 30 42H40C41.1046 42 42 41.1046 42 40V30C42 28.8954 41.1046 28 40 28Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4"
+                  d="M18 28H8C6.89543 28 6 28.8954 6 30V40C6 41.1046 6.89543 42 8 42H18C19.1046 42 20 41.1046 20 40V30C20 28.8954 19.1046 28 18 28Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssAppCenter }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('createApp')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M24 16V32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M16 24H32" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssCreateApp }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'h5page' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddInteractiveWebpage }}</div>
+        <div class="close-icon" @click="toggleSubmenu('h5page')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-item-box2">
+        <!-- <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M40 6H8C5.79086 6 4 7.79086 4 10V30C4 32.2091 5.79086 34 8 34H40C42.2091 34 44 32.2091 44 30V10C44 7.79086 42.2091 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M4 14H44" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M10 11C10.5523 11 11 10.5523 11 10C11 9.44772 10.5523 9 10 9C9.44772 9 9 9.44772 9 10C9 10.5523 9.44772 11 10 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4"
+                  d="M14 11C14.5523 11 15 10.5523 15 10C15 9.44772 14.5523 9 14 9C13.4477 9 13 9.44772 13 10C13 10.5523 13.4477 11 14 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_5"
+                  d="M18 11C18.5523 11 19 10.5523 19 10C19 9.44772 18.5523 9 18 9C17.4477 9 17 9.44772 17 10C17 10.5523 17.4477 11 18 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_6" d="M16 22H32M16 28H26" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssWebpageCenter }}</span>
+        </div> -->
+        <div class="submenu-item" @click="handleToolClick('uploadWebpage')">
+          <div class="submenu-icon">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <circle cx="12" cy="12" r="10"></circle>
+              <path d="M2 12h20"></path>
+              <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"></path>
+              <path d="M16 8l-4 4-4-4" stroke-width="1.5"></path>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadWebpageLink }}</span>
+        </div>
+        <!-- <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M42 30V38C42 39.0609 41.5786 40.0783 40.8284 40.8284C40.0783 41.5786 39.0609 42 38 42H10C8.93913 42 7.92172 41.5786 7.17157 40.8284C6.42143 40.0783 6 39.0609 6 38V30"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M34 16L24 6L14 16" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M24 6V30" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
+        </div> -->
+        <div class="submenu-item" @click="handleToolClick('createWebpage')">
+          <div class="submenu-icon">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
+              <path d="M12 8v6"></path>
+              <path d="M9 11h6"></path>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssNewWebpage }}</span>
+        </div>
+        <!-- <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M4 24H44" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M24 4C29.0026 9.47671 31.8455 16.5841 32 24C31.8455 31.4159 29.0026 38.5233 24 44C18.9974 38.5233 16.1545 31.4159 16 24C16.1545 16.5841 18.9974 9.47671 24 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M32 16L24 24L16 16" stroke="currentColor" stroke-width="3" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssCrawlWebpage }}</span>
+        </div> -->
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadWebpage' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssUploadWebpageLink }}</div>
+        <div class="close-icon" @click="activeSubmenu = 'h5page'">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="line_box">
+        <div class="webpage-link-container">
+          <h3 class="webpage-link-title">{{ lang.ssWebpageLink }}</h3>
+          <input type="text" class="webpage-link-input" :placeholder="lang.ssEnterCompleteUrl" v-model="webpageUrl"
+            @input="handleUrlInput" />
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'error': isValidUrl === false, 'disabled': isValidUrl === null || isLoading }"
+            :disabled="isValidUrl === null || isLoading || isValidUrl === false" @click="uploadWebpageLink">
+            {{ isLoading ? lang.ssUploading : isValidUrl === null ? lang.ssWaitingForInput : isValidUrl === false ?
+              lang.ssInvalidUrl : lang.ssStartUpload }}
+          </button>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'multimedia' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddMultimedia }}</div>
+        <div class="close-icon" @click="toggleSubmenu('multimedia')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('video')">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M38 8H10C7.79086 8 6 9.79086 6 12V36C6 38.2091 7.79086 40 10 40H38C40.2091 40 42 38.2091 42 36V12C42 9.79086 40.2091 8 38 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M20 18L32 24L20 30V18Z" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssVideo }}</span>
+        </div>
+        <!-- <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector" d="M18 36V10L42 6V32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M12 42C15.3137 42 18 39.3137 18 36C18 32.6863 15.3137 30 12 30C8.68629 30 6 32.6863 6 36C6 39.3137 8.68629 42 12 42Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M36 38C39.3137 38 42 35.3137 42 32C42 28.6863 39.3137 26 36 26C32.6863 26 30 28.6863 30 32C30 35.3137 32.6863 38 36 38Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssAudio }}</span>
+        </div>
+        <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M28 4H12C10.9391 4 9.92172 4.42143 9.17157 5.17157C8.42143 5.92172 8 6.93913 8 8V40C8 41.0609 8.42143 42.0783 9.17157 42.8284C9.92172 43.5786 10.9391 44 12 44H36C37.0609 44 38.0783 43.5786 38.8284 42.8284C39.5786 42.0783 40 41.0609 40 40V16L28 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M28 4V16H40" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M16 26H32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M16 34H28" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssDocument }}</span>
+        </div>
+        <div class="submenu-item">
+          <div class="submenu-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M18 8H10C7.79086 8 6 9.79086 6 12V32C6 34.2091 7.79086 36 10 36H18C20.2091 36 22 34.2091 22 32V12C22 9.79086 20.2091 8 18 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M38 8H30C27.7909 8 26 9.79086 26 12V32C26 34.2091 27.7909 36 30 36H38C40.2091 36 42 34.2091 42 32V12C42 9.79086 40.2091 8 38 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M14 18V26" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M34 18V26" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssDocumentSet }}</span>
+        </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">
+        <div class="loading-spinner" v-if="exporting"></div>
+        <div class="success-icon" v-if="!exporting">
+          <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector" d="M5.41675 14.084L9.75008 18.4173L20.5834 7.58398" stroke="#FF9300"
+                stroke-width="2.16667" stroke-linecap="round" stroke-linejoin="round" />
+            </g>
+          </svg>
+
+        </div>
+        <h3>{{ exporting ? lang.ssParsing : lang.ssExportCompleted }}</h3>
+        <p v-if="exporting">{{ lang.ssParsingFile }}{{ currentFileName }}</p>
+        <p v-if="!exporting">{{ lang.ssParsingCompleted }}</p>
+        <button class="close-btn2" @click="handleParsingClose">
+          {{ exporting ? lang.ssClose : lang.ssComplete }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+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'
+import toolVote from '@/assets/img/tool_vote.png'
+import toolPhoto from '@/assets/img/tool_photo.png'
+
+
+
+interface ContentItem {
+  tool?: number
+  title?: string
+  url?: string
+  id?: string
+}
+
+
+
+const props = withDefaults(defineProps<{
+  defaultCollapsed?: boolean
+  userid?: string | null
+}>(), {
+  defaultCollapsed: false,
+  userid: null,
+})
+
+const emit = defineEmits<{
+  (e: 'toggle', collapsed: boolean): void
+}>()
+
+const isCollapsed = ref(props.defaultCollapsed)
+const activeSubmenu = ref<string | null>(null)
+const contentList = ref<ContentItem[]>([])
+const hoveredTool = ref<string | null>(null)
+const webpageUrl = ref('')
+const isLoading = ref(false)
+const isValidUrl = ref<boolean | null>(null) // null: 未输入, true: 有效, false: 无效
+
+const slidesStore = useSlidesStore()
+const { currentSlide } = storeToRefs(slidesStore)
+
+const { createFrameElement } = useCreateElement()
+const { createSlide, createSlideByTemplate } = useSlideHandler()
+
+
+
+import _ from 'lodash'
+
+const handleUrlInput = _.debounce(() => {
+  const url = webpageUrl.value.trim()
+  if (!url) {
+    isValidUrl.value = null
+  }
+  else {
+    // 简化的URL格式验证,支持各种复杂URL
+    const urlRegex = /^https?:\/\/.+/
+    isValidUrl.value = urlRegex.test(url)
+  }
+  console.log('URL输入:', webpageUrl.value, '验证结果:', isValidUrl.value)
+}, 300)
+const uploadWebpageLink = async () => {
+  if (!webpageUrl.value || isValidUrl.value !== true) {
+    // 可以添加提示信息
+    return
+  }
+
+  isLoading.value = true
+
+  // 模拟上传过程
+  // isLoading.value = false
+  // 上传成功后创建iframe元素
+
+  const isValid = await new Promise((resolve) => {
+    // 创建隐藏iframe
+    const iframe = document.createElement('iframe')
+    iframe.style.display = 'none'
+    iframe.src = webpageUrl.value
+    let finished = false
+    const timeout = setTimeout(() => {
+      if (finished) return
+      finished = true
+      // 超时,移除iframe,进入XHR判断
+      document.body.removeChild(iframe)
+      // 用XHR判断
+      const xhr = new XMLHttpRequest()
+      xhr.open('GET', iframe.src, true)
+      xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+          if (xhr.status === 200) {
+            resolve(true)
+          }
+          else {
+            // 再试一次 getFile
+            getFile(iframe.src as any).then(res => {
+              if (res && res.data && res.data !== 1) {
+                resolve(true)
+              }
+              else {
+                resolve(false)
+              }
+            }).catch(() => {
+              resolve(false)
+            })
+          }
+        }
+      }.bind(this)
+      xhr.onerror = function() {
+        // 再试一次 getFile
+        getFile(iframe.src as any).then(res => {
+          if (res && res.data && res.data !== 1) {
+            resolve(true)
+          }
+          else {
+            resolve(false)
+          }
+        }).catch(() => {
+          resolve(false)
+        })
+      }.bind(this)
+      xhr.send()
+    }, 5000)
+
+    iframe.onload = function() {
+      if (finished) return
+      finished = true
+      clearTimeout(timeout)
+      try {
+        // 尝试访问contentWindow.document
+        const doc = iframe?.contentWindow?.document
+        document.body.removeChild(iframe)
+        resolve(true)
+      }
+      catch (e) {
+        // 跨域或其他异常,移除iframe,进入XHR判断
+        document.body.removeChild(iframe)
+        const xhr = new XMLHttpRequest()
+        xhr.open('GET', iframe.src, true)
+        xhr.onreadystatechange = function() {
+          if (xhr.readyState === 4) {
+            if (xhr.status === 200) {
+              resolve(true)
+            }
+            else {
+              // 再试一次 getFile
+              getFile(iframe.src as any).then(res => {
+                if (res && res.data && res.data !== 1) {
+                  resolve(true)
+                }
+                else {
+                  resolve(false)
+                }
+              }).catch(() => {
+                resolve(false)
+              })
+            }
+          }
+        }.bind(this)
+        xhr.onerror = function() {
+          // 再试一次 getFile
+          getFile(iframe.src as any).then(res => {
+            if (res && res.data && res.data !== 1) {
+              resolve(true)
+            }
+            else {
+              resolve(false)
+            }
+          }).catch(() => {
+            resolve(false)
+          })
+        }.bind(this)
+        xhr.send()
+      }
+    }
+    iframe.onerror = function() {
+      if (finished) return
+      finished = true
+      clearTimeout(timeout)
+      document.body.removeChild(iframe)
+      // iframe加载失败,进入XHR判断
+      const xhr = new XMLHttpRequest()
+      xhr.open('GET', iframe.src, true)
+      xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+          if (xhr.status === 200) {
+            resolve(true)
+          }
+          else {
+            // 再试一次 getFile
+            getFile(iframe.src as any).then(res => {
+              if (res && res.data && res.data !== 1) {
+                resolve(true)
+              }
+              else {
+                resolve(false)
+              }
+            }).catch(() => {
+              resolve(false)
+            })
+          }
+        }
+      }.bind(this)
+      xhr.onerror = function() {
+        // 再试一次 getFile
+        getFile(iframe.src as any).then(res => {
+          if (res && res.data && res.data !== 1) {
+            resolve(true)
+          }
+          else {
+            resolve(false)
+          }
+        }).catch(() => {
+          resolve(false)
+        })
+      }.bind(this)
+      xhr.send()
+    }
+    document.body.appendChild(iframe)
+  })
+
+  if (!isValid) {
+    message.error(lang.ssCocoLinkTip)
+    isLoading.value = false
+    return
+  }
+  isLoading.value = false
+
+  createSlide()
+  createFrameElement(webpageUrl.value, 73) // 假设15是网页工具的类型
+  // 清空输入框和验证状态
+  webpageUrl.value = ''
+  isValidUrl.value = null
+}
+
+const toggleCollapse = () => {
+  isCollapsed.value = !isCollapsed.value
+  emit('toggle', isCollapsed.value)
+}
+
+const toggleSubmenu = (menu: string) => {
+  if (activeSubmenu.value === menu) {
+    activeSubmenu.value = null
+  }
+  else {
+    activeSubmenu.value = menu
+  }
+}
+
+// 监听"打开口语配置面板"信号 → 自动展开 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'
+import ImageTextPage from './page/ImageTextPage.json'
+
+const handleToolClick = _.debounce((tool: string) => {
+  interface ParentWindowWithToolList extends Window {
+    addTool?: (id: number) => void;
+    openVideoUploadDialog?: () => void;
+    openApplicationCenter?: () => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  console.log('点击工具:', tool)
+  if (tool === 'h5page') {
+    parentWindow?.addTool?.(73)
+  }
+  else if (tool === 'aiapp') {
+    parentWindow?.addTool?.(72)
+  }
+  else if (tool === 'createApp') {
+    if (lang.lang === 'cn') {
+      window.open('https://cloud.cocorobo.cn/admin.html?type=cocoflow1', '_blank')
+    }
+    else if (lang.lang === 'en') {
+      window.open('https://cloud.cocorobo.com/admin.html?type=cocoflow1', '_blank')
+    }
+    else if (lang.lang === 'hk') {
+      window.open('https://cloud.cocorobo.hk/admin.html?type=cocoflow1', '_blank')
+    }
+  }
+  else if (tool === 'video') {
+    parentWindow?.openVideoUploadDialog?.()
+  }
+  else if (tool === 'creative') {
+    parentWindow?.openApplicationCenter?.()
+  }
+  else if (tool === 'choice') {
+    parentWindow?.addTool?.(45)
+  }
+  else if (tool === 'vote') {
+    parentWindow?.addTool?.(78)
+  }
+  else if (tool === 'photo') {
+    parentWindow?.addTool?.(79)
+  } 
+  else if (tool === 'qa') {
+    parentWindow?.addTool?.(15)
+  }
+  else if (tool === 'titlepage') {
+    createSlideByTemplate(titlePage)
+  }
+  else if (tool === 'ImagePage') {
+    createSlideByTemplate(ImagePage)
+  }
+  else if (tool === 'ContentPage') {
+    createSlideByTemplate(ContentPage)
+  }
+  else if (tool === 'ImageTextPage') {
+    createSlideByTemplate(ImageTextPage)
+  }
+  else if (tool === 'createWebpage') {
+    if (lang.lang === 'cn') {
+      window.open('https://cloud.cocorobo.cn/admin.html?type=cocoflow3', '_blank')
+    }
+    else if (lang.lang === 'en') {
+      window.open('https://cloud.cocorobo.com/admin.html?type=cocoflow3', '_blank')
+    }
+    else if (lang.lang === 'hk') {
+      window.open('https://cloud.cocorobo.hk/admin.html?type=cocoflow3', '_blank')
+    }
+  }
+  else if (tool === 'uploadWebpage') {
+    activeSubmenu.value = 'uploadWebpage'
+  }
+}, 300)
+
+const loadContentList = () => {
+  try {
+    interface ParentWindowWithToolList extends Window {
+      pptToolList?: ContentItem[]
+    }
+    const parentWindow = window.parent as ParentWindowWithToolList
+    contentList.value = parentWindow?.pptToolList || []
+  }
+  catch (error) {
+    console.error('加载内容列表失败:', error)
+    contentList.value = []
+  }
+}
+
+const insertContent = (item: ContentItem) => {
+  if (!item.tool || !item.url) return
+  createFrameElement(item.url, item.tool)
+}
+
+const addContent = (data: ContentItem, type: number) => {
+  // contentList.value.push(data)
+  if (type === 2) {
+    const elements = currentSlide.value?.elements || []
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
+    if (frameElement) {
+      slidesStore.updateElement({
+        id: frameElement.id,
+        props: { url: data.url, toolType: data.tool }
+      })
+    }
+  }
+  else {
+    createSlide()
+    insertContent(data)
+  }
+}
+
+Object.assign(window, { addContent, loadContentList })
+// window.loadContentList = loadContentList
+// window.addContent = addContent
+
+const previewVideo = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    previewVideo?: (item: ContentItem) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.previewVideo?.(item)
+}
+
+const editContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(0, item.id || '')
+}
+
+const copyContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(1, item.id || '')
+}
+
+const deleteContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(2, item.id || '')
+}
+
+const getTypeLabel = (type?: number) => {
+  const typeMap: Record<number, string> = {
+    45: lang.ssChoiceQ,
+    15: lang.ssQATest,
+    72: lang.ssAiApp,
+    73: lang.ssHPage,
+    81: lang.ssHPage,
+    74: lang.ssVideo,
+    75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+    76: lang.ssCreative,
+    77: lang.ssEnglishSpeakingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
+  }
+  return typeMap[type || 0] || lang.ssUnknown
+}
+
+const getTypeClass = (type?: number) => {
+  const classMap: Record<number, string> = {
+    45: 'type-choice',
+    15: 'type-question',
+    72: 'type-ai',
+    73: 'type-h5',
+    74: 'type-video',
+    75: 'type-bilibili',
+    76: 'type-app-center'
+  }
+  return classMap[type || 0] || 'type-default'
+}
+
+import useImport from '@/hooks/useImport'
+import message from '@/utils/message'
+const { importPPTXFile, exporting, getFile } = useImport()
+const currentFileName = ref('')
+const parsingStatus = ref<'parsing' | 'success'>('parsing')
+const parsingAbortController = ref<AbortController | null>(null)
+
+const handleFileUpload = async (files: FileList) => {
+  if (!files || files.length === 0) return
+
+  const file = files[0]
+  currentFileName.value = file.name
+
+  try {
+    // 创建AbortController用于取消操作
+    parsingAbortController.value = new AbortController()
+    const signal = parsingAbortController.value.signal
+
+    // 调用importPPTXFile并传入signal
+    await importPPTXFile(files, { signal })
+  }
+  catch (error) {
+    if (error instanceof DOMException && error.name === 'AbortError') {
+      console.log(lang.ssFileParseCancelled)
+    }
+    else {
+      console.error(lang.ssFileParseFailed, error)
+      message.error(lang.ssFileParseFailedRetry)
+    }
+  }
+}
+
+const handleParsingClose = () => {
+  if (exporting.value && parsingAbortController.value) {
+    parsingAbortController.value.abort()
+    exporting.value = false
+    parsingAbortController.value = null
+    // message.info(lang.ssParseCancelled)
+  }
+  else if (!exporting.value) {
+    emit('close')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.collapsible-toolbar {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  background: #fff;
+  border-right: 1px solid #e5e7eb;
+  transition: width 0.3s ease;
+}
+
+.toolbar-content {
+  flex: 1;
+  overflow: hidden;
+  padding: 16px 8px;
+}
+
+.sidebar-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  width: 84px;
+  position: relative;
+}
+
+.sidebar-item {
+  width: 84px;
+  padding: 12px 8px;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+  position: relative;
+
+  &:hover {
+    background: #f3f4f6;
+  }
+
+  &:active {
+    background: #e5e7eb;
+  }
+
+  &.active {
+    background: #fff5e5;
+    box-shadow: 0 2px 8px rgba(40, 92, 245, 0.15);
+  }
+
+  &.active::after {
+    content: '';
+    position: absolute;
+    left: -8px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 4px;
+    height: 32px;
+    background: #FF9300;
+    border-radius: 0 2px 2px 0;
+  }
+}
+
+.item-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
+  color: #6b7280;
+}
+
+.sidebar-item:hover .item-icon,
+.sidebar-item.active .item-icon {
+  color: #FF9300;
+}
+
+.item-label {
+  font-size: 11px;
+  font-weight: 500;
+  color: #6b7280;
+  text-align: center;
+}
+
+.sidebar-item:hover .item-label,
+.sidebar-item.active .item-label {
+  color: #FF9300;
+  font-weight: 600;
+}
+
+.submenu {
+  width: 0;
+  min-width: 0;
+  overflow: hidden;
+  // transition: all 0.3s ease;
+  background: #fff;
+  border-radius: 0 12px 12px 0;
+  z-index: 100;
+
+  &.visible {
+    width: 420px;
+    // padding: 16px;
+    border-left: 1px solid #E5E7EB;
+  }
+
+  @media screen and (max-width: 1920px) {
+    &.visible {
+      width: 360px;
+    }
+  }
+
+  @media screen and (max-width: 1440px) {
+    &.visible {
+      width: 320px;
+    }
+  }
+
+  &.submenu-english {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
+.submenu-title {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 20px;
+  width: 100%;
+  box-sizing: border-box;
+  padding: 12px 15px;
+
+  .title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .close-icon {
+    width: 28px;
+    height: 28px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background-color: #f3f4f6;
+
+      svg {
+        stroke: #6b7280;
+      }
+    }
+
+    svg {
+      width: 25px;
+      height: 25px;
+      stroke: #9ca3af;
+      transition: all 0.2s ease;
+    }
+  }
+}
+
+.submenu-content {
+  width: 100%;
+  height: calc(100% - 50px);
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+
+.submenu-img {
+  width: calc(100% - 30px);
+  margin: 0 auto;
+  height: 130px;
+  display: flex;
+  object-fit: cover;
+  border-radius: 10px;
+  margin-bottom: 20px;
+}
+
+.submenu-panel {
+  width: calc(100% - 30px);
+  height: 130px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 15px 0;
+  margin-bottom: 20px;
+  background: #FFFAF0;
+  border-radius: 10px;
+  box-sizing: border-box;
+
+  svg,
+  img {
+    width: 120px;
+    height: auto;
+    object-fit: cover;
+  }
+
+  .detail {
+    font-size: 12px;
+    font-weight: 500;
+    color: #6B7280;
+    text-align: center;
+    margin-top: 5px;
+  }
+}
+
+/* 淡入淡出过渡效果 */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+  transform: scale(0.95);
+}
+
+.fade-enter-to,
+.fade-leave-from {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.submenu-item-box {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding: 0 15px;
+
+  .submenu-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 14px;
+    color: #333;
+    border-radius: 12px;
+    background: #fff;
+    border: 1px solid #e5e7eb;
+
+    &:hover {
+      background-color: #fffdfa;
+      border-color: #fffdfa;
+      color: #FF9300;
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+    }
+
+    &:active {
+      background-color: #fffdfa;
+      transform: translateY(0);
+    }
+  }
+
+  .submenu-icon {
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+    color: #6b7280;
+    background: #fffaf0;
+    border-radius: 10px;
+    padding: 8px;
+    border: 1px solid #e5e7eb;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .submenu-item:hover .submenu-icon {
+    color: #FF9300;
+    border-color: #FF9300;
+    box-shadow: 0 0 0 2px rgba(255, 147, 0, 0.1);
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    flex: 1;
+  }
+
+  .submenu-item:hover .submenu-label {
+    color: #FF9300;
+    font-weight: 600;
+  }
+}
+
+.submenu-item-box2 {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding: 0 15px;
+
+  .submenu-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 14px;
+    color: #333;
+    border-radius: 12px;
+    border: 1px solid #e5e7eb;
+    flex-direction: column;
+    background: #fafbfc;
+
+    &:hover {
+      background-color: #fffdfa;
+      border-color: #fffdfa;
+      color: #FF9300;
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+    }
+
+    &:active {
+      background-color: #fffdfa;
+      transform: translateY(0);
+    }
+  }
+
+  .submenu-icon {
+    flex-shrink: 0;
+    color: #6b7280;
+    background: #fff;
+    border-radius: 10px;
+    padding: 25px;
+    border: 1px solid #e5e7eb;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 95%;
+
+    svg {
+      width: 40px;
+      height: 40px;
+      color: #d7dbe0;
+    }
+  }
+
+  .submenu-item:hover .submenu-icon {
+    svg {
+      color: #FF9300;
+    }
+
+    border-color: #FF9300;
+    box-shadow: 0 0 0 2px rgba(255, 147, 0, 0.1);
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+    flex: 1;
+  }
+
+  .submenu-item:hover .submenu-label {
+    color: #FF9300;
+    font-weight: 600;
+  }
+}
+
+.submenu-upload {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-size: 14px;
+  color: #333;
+  border-radius: 12px;
+  border: 1px solid #e5e7eb;
+  background: #fafbfc;
+  width: calc(100% - 30px);
+  margin: 10px auto 0;
+  justify-content: center;
+
+  .submenu-icon {
+    width: 20px;
+    height: 20px;
+
+    svg {
+      color: #d7dbe0;
+      width: 20px;
+      height: 20px;
+    }
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+
+  }
+
+  &:hover {
+    background-color: #fffdfa;
+    border-color: #fffdfa;
+    color: #FF9300;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+
+
+    .submenu-icon {
+
+      svg {
+        color: #FF9300;
+      }
+    }
+
+    .submenu-label {
+      color: #FF9300;
+    }
+  }
+}
+
+
+.parsing-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+
+  .parsing-content {
+    background: white;
+    border-radius: 12px;
+    padding: 24px;
+    width: 400px;
+    text-align: center;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+
+    .loading-spinner {
+      width: 48px;
+      height: 48px;
+      border: 4px solid #f0f0f0;
+      border-top: 4px solid #FF9300;
+      border-radius: 50%;
+      margin: 0 auto 20px;
+      animation: spin 1s linear infinite;
+    }
+
+    .success-icon {
+      width: 48px;
+      height: 48px;
+      margin: 0 auto 20px;
+      background: #FFFAF0;
+      border-radius: 5px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: white;
+      font-size: 24px;
+      font-weight: bold;
+    }
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: #333;
+      margin: 0 0 12px;
+    }
+
+    p {
+      font-size: 14px;
+      color: #666;
+      margin: 0 0 24px;
+    }
+
+    .close-btn2 {
+      background: #FF9300;
+      color: white;
+      border: none;
+      border-radius: 8px;
+      padding: 12px 24px;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      width: 100%;
+      transition: all 0.3s;
+
+      &:hover {
+        background: #e68a00;
+      }
+    }
+  }
+
+  @keyframes spin {
+    0% {
+      transform: rotate(0deg);
+    }
+
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+
+  .close-btn {
+    width: 32px;
+    height: 32px;
+    border: none;
+    background: none;
+    cursor: pointer;
+    color: #999;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 4px;
+    transition: all 0.2s;
+
+    &:hover {
+      background: #f0f0f0;
+      color: #666;
+    }
+
+    svg {
+      width: 16px;
+      height: 16px;
+    }
+  }
+}
+
+.line_box {
+  .webpage-link-container {
+    padding: 20px;
+    // text-align: center;
+
+    .webpage-link-title {
+      margin: 0 0 16px;
+      font-size: 14px;
+      font-weight: 600;
+      color: #333;
+    }
+
+    .webpage-link-input {
+      width: 100%;
+      padding: 12px 12px;
+      border: 1px solid #d9d9d9;
+      border-radius: 8px;
+      font-size: 14px;
+      margin-bottom: 16px;
+      transition: all 0.3s;
+      box-sizing: border-box;
+
+      &:focus {
+        outline: none;
+        border-color: #FF9300;
+        background: #fff8f0;
+      }
+    }
+
+    .webpage-link-button {
+      text-align: center;
+      padding: 10px 24px;
+      border: none;
+      border-radius: 4px;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.3s;
+      background-color: #FF9300;
+      color: white;
+      margin: 0 auto;
+      display: block;
+
+      &:hover:not(:disabled) {
+        background-color: #e68a00;
+      }
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+
+      &.loading {
+        cursor: not-allowed;
+        opacity: 0.8;
+      }
+
+      &.error {
+        background-color: #ff4d4f;
+        color: white;
+
+        &:hover:not(:disabled) {
+          background-color: #ff7875;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 0
src/components/ColorButton.vue

@@ -33,6 +33,7 @@ defineProps<{
 .content {
   width: 100%;
   height: 100%;
+  padding: 0;
 }
 .color-btn-icon {
   width: 32px;

+ 143 - 0
src/components/Contextmenu2/MenuContent.vue

@@ -0,0 +1,143 @@
+<template>
+  <ul class="menu-content" 
+    :style="{width: lang.lang === 'en' ? '160px' : '120px'}"
+  >
+    <template v-for="(menu, index) in menus" :key="menu.text || index">
+      <li
+        v-if="!menu.hide"
+        class="menu-item"
+        @click.stop="handleClickMenuItem(menu)"
+        :class="{'divider': menu.divider, 'disable': menu.disable}"
+      >
+        <div 
+          class="menu-item-content" 
+          :class="{
+            'has-children': menu.children,
+            'has-handler': menu.handler,
+          }" 
+          v-if="!menu.divider"
+        >
+          <span class="text">{{menu.text}}</span>
+          <span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
+
+          <menu-content 
+            class="sub-menu"
+            :menus="menu.children" 
+            v-if="menu.children && menu.children.length"
+            :handleClickMenuItem="handleClickMenuItem" 
+          />
+        </div>
+      </li>
+    </template>
+  </ul>
+</template>
+
+<script lang="ts" setup>
+import type { ContextmenuItem } from './types'
+import { lang } from '@/main'
+defineProps<{
+  menus: ContextmenuItem[]
+  handleClickMenuItem: (item: ContextmenuItem) => void
+}>()
+</script>
+
+<style lang="scss" scoped>
+$menuWidth: 180px;
+$menuHeight: 30px;
+$subMenuWidth: 120px;
+
+.menu-content {
+  width: $menuWidth;
+  padding: 8px;
+  // background: #fff;
+  background: #fff;
+  box-shadow: $boxShadow;
+  border-radius: 10px;
+  list-style: none;
+  margin: 0;
+}
+.menu-item {
+  // padding: 0 20px;
+  color: #555;
+  font-size: 14px;
+  transition: all $transitionDelayFast;
+  white-space: nowrap;
+  height: 40px;
+  line-height: 40px;
+  // background-color: #fff;
+  background-color: #fff;
+  cursor: pointer;
+  border-radius: 10px;
+
+  &:not(.disable):hover > .menu-item-content > .sub-menu {
+    display: block;
+  }
+
+  &:not(.disable):hover > .has-children.has-handler::after {
+    transform: scale(1);
+  }
+
+  &:hover:not(.disable) {
+    // background-color: rgba($color: $themeColor, $alpha: .2);
+    background-color: rgba($color: #ff9300, $alpha: .2);
+  }
+
+  &.divider {
+    height: 1px;
+    overflow: hidden;
+    margin: 5px;
+    background-color: #e5e5e5;
+    line-height: 0;
+    padding: 0;
+  }
+
+  &.disable {
+    color: #b1b1b1;
+    cursor: no-drop;
+  }
+}
+.menu-item-content {
+  display: flex;
+  align-items: center;
+  // justify-content: space-between;
+  justify-content: center;
+  position: relative;
+
+  &.has-children::before {
+    content: '';
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: #666 #666 transparent transparent;
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%) rotate(45deg);
+  }
+  &.has-children.has-handler::after {
+    content: '';
+    display: inline-block;
+    width: 1px;
+    height: 24px;
+    background-color: rgba($color: #fff, $alpha: .3);
+    position: absolute;
+    right: 18px;
+    top: 3px;
+    transform: scale(0);
+    transition: transform $transitionDelay;
+  }
+
+  .sub-text {
+    opacity: 0.6;
+  }
+  .sub-menu {
+    width: $subMenuWidth;
+    position: absolute;
+    display: none;
+    left: 112%;
+    top: -6px;
+  }
+}
+</style>

+ 80 - 0
src/components/Contextmenu2/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div 
+    class="mask"
+    @contextmenu.prevent="removeContextmenu()"
+    @mousedown.left="removeContextmenu()"
+  ></div>
+
+  <div 
+    class="contextmenu"
+    :style="{
+      left: style.left + 'px',
+      top: style.top + 'px',
+    }"
+    @contextmenu.prevent
+  >
+    <MenuContent 
+      :menus="menus"
+      :handleClickMenuItem="handleClickMenuItem" 
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import type { ContextmenuItem, Axis } from './types'
+
+import MenuContent from './MenuContent.vue'
+
+const props = defineProps<{
+  axis: Axis
+  el: HTMLElement
+  menus: ContextmenuItem[]
+  removeContextmenu: () => void
+}>()
+
+const style = computed(() => {
+  const MENU_WIDTH = 180
+  const MENU_HEIGHT = 30
+  const DIVIDER_HEIGHT = 11
+  const PADDING = 5
+
+  const { x, y } = props.axis
+  const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
+  const dividerCount = props.menus.filter(menu => menu.divider).length
+
+  const menuWidth = MENU_WIDTH
+  const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
+
+  const screenWidth = document.body.clientWidth
+  const screenHeight = document.body.clientHeight
+
+  return {
+    left: screenWidth <= x + menuWidth ? x - menuWidth : x,
+    top: screenHeight <= y + menuHeight ? y - menuHeight : y,
+  }
+})
+
+const handleClickMenuItem = (item: ContextmenuItem) => {
+  if (item.disable) return
+  if (item.children && !item.handler) return
+  if (item.handler) item.handler(props.el)
+  props.removeContextmenu()
+}
+</script>
+
+<style lang="scss">
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 9998;
+}
+.contextmenu {
+  position: fixed;
+  z-index: 9999;
+  user-select: none;
+}
+</style>

+ 14 - 0
src/components/Contextmenu2/types.ts

@@ -0,0 +1,14 @@
+export interface ContextmenuItem {
+  text?: string
+  subText?: string
+  divider?: boolean
+  disable?: boolean
+  hide?: boolean
+  children?: ContextmenuItem[]
+  handler?: (el: HTMLElement) => void
+}
+
+export interface Axis {
+  x: number
+  y: number
+}

+ 25 - 23
src/components/CreateCourseDialog.vue

@@ -12,18 +12,6 @@
       <h2>{{ lang.ssCreateCourse }}</h2>
       <p class="subtitle">{{ lang.ssCreateCourseSubtitle }}</p>
       <div class="options-grid">
-        <div class="option-card disabled">
-          <div class="option-icon">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M12 2L2 7l10 5 10-5-10-5z" />
-              <path d="M2 17l10 5 10-5" />
-              <path d="M2 12l10 5 10-5" />
-            </svg>
-          </div>
-          <h3>{{ lang.ssCreateFromAI }}</h3>
-          <p>{{ lang.ssAIGenerateContent }}</p>
-          <div class="coming-soon">{{ lang.ssComingSoon }}</div>
-        </div>
         <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
           @change="handleFileUpload">
           <div class="option-card">
@@ -38,34 +26,46 @@
             <p>{{ lang.ssUploadPPTFile }}</p>
           </div>
         </FileInput>
-        <div class="option-card disabled">
+        <div class="option-card" @click="handleOptionClick('blank')">
           <div class="option-icon">
             <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
                 <path id="Vector"
-                  d="M3.5 10.5007L14 2.33398L24.5 10.5007V23.334C24.5 23.9528 24.2542 24.5463 23.8166 24.9839C23.379 25.4215 22.7855 25.6673 22.1667 25.6673H5.83333C5.21449 25.6673 4.621 25.4215 4.18342 24.9839C3.74583 24.5463 3.5 23.9528 3.5 23.334V10.5007Z"
+                  d="M16.3332 2.33398H6.99984C6.381 2.33398 5.78751 2.57982 5.34992 3.0174C4.91234 3.45499 4.6665 4.04848 4.6665 4.66732V23.334C4.6665 23.9528 4.91234 24.5463 5.34992 24.9839C5.78751 25.4215 6.381 25.6673 6.99984 25.6673H20.9998C21.6187 25.6673 22.2122 25.4215 22.6498 24.9839C23.0873 24.5463 23.3332 23.9528 23.3332 23.334V9.33398L16.3332 2.33398Z"
                   stroke="currentColor" stroke-width="2.33333" />
-                <path id="Vector_2" d="M10.5 25.6667V14H17.5V25.6667" stroke="currentColor" stroke-width="2.33333" />
+                <path id="Vector_2" d="M16.3335 2.33398V9.33398H23.3335" stroke="currentColor" stroke-width="2.33333" />
               </g>
             </svg>
           </div>
-          <h3>{{ lang.ssImportFromLibrary }}</h3>
-          <p>{{ lang.ssSelectExistingContent }}</p>
+          <h3>{{ lang.ssCreateBlank }}</h3>
+          <p>{{ lang.ssStartFromScratch }}</p>
+        </div>
+        <div class="option-card disabled">
+          <div class="option-icon">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M12 2L2 7l10 5 10-5-10-5z" />
+              <path d="M2 17l10 5 10-5" />
+              <path d="M2 12l10 5 10-5" />
+            </svg>
+          </div>
+          <h3>{{ lang.ssCreateFromAI }}</h3>
+          <p>{{ lang.ssAIGenerateContent }}</p>
           <div class="coming-soon">{{ lang.ssComingSoon }}</div>
         </div>
-        <div class="option-card" @click="handleOptionClick('blank')">
+        <div class="option-card disabled">
           <div class="option-icon">
             <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
                 <path id="Vector"
-                  d="M16.3332 2.33398H6.99984C6.381 2.33398 5.78751 2.57982 5.34992 3.0174C4.91234 3.45499 4.6665 4.04848 4.6665 4.66732V23.334C4.6665 23.9528 4.91234 24.5463 5.34992 24.9839C5.78751 25.4215 6.381 25.6673 6.99984 25.6673H20.9998C21.6187 25.6673 22.2122 25.4215 22.6498 24.9839C23.0873 24.5463 23.3332 23.9528 23.3332 23.334V9.33398L16.3332 2.33398Z"
+                  d="M3.5 10.5007L14 2.33398L24.5 10.5007V23.334C24.5 23.9528 24.2542 24.5463 23.8166 24.9839C23.379 25.4215 22.7855 25.6673 22.1667 25.6673H5.83333C5.21449 25.6673 4.621 25.4215 4.18342 24.9839C3.74583 24.5463 3.5 23.9528 3.5 23.334V10.5007Z"
                   stroke="currentColor" stroke-width="2.33333" />
-                <path id="Vector_2" d="M16.3335 2.33398V9.33398H23.3335" stroke="currentColor" stroke-width="2.33333" />
+                <path id="Vector_2" d="M10.5 25.6667V14H17.5V25.6667" stroke="currentColor" stroke-width="2.33333" />
               </g>
             </svg>
           </div>
-          <h3>{{ lang.ssCreateBlank }}</h3>
-          <p>{{ lang.ssStartFromScratch }}</p>
+          <h3>{{ lang.ssImportFromLibrary }}</h3>
+          <p>{{ lang.ssSelectExistingContent }}</p>
+          <div class="coming-soon">{{ lang.ssComingSoon }}</div>
         </div>
       </div>
     </div>
@@ -137,7 +137,9 @@ const handleFileUpload = async (files: FileList) => {
     const signal = parsingAbortController.value.signal
 
     // 调用importPPTXFile并传入signal
-    await importPPTXFile(files, { signal, onclose: () => {emit('setTitle', file.name.replace(/\.[^/.]+$/, '')); emit('close')} })
+    await importPPTXFile(files, { signal, onclose: () => {
+      emit('setTitle', file.name.replace(/\.[^/.]+$/, '')); emit('close')
+    } })
   }
   catch (error) {
     if (error instanceof DOMException && error.name === 'AbortError') {

+ 27 - 2
src/components/FileInput.vue

@@ -1,5 +1,13 @@
 <template>
-  <div class="file-input" @click="handleClick()">
+  <div 
+    class="file-input" 
+    :class="{ 'drag-over': isDragOver }"
+    @click="handleClick()"
+    @dragenter.prevent="handleDragEnter"
+    @dragover.prevent="handleDragOver"
+    @dragleave.prevent="handleDragLeave"
+    @drop.prevent="handleDrop"
+  >
     <slot></slot>
     <input 
       class="input"
@@ -14,7 +22,7 @@
 </template>
 
 <script lang="ts" setup>
-import { useTemplateRef } from 'vue'
+import { ref, useTemplateRef } from 'vue'
 
 withDefaults(defineProps<{
   accept?: string
@@ -27,6 +35,7 @@ const emit = defineEmits<{
 }>()
 
 const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
+const isDragOver = ref(false)
 
 const handleClick = () => {
   if (!inputRef.value) return
@@ -37,6 +46,22 @@ const handleChange = (e: Event) => {
   const files = (e.target as HTMLInputElement).files
   if (files) emit('change', files)
 }
+const handleDragEnter = () => {
+  isDragOver.value = true
+}
+const handleDragOver = () => {
+  isDragOver.value = true
+}
+const handleDragLeave = () => {
+  isDragOver.value = false
+}
+const handleDrop = (e: DragEvent) => {
+  isDragOver.value = false
+  const files = e.dataTransfer?.files
+  if (files && files.length > 0) {
+    emit('change', files)
+  }
+}
 </script>
 
 <style lang="scss" scoped>

+ 6 - 4
src/components/Switch.vue

@@ -31,14 +31,16 @@ const handleChange = () => {
 
 <style lang="scss" scoped>
 .switch {
-  height: 20px;
-  display: inline-block;
+  // height: 20px;
+  // display: inline-block;
+  display: flex;
+  align-items: center;
   cursor: pointer;
 
   &:not(.disabled).active {
     .switch-core {
-      border-color: $themeColor;
-      background-color: $themeColor;
+      border-color: #f6c82b;
+      background-color: #f6c82b;
 
       &::after {
         left: 100%;

+ 38 - 0
src/components/TextColorButton2.vue

@@ -0,0 +1,38 @@
+<template>
+  <Button class="text-color-btn">
+    <slot></slot>
+    <div class="text-color-block">
+      <div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
+    </div>
+  </Button>
+</template>
+
+<script lang="ts" setup>
+import Button from './Button.vue'
+
+defineProps<{
+  color: string
+}>()
+</script>
+
+<style lang="scss" scoped>
+.text-color-btn {
+  width: 100%;
+  display: flex !important;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  padding: 0 15px;
+}
+.text-color-block {
+  width: 17px;
+  height: 4px;
+  margin-top: 1px;
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
+
+  .text-color-block-content {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 1 - 0
src/configs/hotkey.ts

@@ -13,6 +13,7 @@ export const enum KEYS {
   O = 'O',
   R = 'R',
   T = 'T',
+  V = 'V',
   MINUS = '-',
   EQUAL = '=',
   DIGIT_0 = '0',

+ 1 - 0
src/hooks/useCopyAndPasteElement.ts

@@ -35,6 +35,7 @@ export default () => {
 
   // 尝试将剪贴板元素数据解密后进行粘贴
   const pasteElement = () => {
+    console.log('pasteElement')
     readClipboard().then(text => {
       pasteTextClipboardData(text)
     }).catch(err => message.warning(err))

+ 2 - 1
src/hooks/useCreateElement.ts

@@ -318,7 +318,7 @@ export default () => {
    * 创建网页元素
    * @param url 网页链接地址
    */
-  const createFrameElement = (url: string, type: number) => {
+  const createFrameElement = (url: string, type: number, id?: string) => {
     // 检查当前幻灯片是否已经包含网页元素
     const { currentSlide } = storeToRefs(useSlidesStore())
     const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
@@ -334,6 +334,7 @@ export default () => {
     createElement({
       type: 'frame',
       id: nanoid(10),
+      webId: id || '',
       url,
       width: width,
       height: height,

+ 12 - 1
src/hooks/useGlobalHotkey.ts

@@ -44,7 +44,7 @@ export default () => {
   const { combineElements, uncombineElements } = useCombineElement()
   const { deleteElement } = useDeleteElement()
   const { lockElement } = useLockElement()
-  const { copyElement, cutElement, quickCopyElement } = useCopyAndPasteElement()
+  const { copyElement, cutElement, quickCopyElement, pasteElement } = useCopyAndPasteElement()
   const { selectAllElements } = useSelectElement()
   const { moveElement } = useMoveElement()
   const { orderElement } = useOrderElement()
@@ -62,6 +62,12 @@ export default () => {
     else if (thumbnailsFocus.value) cutSlide()
   }
 
+  const paste = () => {
+    console.log('paste')
+    
+    if (editorAreaFocus.value) pasteElement()
+  }
+
   const quickCopy = () => {
     if (activeElementIdList.value.length) quickCopyElement()
     else if (thumbnailsFocus.value) copyAndPasteSlide()
@@ -186,6 +192,11 @@ export default () => {
       e.preventDefault()
       cut()
     }
+    // if (ctrlOrMetaKeyActive && key === KEYS.V) {
+    //   // if (disableHotkeys.value) return
+    //   e.preventDefault()
+    //   paste()
+    // }
     if (ctrlOrMetaKeyActive && key === KEYS.D) {
       if (disableHotkeys.value) return
       e.preventDefault()

+ 87 - 32
src/hooks/useImport.ts

@@ -15,6 +15,29 @@ import { showConfirmDialog } from '@/utils/confirmDialog'
 import { EMFJS, WMFJS } from 'rtf.js'
 import * as UTIF from 'utif2'
 
+/**
+ * 获取 PPT 文件的基本信息
+ */
+const getPPTInfo = async (file: File): Promise<{ pageCount: number; filename: string }> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = async (e) => {
+      try {
+        const json = await parse(e.target!.result as ArrayBuffer)
+        resolve({
+          pageCount: json.slides.length,
+          filename: file.name
+        })
+      }
+      catch (error) {
+        reject(error)
+      }
+    }
+    reader.onerror = reject
+    reader.readAsArrayBuffer(file)
+  })
+}
+
 import type {
   Slide,
   TableCellStyle,
@@ -50,9 +73,9 @@ const getStyle = (htmlString: string) => {
   // 2. 解析 HTML 字符串为文档对象
   const doc = parser.parseFromString(htmlString, 'text/html')
   // 3. 获取 p 元素
-  const firstElem = doc.querySelector('p, ul, ol, table');
+  const firstElem = doc.querySelector('p, ul, ol, table')
   // 读取该元素的 allstyle 属性
-  const styleAttr = firstElem?.getAttribute('allstyle');
+  const styleAttr = firstElem?.getAttribute('allstyle')
   console.log(styleAttr) // 输出完整的 style 字符串
   return styleAttr || ''
 
@@ -384,29 +407,41 @@ export default () => {
   }
 
   const calculateRotatedPosition = (
-    x: number,
-    y: number,
-    w: number,
-    h: number,
-    ox: number,
-    oy: number,
-    k: number,
+    x: number, // 原外层容器左上角 X
+    y: number, // 原外层容器左上角 Y
+    w: number, // 原外层容器宽度
+    h: number, // 原外层容器高度
+    ox: number, // 子元素在原容器内左上角 X
+    oy: number, // 子元素在原容器内左上角 Y
+    childW: number, // 子元素宽度
+    childH: number, // 子元素高度
+    k: number // 顺时针旋转角度(度)
   ) => {
     const radians = k * (Math.PI / 180)
+    const cos = Math.cos(radians)
+    const sin = Math.sin(radians)
 
+    // 原容器中心点(全局坐标)
     const containerCenterX = x + w / 2
     const containerCenterY = y + h / 2
 
-    const relativeX = ox - w / 2
-    const relativeY = oy - h / 2
+    // 子元素中心相对于容器中心的偏移
+    const childCenterInContainerX = (ox + childW / 2) - w / 2
+    const childCenterInContainerY = (oy + childH / 2) - h / 2
 
-    const rotatedX = relativeX * Math.cos(radians) + relativeY * Math.sin(radians)
-    const rotatedY = -relativeX * Math.sin(radians) + relativeY * Math.cos(radians)
+    // 屏幕坐标系下的顺时针旋转
+    const rotatedOffsetX = childCenterInContainerX * cos - childCenterInContainerY * sin
+    const rotatedOffsetY = childCenterInContainerX * sin + childCenterInContainerY * cos
 
-    const graphicX = containerCenterX + rotatedX
-    const graphicY = containerCenterY + rotatedY
+    // 子元素中心的新全局坐标
+    const newCenterX = containerCenterX + rotatedOffsetX
+    const newCenterY = containerCenterY + rotatedOffsetY
 
-    return { x: graphicX, y: graphicY }
+    // 反推新左上角
+    return {
+      x: newCenterX - childW / 2,
+      y: newCenterY - childH / 2
+    }
   }
 
   /**
@@ -878,7 +913,8 @@ export default () => {
           width = parseFloat(parts[2])
           height = parseFloat(parts[3])
         }
-      } else {
+      }
+      else {
         const svgWidth = tempSvg.getAttribute('width')
         const svgHeight = tempSvg.getAttribute('height')
         if (svgWidth && svgHeight) {
@@ -886,7 +922,8 @@ export default () => {
           height = parseFloat(svgHeight)
         }
       }
-    } catch (e) {
+    }
+    catch (e) {
       console.warn('Failed to get dimensions from SVG, using default', e)
     }
 
@@ -948,7 +985,8 @@ export default () => {
           'image/png'
         )
       })
-    } finally {
+    }
+    finally {
       URL.revokeObjectURL(url)
     }
   }
@@ -1605,13 +1643,13 @@ export default () => {
     }
   */
 
-  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
+  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void, startTimer?: () => void, confirmOnclose?: () => void }) => {
     console.log('导入', files)
     const defaultOptions = {
       cover: false,
       fixedViewport: false,
     }
-    const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
+    const { cover, fixedViewport, signal, onclose, startTimer, confirmOnclose } = { ...defaultOptions, ...options }
 
     let isNone = false
     if (slides.value.length === 1 && slides.value[0].elements.length === 0) {
@@ -1623,7 +1661,7 @@ export default () => {
 
     exporting.value = true // 假设 exporting 是一个全局 ref
     imgExporting.value = true // 假设 imgExporting 是一个全局 ref
-
+    startTimer?.()
     // 预加载形状库(用于后续形状匹配)
     const shapeList: ShapePoolItem[] = []
     for (const item of SHAPE_LIST) {
@@ -1678,6 +1716,7 @@ export default () => {
             exporting.value = false
             imgExporting.value = false
             onclose?.()
+            confirmOnclose?.()
             return Promise.reject(new Error('用户取消导入'))
           }
         }
@@ -1694,10 +1733,9 @@ export default () => {
 
       // 收集当前幻灯片内所有上传任务
       const uploadTasks: Promise<void>[] = []
-      
       // 遍历每一张幻灯片
       for (const item of json.slides) {
-        let type = "", value = "";
+        let type = '', value = ''
         if (item.fill) {
           // ----- 解析背景 -----
           type = item.fill.type
@@ -1785,7 +1823,7 @@ export default () => {
                 defaultColor: theme.value.fontColor,
                 content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
                 style: getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)),
-                lineHeight: 1.5,
+                lineHeight: 1.2,
                 align: vAlignMap[el.vAlign] || 'middle',
                 outline: {
                   color: el.borderColor,
@@ -1986,8 +2024,8 @@ export default () => {
             // ---------- 形状 ----------
             else if (el.type === 'shape') {
               if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
-                //el.isFlipH = el.isFlipV = false;
-                //el.rotate = 0;
+                // el.isFlipH = el.isFlipV = false;
+                // el.rotate = 0;
                 //   // 线条元素(单独处理)
                 //   const lineElement = parseLineElement(el, ratio)
                 //   slide.elements.push(lineElement)
@@ -2013,8 +2051,23 @@ export default () => {
                 : undefined
 
               const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
-              const fill = el.fill?.type === 'color' ? el.fill.value : 'none'
-              const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
+              const fill = el.fill?.type === 'color' ? el.fill.value : el.fill || 'none'
+              //const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
+              const baseStyle = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit));
+              const isVertical =
+                (typeof el.content === 'string' && el.content.includes('writing-mode: vertical-rl')) ||
+                baseStyle.includes('writing-mode: vertical-rl');
+
+              let extraStyle = '';
+              if (el.pathBBox?.pWidth) {
+                const w = el.pathBBox.pWidth * ratio;
+                const h = el.pathBBox.pHeight * ratio;
+                extraStyle = isVertical
+                  ? `;width:${h}px;height:${w}px;`
+                  : `;width:${w}px;height:${h}px;`;
+              }
+
+              const style = baseStyle + extraStyle;
               const element: PPTShapeElement = {
                 type: 'shape',
                 id: nanoid(10),
@@ -2100,7 +2153,7 @@ export default () => {
               }
 
               if (element.path) slide.elements.push(element)
-              //}
+              // }
             }
 
             // ---------- 表格 ----------
@@ -2271,7 +2324,7 @@ export default () => {
                 if (el.rotate) {
                   const { x, y } = calculateRotatedPosition(
                     originLeft, originTop, originWidth, originHeight,
-                    _el.left, _el.top, el.rotate
+                    _el.left, _el.top, _el.width, _el.height, el.rotate
                   )
                   left = x
                   top = y
@@ -2284,6 +2337,7 @@ export default () => {
                 }
                 if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
                 if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
+                if (el.rotate) element.rotate = (_el.rotate || 0) + el.rotate
 
                 return element
               })
@@ -2487,6 +2541,7 @@ export default () => {
     getFile,
     getFile2,
     dataToFile,
-    uploadFileToS3
+    uploadFileToS3,
+    getPPTInfo
   }
 }

+ 46 - 1
src/hooks/usePasteEvent.ts

@@ -4,6 +4,50 @@ import { useMainStore } from '@/store'
 import { getImageDataURL } from '@/utils/image'
 import usePasteTextClipboardData from './usePasteTextClipboardData'
 import useCreateElement from './useCreateElement'
+/**
+     * 上传 File 到 S3,返回公开访问的 URL
+     */
+const uploadFileToS3 = (file: File): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    if (typeof window === 'undefined' || !window.AWS) {
+      reject(new Error('AWS SDK not available'))
+      return
+    }
+
+    const credentials = {
+      accessKeyId: 'AKIATLPEDU37QV5CHLMH',
+      secretAccessKey: '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',
+    }
+    window.AWS.config.update(credentials)
+    window.AWS.config.region = 'cn-northwest-1'
+
+    const bucket = new window.AWS.S3({
+      params: { Bucket: 'ccrb' }, httpOptions: {
+        timeout: 600000 // 10分钟超时
+      }
+    })
+    const ext = file.name.split('.').pop() || 'bin'
+    const key = `${file.name.split('.')[0]}_${Date.now()}.${ext}`
+
+    const params = {
+      Key: 'pptto/' + key,
+      ContentType: file.type,
+      Body: file,
+      ACL: 'public-read',
+    }
+    const options = {
+      partSize: 5 * 1024 * 1024, // 2GB 分片,可酌情调小
+      queueSize: 2,
+      leavePartsOnError: true,
+    }
+
+    bucket
+      .upload(params, options)
+      .promise()
+      .then(data => resolve(data.Location))
+      .catch(err => reject(err))
+  })
+}
 
 export default () => {
   const { editorAreaFocus, thumbnailsFocus, disableHotkeys } = storeToRefs(useMainStore())
@@ -13,7 +57,8 @@ export default () => {
 
   // 粘贴图片到幻灯片元素
   const pasteImageFile = (imageFile: File) => {
-    getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
+    // getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
+    uploadFileToS3(imageFile).then(url => createImageElement(url))
   }
 
   /**

+ 1 - 0
src/i18n/lang.ts

@@ -3,6 +3,7 @@ import cn from '@/views/lang/cn.json'
 import hk from '@/views/lang/hk.json'
 
 const href = window.location.href.toLowerCase()
+// export const lang = en
 export const lang =
   href.includes('cocorobo.com') ? en
     : href.includes('cocorobo.hk') ? hk

+ 64 - 0
src/plugins/directive/contextmenu2.ts

@@ -0,0 +1,64 @@
+import { type Directive, type DirectiveBinding, createVNode, render } from 'vue'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
+
+const CTX_CONTEXTMENU_HANDLER = 'CTX_CONTEXTMENU_HANDLER'
+
+interface CustomHTMLElement extends HTMLElement {
+  [CTX_CONTEXTMENU_HANDLER]?: (event: MouseEvent) => void
+} 
+
+const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
+  event.stopPropagation()
+  event.preventDefault()
+
+  const menus = binding.value(el)
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  // 移除右键菜单并取消相关的事件监听
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    el.classList.remove('contextmenu-active')
+    document.body.removeEventListener('scroll', removeContextmenu)  
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  // 创建自定义菜单
+  const options = {
+    axis: { x: event.x, y: event.y },
+    el,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  // 为目标节点添加菜单激活状态的className
+  el.classList.add('contextmenu-active')
+
+  // 页面变化时移除菜单
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+
+const ContextmenuDirective: Directive = {
+  mounted(el: CustomHTMLElement, binding) {
+    el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
+    el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+  },
+
+  unmounted(el: CustomHTMLElement) {
+    if (el && el[CTX_CONTEXTMENU_HANDLER]) {
+      el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+      delete el[CTX_CONTEXTMENU_HANDLER]
+    }
+  },
+}
+
+export default ContextmenuDirective

+ 2 - 0
src/plugins/directive/index.ts

@@ -1,12 +1,14 @@
 import type { App } from 'vue'
 
 import Contextmenu from './contextmenu'
+import Contextmenu2 from './contextmenu2'
 import ClickOutside from './clickOutside'
 import Tooltip from './tooltip'
 
 export default {
   install(app: App) {
     app.directive('contextmenu', Contextmenu)
+    app.directive('contextmenu2', Contextmenu2)
     app.directive('click-outside', ClickOutside)
     app.directive('tooltip', Tooltip)
   }

+ 4 - 0
src/plugins/icon.ts

@@ -131,6 +131,8 @@ import {
   LoadingFour, // 引入loadingIcon
   UpTwo,
   Refresh,
+  ThumbsUp,
+  CastScreen
 } from '@icon-park/vue-next'
 
 export interface Icons {
@@ -267,6 +269,8 @@ export const icons: Icons = {
   IconLoading: LoadingFour, // 添加loadingIcon
   UpTwo: UpTwo,
   IconRefresh: Refresh,
+  IconThumbsUp: ThumbsUp,
+  IconCastScreen: CastScreen,
 }
 
 export default {

+ 1 - 1
src/services/config.ts

@@ -17,7 +17,7 @@ instance.interceptors.request.use(
     // 确保每个请求都带上cookie
     config.withCredentials = true
 
-    if (url.includes('https://gpt4.cocorobo.cn') || url.includes('https://appapi.cocorobo.cn') || url.includes('https://ccrb.s3.cn-northwest-1.amazonaws.com.cn')) {
+    if (url.includes('https://yjsredis.cocorobo.cn/') || url.includes('https://gpt4.cocorobo.cn') || url.includes('https://appapi.cocorobo.cn') || url.includes('https://ccrb.s3.cn-northwest-1.amazonaws.com.cn')) {
       config.withCredentials = false 
     }
 

+ 16 - 3
src/services/course.ts

@@ -1,8 +1,8 @@
 import axios from './config'
 
 export const API_URL = 'https://pbl.cocorobo.cn/api/pbl/'
-export const yweb_socket = 'wss://yjs.cocorobo.cn'
-// export const yweb_socket = 'wss://yrs.cocorobo.cn'
+// export const yweb_socket = 'wss://yjs.cocorobo.cn'
+export const yweb_socket = 'wss://yrs.cocorobo.cn'
 
 /**
  * 获取课程详情
@@ -199,6 +199,17 @@ export const getWorkPageId = (params: any): Promise<any> => {
 }
 
 
+/**
+ * 
+ * 点赞功能
+ * @param any wid 作业id lid 用户id t 1点赞 2评论 c 评论内容
+ * @returns Promise<any>
+ */
+
+export const likeWork = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}insertComment`, [params])
+}
+
 /**
  * 
  * 获取年级
@@ -229,6 +240,8 @@ export default {
   getAgentData,
   clearDialogue,
   getWorkDetail,
-  getWorkPageId
+  getWorkPageId,
+  likeWork,
+  getClassById,
 }
 

+ 105 - 0
src/services/speaking.ts

@@ -0,0 +1,105 @@
+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 res = await fetch(`${DIALOGUE_BASE}/sessions/by-config`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    credentials: 'include',
+    body: JSON.stringify({ configId, userIds }),
+  })
+  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)
+}

+ 22 - 1
src/store/main.ts

@@ -1,4 +1,4 @@
-import { customAlphabet } from 'nanoid'
+import { customAlphabet } from 'nanoid'
 import { defineStore } from 'pinia'
 import { ToolbarStates } from '@/types/toolbar'
 import type { CreatingElement, ShapeFormatPainter, TextFormatPainter } from '@/types/edit'
@@ -37,6 +37,11 @@ export interface MainState {
   showNotesPanel: boolean
   showMarkupPanel: boolean
   showAIPPTDialog: boolean
+  childPageVisible: boolean
+  childPageToolsArray: any[]
+  childPageIndex: number
+  userid: string
+  courseTitle: string
 }
 
 const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
@@ -73,6 +78,11 @@ export const useMainStore = defineStore('main', {
     showNotesPanel: false, // 打开批注面板
     showMarkupPanel: false, // 打开类型标注面板
     showAIPPTDialog: false, // 打开AIPPT创建窗口
+    childPageVisible: false, // 打开带选页
+    childPageToolsArray: [], // 带选页工具数组
+    childPageIndex: 0, // 带选页当前页面索引
+    userid: '', // 当前用户ID
+    courseTitle: '', // 课程标题
   }),
 
   getters: {
@@ -206,5 +216,16 @@ export const useMainStore = defineStore('main', {
     setAIPPTDialogState(show: boolean) {
       this.showAIPPTDialog = show
     },
+    setChildPageState(show: boolean, tools: any[], pageIndex: number) {
+      this.childPageVisible = show
+      this.childPageToolsArray = tools
+      this.childPageIndex = pageIndex
+    },
+    setUserid(userid: string) {
+      this.userid = userid
+    },
+    setCourseTitle(courseTitle: string) {
+      this.courseTitle = courseTitle
+    }
   },
 })

+ 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
+    },
+  },
+})

+ 5 - 4
src/tools/aiChat.ts

@@ -50,7 +50,7 @@ export const chat_no_stream = (msg: string, agentId: string, userId: string, lan
         : language === 'hk'
           ? 'Traditional Chinese'
           : 'Chinese'
-      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
       uid: uuidv4(),
       stream: false,
       model: agentData?.modelType || 'open-doubao',
@@ -103,7 +103,8 @@ export const chat_stream = async (
   language: string,
   onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
   session_name?: string,
-  file_ids?: Array<string>
+  file_ids?: Array<string>,
+  model?: string
 ): Promise<{ abort: () => void }> => {
   const agentData = await getAgentModel(agentId)
   const params: ChatParams = {
@@ -115,10 +116,10 @@ export const chat_stream = async (
       : language === 'hk'
         ? 'Traditional Chinese'
         : 'Chinese'
-    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
     uid: uuidv4(),
     stream: true,
-    model: agentData?.modelType || 'open-doubao',
+    model: model || agentData?.modelType || 'open-doubao',
     userId: userId,
     tts_language: getTtsLanguage(language),
     session_name: session_name || uuidv4()

+ 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'

+ 3 - 1
src/types/slides.ts

@@ -651,6 +651,7 @@ export interface PPTFrameElement extends PPTBaseElement {
   isHTML?: boolean,
   toolType?: number,
   isDone?: boolean,
+  webId?: string,
 }
 
 export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement | PPTAudioElement | PPTFrameElement
@@ -761,7 +762,8 @@ export interface Slide {
   animations?: PPTAnimation[]
   turningMode?: TurningMode
   sectionTag?: SectionTag
-  type?: SlideType
+  type?: SlideType,
+  toolsArray?: Array<any>
 }
 
 /**

+ 22 - 0
src/utils/clipboard.ts

@@ -1,5 +1,26 @@
 import Clipboard from 'clipboard'
 import { decrypt } from '@/utils/crypto'
+import { lang } from '@/main'
+
+// 全局隐藏的 input 元素,用于保持页面焦点
+let clipboardFocusInput: HTMLInputElement | null = null
+
+// 初始化隐藏的 input 元素以保持焦点
+export const initClipboardFocus = () => {
+  if (clipboardFocusInput) return clipboardFocusInput
+  
+  clipboardFocusInput = document.createElement('input')
+  clipboardFocusInput.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none;'
+  // clipboardFocusInput.style.cssText = 'position:fixed;left:-0;top:-0;'
+  // clipboardFocusInput.setAttribute('readonly', 'readonly')
+  clipboardFocusInput.setAttribute('tabindex', '-1')
+  document.querySelector('.viewport-wrapper')?.appendChild(clipboardFocusInput)
+  
+  // 尝试聚焦
+  clipboardFocusInput.focus()
+  readClipboard()
+  return clipboardFocusInput
+}
 
 /**
  * 复制文本到剪贴板
@@ -27,6 +48,7 @@ export const copyText = (text: string) => {
   })
 }
 
+// 读取剪贴板
 // 读取剪贴板
 export const readClipboard = (): Promise<string> => {
   return new Promise((resolve, reject) => {

+ 162 - 0
src/utils/components/messageInstruction.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="messageInstruction">
+    <div class="messageItem" :class="item.type" v-for="item in messageList" :key="item.id">
+      <div class="icon">
+        <svg v-if="['success'].includes(item.type)" viewBox="0 0 1024 1024"><path d="M512 85.333333c235.648 0 426.666667 191.018667 426.666667 426.666667s-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512 276.352 85.333333 512 85.333333z m-74.965333 550.4L346.453333 545.152a42.666667 42.666667 0 1 0-60.330666 60.330667l120.704 120.704a42.666667 42.666667 0 0 0 60.330666 0l301.653334-301.696a42.666667 42.666667 0 1 0-60.288-60.330667l-271.530667 271.488z" fill="#67C239"></path></svg>
+        <svg v-if="['pptMessage'].includes(item.type)" viewBox="0 0 1024 1024" ><path d="M512 85.333c-234.667 0-426.667 192-426.667 426.667s192 426.667 426.667 426.667 426.667-192 426.667-426.667S746.667 85.333 512 85.333z m21.333 640h-42.666c-12.8 0-21.334-8.533-21.334-21.333v-42.667c0-12.8 8.534-21.333 21.334-21.333h42.666c12.8 0 21.334 8.533 21.334 21.333V704c0 12.8-8.534 21.333-21.334 21.333zM512 554.667c-25.6 0-42.667-17.067-42.667-42.667V341.333c0-25.6 17.067-42.666 42.667-42.666s42.667 17.066 42.667 42.666V512c0 25.6-17.067 42.667-42.667 42.667z" fill="#FCCF00"></path></svg>
+        <!-- <svg v-if="['pptMessage'].includes(item.type)" viewBox="0 0 1024 1024"><path d="M512 28.444444C244.906667 28.444444 28.444444 244.906667 28.444444 512s216.462222 483.555556 483.555556 483.555556 483.555556-216.462222 483.555556-483.555556S779.093333 28.444444 512 28.444444z m30.225067 755.5584A30.190933 30.190933 0 0 1 512 814.222222a30.190933 30.190933 0 0 1-30.225067-30.225066A30.190933 30.190933 0 0 1 512 753.777778a30.190933 30.190933 0 0 1 30.225067 30.225066z m0-120.888888A30.190933 30.190933 0 0 1 512 693.327644a30.190933 30.190933 0 0 1-30.225067-30.225066V240.008533A30.190933 30.190933 0 0 1 512 209.772089a30.190933 30.190933 0 0 1 30.225067 30.225067v423.105422z" fill="#FCCF00"></path></svg> -->
+      </div>
+      <div class="message" v-html="item.message"></div>
+      <div class="close" @click="close(item.id)" v-if="item.showClose">
+        <svg viewBox="0 0 1025 1024"><path d="M997.553471 154.252491 639.804427 512.001535 997.553471 869.751602l0 0c16.34988 16.374439 26.450623 38.948897 26.450623 63.898376 0 49.899981-40.450041 90.350022-90.351045 90.350022-24.949479 0-47.549519-10.100743-63.897353-26.475181l0 0L512.003581 639.775775 154.255561 997.525842l0 0c-16.34988 16.374439-38.948897 26.475181-63.899399 26.475181-49.901004 0-90.350022-40.450041-90.350022-90.350022 0-24.950502 10.099719-47.523938 26.449599-63.898376l0 0 357.750067-357.750067L26.454716 154.252491l0 0c-16.34988-16.350903-26.449599-38.949921-26.449599-63.900423 0-49.899981 40.449018-90.348999 90.350022-90.348999 24.950502 0 47.550543 10.099719 63.899399 26.474158l0 0 357.748021 357.749044L869.754672 26.477228l0 0c16.348857-16.374439 38.948897-26.474158 63.897353-26.474158 49.901004 0 90.351045 40.449018 90.351045 90.348999C1024.00307 115.301547 1013.902327 137.901588 997.553471 154.252491L997.553471 154.252491z" fill="#1A1A1A"></path></svg>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+
+const messageList = ref([])
+
+const success = (data) => {
+  if (typeof data === 'string') {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'success',
+      message: data,
+      showClose: data.showClose || false
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, 3000)
+  } else if (typeof data === 'object' && data.message && data.duration) {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'success',
+      message: data.message,
+      showClose: data.showClose || false
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, data.duration)
+  }
+}
+
+const pptMessage = (data) => {
+  if (typeof data === 'string') {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'pptMessage',
+      message: data,
+      showClose: data.showClose || true
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, 3000)
+  } else if (typeof data === 'object' && data.message && data.duration) {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'pptMessage',
+      message: data.message,
+      showClose: data.showClose || true
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, data.duration)
+  }
+}
+
+const close = (id) => {
+  messageList.value = messageList.value.filter(item => item.id !== id)
+}
+defineExpose({
+  success,
+  pptMessage,
+  close
+})
+</script>
+
+<style lang="scss" scoped>
+.messageInstruction {
+  position: fixed;
+  top: 80px;
+  left: 50%;
+  transform: translate(-50%, 0%);
+  width: auto;
+  height: auto;
+  z-index: 9999;
+  display: flex;
+  flex-direction: column;
+  gap: .5rem;
+}
+
+.messageItem{
+  padding: 15px 20px;
+  border-radius: .2rem;
+  font-size: 1rem;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #FFFFFF;
+  box-shadow: 0 0 .2rem rgba(0, 0, 0, 0.1);
+  transition: .3s;
+}
+
+.messageItem>.icon{
+  width: 1.3rem;
+  height: 1.3rem;
+  margin-right: .8rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  //margin-top: -.1rem;
+  line-height: 1rem;
+}
+
+.messageItem>.icon>svg{
+  width: 100%;
+  height: 100%;
+}
+
+.messageItem>.close{
+  width: .6rem;
+  height: .6rem;
+  margin-left: 1.5rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: .3rem;
+}
+
+.messageItem>.close>svg{
+  width: 100%;
+  height: 100%;
+  cursor: pointer;
+}
+
+.messageItem>.message{
+  flex: 1;
+  max-width: 50vh;
+}
+
+.messageItem {
+  animation: slideDownFadeIn 0.3s ease-out;
+}
+
+@keyframes slideDownFadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+</style>

+ 4 - 0
src/utils/confirmDialog.ts

@@ -8,6 +8,8 @@ interface ConfirmDialogOptions {
   confirmText?: string
   cancelText?: string
   width?: number
+  onConfirm?: () => void
+  onCancel?: () => void
 }
 
 export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolean> {
@@ -27,6 +29,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         setTimeout(() => {
           app.unmount()
+          options.onConfirm?.()
           document.body.removeChild(container)
           resolve(true)
         }, 300)
@@ -35,6 +38,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         setTimeout(() => {
           app.unmount()
+          options.onCancel?.()
           document.body.removeChild(container)
           resolve(false)
         }, 300)

+ 137 - 18
src/utils/prosemirror/schema/marks.ts

@@ -1,6 +1,7 @@
 import { marks } from 'prosemirror-schema-basic'
 import type { MarkSpec } from 'prosemirror-model'
 
+// ===== 原有定义 =====
 const subscript: MarkSpec = {
   excludes: 'subscript',
   parseDOM: [
@@ -10,7 +11,7 @@ const subscript: MarkSpec = {
       getAttrs: value => value === 'sub' && null
     },
   ],
-  toDOM: () => ['sub',  0],
+  toDOM: () => ['sub', 0],
 }
 
 const superscript: MarkSpec = {
@@ -56,9 +57,7 @@ const underline: MarkSpec = {
 }
 
 const forecolor: MarkSpec = {
-  attrs: {
-    color: {},
-  },
+  attrs: { color: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -76,9 +75,7 @@ const forecolor: MarkSpec = {
 }
 
 const backcolor: MarkSpec = {
-  attrs: {
-    backcolor: {},
-  },
+  attrs: { backcolor: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -96,9 +93,7 @@ const backcolor: MarkSpec = {
 }
 
 const fontsize: MarkSpec = {
-  attrs: {
-    fontsize: {},
-  },
+  attrs: { fontsize: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -116,16 +111,14 @@ const fontsize: MarkSpec = {
 }
 
 const fontname: MarkSpec = {
-  attrs: {
-    fontname: {},
-  },
+  attrs: { fontname: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
     {
       style: 'font-family',
       getAttrs: fontname => {
-        return { fontname: fontname && typeof fontname === 'string' ? fontname.replace(/[\"\']/g, '') : '' }
+        return { fontname: fontname && typeof fontname === 'string' ? fontname.replace(/["']/g, '') : '' }
       }
     },
   ],
@@ -158,9 +151,7 @@ const link: MarkSpec = {
 }
 
 const mark: MarkSpec = {
-  attrs: {
-    index: { default: null },
-  },
+  attrs: { index: { default: null } },
   parseDOM: [
     {
       tag: 'mark',
@@ -173,14 +164,135 @@ const mark: MarkSpec = {
   toDOM: node => ['mark', { 'data-index': node.attrs.index }, 0],
 }
 
+// ===== 新增 marks =====
+
+// 通用 font-weight(如 100, 200, bold, bolder 等)
+const fontWeight: MarkSpec = {
+  attrs: { fontWeight: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'font-weight',
+      getAttrs: value => value ? { fontWeight: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { fontWeight } = mark.attrs
+    let style = ''
+    if (fontWeight) style += `font-weight: ${fontWeight};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 font-style(如 italic, oblique, normal)
+const fontStyle: MarkSpec = {
+  attrs: { fontStyle: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'font-style',
+      getAttrs: value => value ? { fontStyle: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { fontStyle } = mark.attrs
+    let style = ''
+    if (fontStyle) style += `font-style: ${fontStyle};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 text-decoration(可包含 underline, overline, line-through 等组合)
+const textDecoration: MarkSpec = {
+  attrs: { textDecoration: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'text-decoration',
+      getAttrs: value => value ? { textDecoration: value } : {}
+    },
+    {
+      style: 'text-decoration-line',
+      getAttrs: value => value ? { textDecoration: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { textDecoration } = mark.attrs
+    let style = ''
+    if (textDecoration) style += `text-decoration: ${textDecoration};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 vertical-align(支持 baseline, sub, super, top, middle, bottom, 长度值等)
+const verticalAlign: MarkSpec = {
+  attrs: { verticalAlign: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'vertical-align',
+      getAttrs: value => value ? { verticalAlign: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { verticalAlign } = mark.attrs
+    let style = ''
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`
+    return ['span', { style }, 0]
+  }
+}
+
+// letter-spacing
+const letterSpacing: MarkSpec = {
+  attrs: { letterSpacing: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'letter-spacing',
+      getAttrs: value => value ? { letterSpacing: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { letterSpacing } = mark.attrs
+    let style = ''
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`
+    return ['span', { style }, 0]
+  }
+}
+
+// text-shadow
+const textShadow: MarkSpec = {
+  attrs: { textShadow: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'text-shadow',
+      getAttrs: value => value ? { textShadow: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { textShadow } = mark.attrs
+    let style = ''
+    if (textShadow) style += `text-shadow: ${textShadow};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 从 prosemirror-schema-basic 获取的默认 marks
 const { em, strong, code } = marks
 
 export default {
   em,
   strong,
+  code,
   fontsize,
   fontname,
-  code,
   forecolor,
   backcolor,
   subscript,
@@ -189,4 +301,11 @@ export default {
   underline,
   link,
   mark,
+  // 新增
+  fontWeight,
+  fontStyle,
+  textDecoration,
+  verticalAlign,
+  letterSpacing,
+  textShadow,
 }

+ 111 - 52
src/utils/prosemirror/schema/nodes.ts

@@ -115,7 +115,16 @@ const listItem: NodeSpec = {
     paddingRight: { default: '' },
     paddingBottom: { default: '' },
     paddingLeft: { default: '' },
-    whiteSpace: { default: 'normal' }, // 新增 white-space 属性
+    whiteSpace: { default: 'normal' },
+    writingMode: { default: '' },
+    // 新增字体相关属性
+    fontWeight: { default: '' },
+    fontStyle: { default: '' },
+    textDecoration: { default: '' },
+    textDecorationLine: { default: '' },
+    verticalAlign: { default: '' },
+    letterSpacing: { default: '' },
+    textShadow: { default: '' },
   },
   content: 'paragraph block*',
   group: 'block',
@@ -138,7 +147,17 @@ const listItem: NodeSpec = {
         const paddingRight = style.paddingRight || '';
         const paddingBottom = style.paddingBottom || '';
         const paddingLeft = style.paddingLeft || '';
-        const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
+        const whiteSpace = style.whiteSpace || 'normal';
+        const writingMode = style.writingMode || '';
+
+        // 读取新增属性
+        const fontWeight = style.fontWeight || '';
+        const fontStyle = style.fontStyle || '';
+        const textDecoration = style.textDecoration || '';
+        const textDecorationLine = style.textDecorationLine || '';
+        const verticalAlign = style.verticalAlign || '';
+        const letterSpacing = style.letterSpacing || '';
+        const textShadow = style.textShadow || '';
 
         return {
           textAlign,
@@ -153,7 +172,15 @@ const listItem: NodeSpec = {
           paddingRight,
           paddingBottom,
           paddingLeft,
-          whiteSpace, // 返回 whiteSpace
+          whiteSpace,
+          writingMode,
+          fontWeight,
+          fontStyle,
+          textDecoration,
+          textDecorationLine,
+          verticalAlign,
+          letterSpacing,
+          textShadow,
         };
       },
     },
@@ -172,7 +199,15 @@ const listItem: NodeSpec = {
       paddingRight,
       paddingBottom,
       paddingLeft,
-      whiteSpace, // 获取 whiteSpace
+      whiteSpace,
+      writingMode,
+      fontWeight,
+      fontStyle,
+      textDecoration,
+      textDecorationLine,
+      verticalAlign,
+      letterSpacing,
+      textShadow,
     } = node.attrs;
 
     let style = '';
@@ -189,13 +224,12 @@ const listItem: NodeSpec = {
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
     if (marginLeft) {
-      // 解析数值和单位
       const str = String(marginLeft).trim();
       const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
       if (match) {
         let num = parseFloat(match[1]);
         const unit = match[2] || 'px';
-        const absNum = Math.abs(num); // 负数转正
+        const absNum = Math.abs(num);
         const val = absNum + unit;
         style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
       } else {
@@ -206,35 +240,43 @@ const listItem: NodeSpec = {
     if (lineHeight) {
       let finalValue;
       const str = String(lineHeight).trim();
-      // 匹配纯数字(整数或小数,可带负号)
       if (/^-?\d+(\.\d+)?$/.test(str)) {
         finalValue = parseFloat(str) * 1.2;
       } else {
-        // 带单位或其他非纯数字内容,直接使用原值
         finalValue = lineHeight;
       }
       style += `line-height: ${finalValue};`;
     }
+    else{
+      style += `line-height: 1.2;`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
     if (paddingLeft) style += `padding-left: ${paddingLeft};`;
     if (whiteSpace && whiteSpace !== 'normal') {
-      style += `white-space: ${whiteSpace};`; // 添加 white-space
+      style += `white-space: ${whiteSpace};`;
+    }
+    if (writingMode) {
+      style += `writing-mode: ${writingMode};`;
     }
 
-    //const attrs: { style?: string } = {};
-    //if (style) attrs.style = style;
+    // 新增字体相关样式输出
+    if (fontWeight) style += `font-weight: ${fontWeight};`;
+    if (fontStyle) style += `font-style: ${fontStyle};`;
+    if (textDecoration) style += `text-decoration: ${textDecoration};`;
+    if (textDecorationLine) style += `text-decoration-line: ${textDecorationLine};`;
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`;
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`;
+    if (textShadow) style += `text-shadow: ${textShadow};`;
 
     let isEmpty = false;
     const firstChild = node.content.firstChild;
     if (firstChild && firstChild.type.name === 'paragraph') {
-      // 段落无任何子节点(包括 text 和 inline 节点)
       if (firstChild.content.size === 0) {
         isEmpty = true;
       }
     }
-    // 如果整个 li 的内容长度为 0 也可以作为判断
     if (node.content.size === 0) isEmpty = true;
 
     const attrs: { style?: string; class?: string } = {};
@@ -246,7 +288,7 @@ const listItem: NodeSpec = {
 };
 
 const paragraph: NodeSpec = {
-  whitespace: "pre", // 此属性控制 ProseMirror 内部空格处理,与 CSS white-space 无关,保留不变
+  whitespace: "pre",
   attrs: {
     textAlign: { default: '' },
     textAlignLast: { default: '' },
@@ -261,7 +303,16 @@ const paragraph: NodeSpec = {
     paddingRight: { default: '' },
     paddingBottom: { default: '' },
     paddingLeft: { default: '' },
-    whiteSpace: { default: 'normal' }, // 新增 white-space 属性
+    whiteSpace: { default: 'normal' },
+    writingMode: { default: '' },
+    // 新增字体相关属性
+    fontWeight: { default: '' },
+    fontStyle: { default: '' },
+    textDecoration: { default: '' },
+    textDecorationLine: { default: '' },
+    verticalAlign: { default: '' },
+    letterSpacing: { default: '' },
+    textShadow: { default: '' },
   },
   content: 'inline*',
   group: 'block',
@@ -284,7 +335,17 @@ const paragraph: NodeSpec = {
         const paddingRight = style.paddingRight || '';
         const paddingBottom = style.paddingBottom || '';
         const paddingLeft = style.paddingLeft || '';
-        const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
+        const whiteSpace = style.whiteSpace || 'normal';
+        const writingMode = style.writingMode || '';
+
+        // 读取新增属性
+        const fontWeight = style.fontWeight || '';
+        const fontStyle = style.fontStyle || '';
+        const textDecoration = style.textDecoration || '';
+        const textDecorationLine = style.textDecorationLine || '';
+        const verticalAlign = style.verticalAlign || '';
+        const letterSpacing = style.letterSpacing || '';
+        const textShadow = style.textShadow || '';
 
         return {
           textAlign,
@@ -299,7 +360,15 @@ const paragraph: NodeSpec = {
           paddingRight,
           paddingBottom,
           paddingLeft,
-          whiteSpace, // 返回 whiteSpace
+          whiteSpace,
+          writingMode,
+          fontWeight,
+          fontStyle,
+          textDecoration,
+          textDecorationLine,
+          verticalAlign,
+          letterSpacing,
+          textShadow,
         };
       },
     },
@@ -326,7 +395,15 @@ const paragraph: NodeSpec = {
       paddingRight,
       paddingBottom,
       paddingLeft,
-      whiteSpace, // 获取 whiteSpace
+      whiteSpace,
+      writingMode,
+      fontWeight,
+      fontStyle,
+      textDecoration,
+      textDecorationLine,
+      verticalAlign,
+      letterSpacing,
+      textShadow,
     } = node.attrs;
 
     let style = '';
@@ -342,60 +419,42 @@ const paragraph: NodeSpec = {
     }
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
-    /*
-    if (marginLeft) {
-      // 解析数值和单位
-      const str = String(marginLeft).trim();
-      const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
-      if (match) {
-        let num = parseFloat(match[1]);
-        const unit = match[2] || 'px';
-        const absNum = Math.abs(num); // 负数转正
-        const val = absNum + unit;
-        style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
-      } else {
-        style += `margin-left: ${marginLeft};`;
-      }
-    }
-    */
     if (marginLeft) style += `margin-left: ${marginLeft};`;
     if (marginRight) style += `margin-right: ${marginRight};`;
     if (lineHeight) {
       let finalValue;
       const str = String(lineHeight).trim();
-      // 匹配纯数字(整数或小数,可带负号)
       if (/^-?\d+(\.\d+)?$/.test(str)) {
         finalValue = parseFloat(str) * 1.2;
       } else {
-        // 带单位或其他非纯数字内容,直接使用原值
         finalValue = lineHeight;
       }
       style += `line-height: ${finalValue};`;
     }
+    else{
+      style += `line-height: 1.2;`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
     if (paddingLeft) style += `padding-left: ${paddingLeft};`;
     if (whiteSpace && whiteSpace !== 'normal') {
-      style += `white-space: ${whiteSpace};`; // 添加 white-space
+      style += `white-space: ${whiteSpace};`;
     }
+    if (writingMode) {
+      style += `writing-mode: ${writingMode};`;
+    }
+
+    // 新增字体相关样式输出
+    if (fontWeight) style += `font-weight: ${fontWeight};`;
+    if (fontStyle) style += `font-style: ${fontStyle};`;
+    if (textDecoration) style += `text-decoration: ${textDecoration};`;
+    if (textDecorationLine) style += `text-decoration-line: ${textDecorationLine};`;
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`;
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`;
+    if (textShadow) style += `text-shadow: ${textShadow};`;
 
     const attrs: { style?: string; class?: string } = {};
-/*
-    let isEmpty = false;
-    const firstChild = node.content.firstChild;
-    if (firstChild) {
-      // 段落无任何子节点(包括 text 和 inline 节点)
-      if (firstChild.content.size === 0) {
-        isEmpty = true;
-      }
-    }
-    
-    // 如果整个 li 的内容长度为 0 也可以作为判断
-    if (node.content.size === 0) isEmpty = true;
-    if (style) attrs.style = style;
-    //if (isEmpty) attrs.class = 'empty';
-*/
     if (style) attrs.style = style;
     return ['p', attrs, 0];
   },

+ 455 - 0
src/views/Editor/Canvas/ChildPageDisplay.vue

@@ -0,0 +1,455 @@
+<template>
+  <div class="child-page-display">
+    <div v-if="loading" class="loading-overlay">
+      <div class="loading-spinner"></div>
+      <span class="loading-text">{{ lang.ssAiWait }}</span>
+    </div>
+    
+    <div class="child-page-header">
+      <div class="header-left">
+        <span class="badge">{{ lang.ssWaitSelection }}</span>
+        <span class="hint">{{ formatHint(lang.ssWaitSelectionHint, toolsArray.length) }}</span>
+      </div>
+      <div class="header-right">
+        <button v-for="(tool, index) in toolsArray" :key="tool.tool_id" class="candidate-tag"
+          :class="{ active: selectedToolIndex === index }" @click="selectTool(index)">
+          {{ lang.ssCandidate }} {{ fillDigit(index + 1, 2) }} {{ tool.tool_name }}
+        </button>
+      </div>
+    </div>
+
+    <div class="child-page-body">
+      <div class="question-area">
+        <iframe
+          :src="toolsArray[selectedToolIndex]?.url + '&isSet=2' || ''"
+          frameborder="0"></iframe>
+      </div>
+
+      <div class="side-panel" v-if="!toolsArray[selectedToolIndex].isSet">
+         <button class="side-btn primary" @click="saveTool(toolsArray[selectedToolIndex].url || '', currentToolType, selectedToolIndex)">{{ lang.ssKeep }}</button>
+        <button class="side-btn secondary" @click="discardTool(selectedToolIndex)">{{ lang.ssDiscard }}</button>
+        <div class="divider"></div>
+        <button class="side-btn link" @click="keepAllTools">{{ lang.ssKeepAll }}</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore } from '@/store'
+import { fillDigit } from '@/utils/common'
+import { lang } from '@/main'
+import { useSlidesStore } from '@/store/slides'
+import { chat_no_stream, chat_no_stream2 } from '@/tools/aiChat'
+
+const slidesStore = useSlidesStore()
+const mainStore = useMainStore()
+const { childPageToolsArray, userid, courseTitle, childPageIndex } = storeToRefs(mainStore)
+const { setChildPageState } = mainStore
+const { currentSlide, slideIndex } = storeToRefs(slidesStore)
+
+const toolsArray = childPageToolsArray.value
+const selectedToolIndex = ref(0)
+const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
+const loading = ref(true)
+
+const formatHint = (str: string, count: number) => {
+  return str.replace('{0}', String(count))
+}
+
+const selectTool = (index: number) => {
+  selectedToolIndex.value = index
+  console.log('👶 选中工具:', toolsArray[index])
+}
+
+onMounted(async () => {
+  console.log('👶 带选页打开,toolsArray:', toolsArray)
+  const toolsToCreate = toolsArray.filter(t => !t.url)
+  if (toolsToCreate.length === 0) {
+    loading.value = false
+    return
+  }
+  
+  const createPromises = toolsToCreate.map((tool) => {
+    const originalIndex = toolsArray.findIndex(t => t.tool_id === tool.tool_id)
+    return createTool(toolsArray, tool, originalIndex)
+  })
+  
+  await Promise.all(createPromises)
+  loading.value = false
+})
+
+watch(childPageToolsArray, async (newVal) => {
+  console.log('👶 带选页 toolsArray 更新:', newVal)
+  const toolsToCreate = newVal.filter(t => !t.url)
+  if (toolsToCreate.length === 0) {
+    loading.value = false
+    return
+  }
+  
+  loading.value = true
+  const createPromises = toolsToCreate.map((tool) => {
+    const originalIndex = newVal.findIndex(t => t.tool_id === tool.tool_id)
+    return createTool(newVal, tool, originalIndex)
+  })
+  
+  await Promise.all(createPromises)
+  loading.value = false
+})
+
+const createTool = async (array: any[], tool: any, index: number) => {
+  let action = ''
+  let type = 0
+  if (tool.tool_id === 'choice') {
+    action = lang.ssAiChatQuickAction1
+    type = 45
+  }
+  else if (tool.tool_id === 'qa') {
+    action = lang.ssAiChatQuickAction2
+    type = 15
+  }
+  else if (tool.tool_id === 'poll') {
+    action = lang.ssAiChatQuickAction6
+    type = 78
+  }
+  else if (tool.tool_id === 'photo') {
+    action = lang.ssAiChatQuickAction7
+    type = 79
+  }
+  
+  const textContents = currentSlide.value?.elements
+    .filter((element: any) => element.type === 'text' || (element.type === 'shape' && element.text && element.text.content))
+    .map((textElement: any) => {
+      if (textElement.type === 'shape') {
+        const tempElement = document.createElement('div')
+        tempElement.innerHTML = textElement.text.content
+        return tempElement.textContent || tempElement.innerText || ''
+      }
+      const tempElement = document.createElement('div')
+      tempElement.innerHTML = textElement.content
+      return tempElement.textContent || tempElement.innerText || ''
+    })
+    .filter(content => content.trim() !== '') || []
+  
+  console.log('textContents', textContents)
+  const prompt = `
+      #课程标题(course_title): ${courseTitle.value || ''}
+      #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
+      #query: ${action}
+      `
+  const result = chat_no_stream(prompt, agentid2.value, userid.value || '', lang.lang)
+  console.log(result)
+  const content = await result.promise
+  console.log('👶 创建工具:', content)
+
+  let jsonFormat = ''
+  if (tool.tool_id === 'choice') {
+    jsonFormat = `{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}`
+  }
+  else if (tool.tool_id === 'qa') {
+    jsonFormat = `{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}`
+  }
+  else if (tool.tool_id === 'poll') {
+    jsonFormat = `{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[],"userAnswer":[]}]}`
+  }
+  else if (tool.tool_id === 'photo') {
+    jsonFormat = `{"answerQ":"问题","answer":"","fileList":[]}`
+  }
+  
+  const res = await chat_no_stream2([{
+    role: 'user',
+    content: `这是用户输入的内容:"${content}",根据用户输入的内容,生成json。输出一个json格式的回复,格式如下:${jsonFormat}。`,
+  }], { type: 'json_object' })
+  
+  console.log(`${tool.tool_name}`, JSON.parse(res))
+  
+  const pageId = await setPageId(type, res)
+  const baseUrl = setUrl()
+  const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${pageId}&type=${type}`
+  array[index].url = url
+  setTool()
+}
+
+const setTool = () => {
+  const slide = currentSlide.value
+  slidesStore.updateSlide({ toolsArray: toolsArray }, slide.id)
+  console.log(slide)
+
+  // 判断所有工具是否都已保留
+  const allToolsSet = toolsArray.every(t => t.isSet)
+  if (allToolsSet) {
+    const slide = currentSlide.value
+    slidesStore.updateSlide({ toolsArray: [] }, slide.id) 
+    setChildPageState(false, [], slideIndex.value || 0)
+  }
+}
+
+const setUrl = () => {
+  let url = 'https://beta.pbl.cocorobo.cn'
+  if (lang.lang === 'cn') {
+    url = 'https://pbl.cocorobo.cn/'
+  }
+  else if (lang.lang === 'hk') {
+    url = 'https://pbl.cocorobo.hk/'
+  }
+  else if (lang.lang === 'en') {
+    url = 'https://pbl.cocorobo.com/'
+  }
+  return url
+}
+
+import { getWorkPageId } from '@/services/course'
+const setPageId = async (tool: any, json: any) => {
+  const res = await getWorkPageId({
+    userid: userid.value || '',
+    type: tool,
+    json: json
+  })
+  return res[0][0].id
+}
+
+import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
+const { createSlide } = useSlideHandler()
+const { createFrameElement } = useCreateElement()
+
+// 切换页面
+const changeSlideIndex = (index: number) => {
+  mainStore.setActiveElementIdList([])
+
+  if (slideIndex.value === index) return
+  slidesStore.updateSlideIndex(index)
+}
+// 保留工具
+const saveTool = (url: string, type: any, index: number) => {
+  createSlide()
+  createFrameElement(url, type)
+  changeSlideIndex(childPageIndex.value)
+  toolsArray[index].isSet = true
+  setTool()
+}
+
+// 放弃工具
+const discardTool = (index: number) => {
+  toolsArray[index].isSet = true
+  setTool()
+}
+
+// 保留所有工具
+const keepAllTools = () => {
+  toolsArray.forEach(tool => {
+    if (!tool.isSet) {
+      createSlide()
+      createFrameElement(tool.url, getToolType(tool.tool_id))
+      tool.isSet = true
+    }
+  })
+  changeSlideIndex(childPageIndex.value)
+  setTool()
+}
+
+// 判断工具type
+const toolTypeMap: Record<string, number> = {
+  'choice': 45,
+  'qa': 15,
+  'poll': 78,
+  'photo': 79
+}
+
+const currentToolType = computed(() => {
+  const toolId = toolsArray[selectedToolIndex.value]?.tool_id
+  return toolId ? toolTypeMap[toolId] || 0 : 0
+})
+
+const getToolType = (toolId: string) => {
+  return toolTypeMap[toolId] || 0
+}
+</script>
+
+<style lang="scss" scoped>
+.child-page-display {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background-color: #fffbeb;
+  position: absolute;
+  top: 0;
+  left: 0;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.child-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 7px 20px;
+  background: linear-gradient(180deg, #fcd34d 0%, #f59e0b 100%);
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .badge {
+    background-color: #d97706;
+    color: white;
+    padding: 4px 12px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 600;
+  }
+
+  .hint {
+    color: #78350f;
+    font-size: 14px;
+  }
+
+  .header-right {
+    display: flex;
+    gap: 8px;
+    background: #d97706;
+    padding: 5px 10px;
+    border-radius: 30px;
+  }
+
+  .candidate-tag {
+    background-color: #d97706;
+    color: white;
+    padding: 4px 12px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    border: none;
+    cursor: pointer;
+    transition: all 0.2s;
+    border-radius: 30px;
+
+    &:hover {
+      background-color: #b45309;
+    }
+
+    &.active {
+      background-color: white;
+      color: #d97706;
+    }
+  }
+}
+
+.child-page-body {
+  flex: 1;
+  display: flex;
+  padding: 20px;
+  gap: 20px;
+  overflow: auto;
+}
+
+.question-area {
+  flex: 1;
+  background-color: white;
+  border: 2px solid #f59e0b;
+  border-radius: 8px;
+  padding: 20px;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+
+  iframe {
+    width: 100%;
+    height: 100%;
+    border: none;
+  }
+}
+
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(255, 251, 235, 0.95);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  gap: 16px;
+  z-index: 100;
+}
+
+.loading-spinner {
+  width: 48px;
+  height: 48px;
+  border: 4px solid #f59e0b;
+  border-top-color: transparent;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+.loading-text {
+  color: #d97706;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.side-panel {
+  width: 90px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+
+  .side-btn {
+    padding: 12px 20px;
+    border: none;
+    border-radius: 8px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &.primary {
+      background-color: #f59e0b;
+      color: white;
+
+      &:hover {
+        background-color: #d97706;
+      }
+    }
+
+    &.secondary {
+      background-color: white;
+      color: #6b7280;
+      border: 1px solid #e5e7eb;
+
+      &:hover {
+        border-color: #f59e0b;
+        color: #f59e0b;
+      }
+    }
+
+    &.link {
+      background-color: transparent;
+      color: #f59e0b;
+      text-align: left;
+      padding-left: 0;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .divider {
+    height: 1px;
+    background-color: #e5e7eb;
+    margin: 4px 0;
+  }
+}
+</style>

+ 11 - 2
src/views/Editor/Canvas/EditableElement.vue

@@ -17,7 +17,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue'
+import { computed, onMounted } from 'vue'
 import { storeToRefs } from 'pinia'
 import { ElementTypes, type PPTElement } from '@/types/slides'
 import type { ContextmenuItem } from '@/components/Contextmenu/types'
@@ -30,7 +30,7 @@ import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
 import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
 import useSelectElement from '@/hooks/useSelectElement'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
-import { useSlidesStore } from '@/store'
+import { useSlidesStore, useMainStore } from '@/store'
 import message from '@/utils/message'
 import { lang } from '@/main'
 
@@ -46,6 +46,15 @@ import LatexElement from '@/views/components/element/LatexElement/index.vue'
 import VideoElement from '@/views/components/element/VideoElement/index.vue'
 import AudioElement from '@/views/components/element/AudioElement/index.vue'
 import FrameElement from '@/views/components/element/FrameElement/index.vue'
+import { initClipboardFocus } from '@/utils/clipboard'
+
+const mainStore = useMainStore()
+
+onMounted(() => {
+  console.log('ed,onMounted')
+  initClipboardFocus()
+  mainStore.setDisableHotkeysState(false)
+})
 
 const props = defineProps<{
   elementInfo: PPTElement

+ 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>

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

@@ -94,6 +94,13 @@
 
     <div class="drag-mask" v-if="spaceKeyState"></div>
 
+    <ChildPageDisplay :style="{
+        width: viewportStyles.width * canvasScale + 'px',
+        height: viewportStyles.height * canvasScale + 'px',
+        left: viewportStyles.left + 'px',
+        top: viewportStyles.top + 'px',
+      }" v-if="childPageVisible" />
+
     <Ruler :viewportStyles="viewportStyles" :elementList="elementList" v-if="showRuler" />
 
     <Modal
@@ -159,6 +166,7 @@ import Operate from './Operate/index.vue'
 import LinkDialog from './LinkDialog.vue'
 import WebpageLinkEditDialog from './WebpageLinkEditDialog.vue'
 import Modal from '@/components/Modal.vue'
+import ChildPageDisplay from './ChildPageDisplay.vue'
 import api from '@/services/course'
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
@@ -194,6 +202,7 @@ const {
 } = storeToRefs(mainStore)
 const { currentSlide } = storeToRefs(useSlidesStore())
 const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
+const { childPageVisible } = storeToRefs(mainStore)
 
 const viewportRef = useTemplateRef<HTMLElement>('viewportRef')
 const alignmentLines = ref<AlignmentLineProps[]>([])
@@ -384,6 +393,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键时:缩放画布

+ 6 - 2
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -103,9 +103,13 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
-    75: lang.ssBiliVideo,
-    76: lang.ssCreative
+    75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+    76: lang.ssCreative,
+    77: lang.ssEnglishSpeakingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type] || lang.ssUnknown
 }

+ 245 - 5
src/views/Editor/CanvasTool/index2.vue

@@ -1,6 +1,8 @@
 <template>
   <div class="canvas-tool">
     <div class="left-handler">
+      <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="lang.ssUndoTip" @click="undo()"  v-if="!isFrame" />
+      <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="lang.ssRedoTip" @click="redo()"  v-if="!isFrame" />
       <Popover trigger="click" v-model:value="toolVisible" style="height: 100%;display: flex;align-items: center;"
         :offset="10" v-if="hasInteractiveTool">
         <template #content>
@@ -17,6 +19,20 @@
             </svg>
             <span>{{ lang.ssQandA }}</span>
           </div>
+          <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
+            <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <polyline points="9 11 12 14 22 4"></polyline>
+                <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+            </svg>
+            <span>{{ lang.ssVote }}</span>
+          </div>
+          <div class="popover-item" @click="editContent(79)" v-if="frametype != 79">
+            <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
+            <span>{{ lang.ssPhoto }}</span>
+          </div>
         </template>
         <div class="handler-item" :class="{ active: toolVisible }">
           <span class="svg-icon">
@@ -29,6 +45,14 @@
               stroke-width="2">
               <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
             </svg>
+            <svg v-if="frametype == 78" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <polyline points="9 11 12 14 22 4"></polyline>
+                <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+            </svg>
+            <svg v-if="frametype == 79" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
           </span>
           <span>{{ iframeLabel }}</span>
           <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
@@ -104,6 +128,87 @@
           <span>{{ lang.ssShape }}</span>
         </div>
       </Popover>
+      <RichTextBase  v-if="handleElement?.type == 'text'"/>
+      <div style="display: flex; align-items: center;" v-if="handleElement?.type == 'shape'">
+        <Select 
+          style="flex: 1;" 
+          :value="fillType" 
+          @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
+          :options="[
+            { label: lang.ssSolidFill, value: 'fill' },
+            { label: lang.ssGradFill, value: 'gradient' },
+            // { label: lang.ssImgFill, value: 'pattern' },
+          ]"
+        />
+        <Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
+          <template #content>
+            <ColorPicker
+              :modelValue="fill"
+              @update:modelValue="value => updateFill(value)"
+            />
+          </template>
+          <ColorButton style="width: 100px;" :color="fill" />
+        </Popover>
+        <Select 
+          style="flex: 1;" 
+          :value="gradient.type" 
+          @update:value="value => updateGradient({ type: value as GradientType })"
+          v-else-if="fillType === 'gradient'"
+          :options="[
+            { label: lang.ssLinearGrad, value: 'linear' },
+            { label: lang.ssRadialGrad, value: 'radial' },
+          ]"
+        />
+      </div>
+
+      <template v-if="fillType === 'gradient' && handleElement?.type == 'shape'">
+        <div class="row">
+          <GradientBar
+            :value="gradient.colors"
+            :index="currentGradientIndex"
+            @update:value="value => updateGradient({ colors: value })"
+            @update:index="index => currentGradientIndex = index"
+          />
+        </div>
+        <div class="row">
+          <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
+          <Popover trigger="click" style="width: 60%;">
+            <template #content>
+              <ColorPicker
+                :modelValue="gradient.colors[currentGradientIndex].color"
+                @update:modelValue="value => updateGradientColors(value)"
+              />
+            </template>
+            <ColorButton :color="gradient.colors[currentGradientIndex].color" />
+          </Popover>
+        </div>
+        <div class="row" v-if="gradient.type === 'linear'">
+          <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
+          <Slider
+            style="width: 60%;"
+            :min="0"
+            :max="360"
+            :step="15"
+            :value="gradient.rotate"
+            @update:value="value => updateGradient({ rotate: value as number })" 
+          />
+        </div>
+      </template>
+      <ElementFlip  v-if="handleElement?.type == 'shape'"/>
+
+      <!-- 英语口语工具:重置预览 -->
+      <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>
 
 
@@ -130,9 +235,10 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue'
+import { ref, computed, watch } 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'
@@ -156,11 +262,37 @@ import Popover from '@/components/Popover.vue'
 import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
 import Button from '@/components/Button.vue'
 
+import RichTextBase from '@/views/Editor/Toolbar/common/RichTextBase2.vue'
+import Select from '@/components/Select.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import GradientBar from '@/components/GradientBar.vue'
+import Slider from '@/components/Slider.vue'
+import ColorButton from '@/components/ColorButton.vue'
+import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.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)
@@ -179,7 +311,7 @@ const viewMode = computed(() => getInitialViewMode())
 
 const hasInteractiveTool = computed(() => {
   const elements = currentSlide.value?.elements || []
-  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
+  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
 })
 
 const iframeLabel = computed(() => {
@@ -198,9 +330,13 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
-    75: lang.ssBiliVideo,
-    76: lang.ssCreative
+    75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+    76: lang.ssCreative,
+    77: lang.ssEnglishSpeakingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type] || lang.ssUnknown
 }
@@ -396,12 +532,95 @@ const editContent = (toolType: number) => {
     }
     const parentWindow = window.parent as ParentWindowWithToolList
     const elements = currentSlide.value?.elements || []
-    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
     parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
     toolVisible.value = false
   })
 }
 
+import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
+const { handleElement, handleElementId } = storeToRefs(useMainStore())
+const fillType = ref('fill')
+const fill = ref<string>('#000')
+const textAlign = ref('middle')
+const pattern = ref('')
+import emitter, { EmitterEvents } from '@/utils/emitter'
+const { addHistorySnapshot } = useHistorySnapshot()
+const currentGradientIndex = ref(0)
+
+watch(handleElementId, () => {
+  currentGradientIndex.value = 0
+})
+
+const updateElement = (props: Partial<PPTShapeElement>) => {
+  slidesStore.updateElement({ id: handleElementId.value, props })
+  addHistorySnapshot()
+}
+
+const gradient = ref<Gradient>({
+  type: 'linear', 
+  rotate: 0,
+  colors: [
+    { pos: 0, color: '#fff' },
+    { pos: 100, color: '#fff' },
+  ],
+})
+
+watch(handleElement, () => {
+  if (!handleElement.value || handleElement.value.type !== 'shape') return
+
+  fill.value = handleElement.value.fill || '#fff'
+  const defaultGradientColor = [
+    { pos: 0, color: fill.value },
+    { pos: 100, color: '#fff' },
+  ]
+  gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
+  pattern.value = handleElement.value.pattern || ''
+  fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
+  textAlign.value = handleElement.value?.text?.align || 'middle'
+
+  if (handleElement.value.text?.content) {
+    emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
+  }
+}, { deep: true, immediate: true })
+
+// 设置填充类型:渐变、纯色
+const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
+  console.log('设置填充类型:', type)
+  if (type === 'fill') {
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
+    addHistorySnapshot()
+  }
+  else if (type === 'gradient') {
+    currentGradientIndex.value = 0
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
+    updateElement({ gradient: gradient.value })
+  }
+  // else if (type === 'pattern') {
+  //   slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
+  //   updateElement({ pattern: '' })
+  // }
+}
+
+// 设置填充色
+const updateFill = (value: string) => {
+  updateElement({ fill: value })
+}
+
+// 设置渐变填充
+const updateGradient = (gradientProps: Partial<Gradient>) => {
+  if (!gradient.value) return
+  const _gradient = { ...gradient.value, ...gradientProps }
+  updateElement({ gradient: _gradient })
+}
+const updateGradientColors = (color: string) => {
+  const colors = gradient.value.colors.map((item, index) => {
+    if (index === currentGradientIndex.value) return { ...item, color }
+    return item
+  })
+  updateGradient({ colors })
+}
+
 </script>
 
 <style lang="scss" scoped>
@@ -481,6 +700,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;
@@ -624,4 +857,11 @@ const editContent = (toolType: number) => {
     }
   }
 }
+
+.row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 180px;
+}
 </style>

+ 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]!))
+}

+ 80 - 0
src/views/Editor/Thumbnails/ThumbnailChildPage.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="thumbnail-child-page" @click="handleClick">
+    <div class="child-page-icon">
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
+        <line x1="9" y1="9" x2="15" y2="9"></line>
+        <line x1="9" y1="15" x2="15" y2="15"></line>
+      </svg>
+    </div>
+    <div class="child-page-label">{{ lang.ssChildPageLabel }}</div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { lang } from '@/main'
+import { useMainStore, useSlidesStore } from '@/store'
+import { storeToRefs } from 'pinia'
+
+const props = defineProps<{
+  toolsArray?: any[]
+  slideIndex?: number
+}>()
+
+const mainStore = useMainStore()
+const { setChildPageState } = mainStore
+const slidesStore = useSlidesStore()
+const { slideIndex } = storeToRefs(slidesStore)
+
+// 切换页面
+const changeSlideIndex = (index: number) => {
+  mainStore.setActiveElementIdList([])
+
+  if (slideIndex.value === index) return
+  slidesStore.updateSlideIndex(index)
+}
+
+const handleClick = () => {
+  changeSlideIndex(props.slideIndex || 0)
+  setChildPageState(true, props.toolsArray || [], props.slideIndex || 0)
+}
+</script>
+
+<style lang="scss" scoped>
+.thumbnail-child-page {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 5px;
+  width: 60px;
+  height: 60px;
+  background-color: #fffbeb;
+  border: 2px dashed #f59e0b;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    background-color: #fef3c7;
+    border-style: solid;
+  }
+
+  .child-page-icon {
+    width: 24px;
+    height: 24px;
+    color: #f59e0b;
+    margin-bottom: 4px;
+  }
+
+  .child-page-label {
+    font-size: 10px;
+    color: #d97706;
+    text-align: center;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 100%;
+  }
+}
+</style>

+ 193 - 45
src/views/Editor/Thumbnails/index2.vue

@@ -1,11 +1,11 @@
-<template>
+<template>
   <div 
     class="thumbnails"
     @mousedown="() => setThumbnailsFocus(true)"
     v-click-outside="() => setThumbnailsFocus(false)"
     v-contextmenu="contextmenusThumbnails"
   >
-    <div class="add-slide">
+    <div class="add-slide" v-show="false">
       <div class="btn" @click="createSlide()"><IconPlus class="icon" /></div>
       <!-- 添加幻灯片 <Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center>
         <template #content>
@@ -56,14 +56,23 @@
               'selected': selectedSlidesIndex.includes(index),
             }"
             @mousedown="$event => handleClickSlideThumbnail($event, index)"
-            @dblclick="enterScreening()"
-            v-contextmenu="contextmenusThumbnailItem"
+            v-contextmenu2="contextmenusThumbnailItem"
           >
+            <!-- @dblclick="enterScreening()" -->
             <div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div>
             <ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" />
+            <div class="page_menu" @click.stop="$event => showPageMenu($event, index)">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <circle cx="12" cy="12" r="1"></circle>
+                  <circle cx="12" cy="5" r="1"></circle>
+                  <circle cx="12" cy="19" r="1"></circle>
+              </svg>
+            </div>
 
             <div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
           </div>
+          <ThumbnailChildPage v-if="element.toolsArray && element.toolsArray.length" :toolsArray="element.toolsArray" :slideIndex="index" />
+          <div class="add-page-between" @click="createSlide()">+</div>
         </div>
       </template>
     </Draggable>
@@ -73,7 +82,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, nextTick, ref, watch, useTemplateRef } from 'vue'
+import { computed, nextTick, ref, watch, useTemplateRef, createVNode, render } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
 import { fillDigit } from '@/utils/common'
@@ -90,6 +99,8 @@ import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Templates from './Templates.vue'
 import Popover from '@/components/Popover.vue'
 import Draggable from 'vuedraggable'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
+import ThumbnailChildPage from './ThumbnailChildPage.vue'
 
 // 检测是否为移动设备(包括iPad和手机)
 const isMobileDevice = computed(() => {
@@ -167,6 +178,7 @@ const changeSlideIndex = (index: number) => {
 
 // 点击缩略图
 const handleClickSlideThumbnail = (e: MouseEvent, index: number) => {
+  mainStore.setChildPageState(false, [], index)
   if (editingSectionId.value) return
 
   const isMultiSelected = selectedSlidesIndex.value.length > 1
@@ -315,54 +327,117 @@ const contextmenusThumbnails = (): ContextmenuItem[] => {
   ]
 }
 
-const contextmenusThumbnailItem = (): ContextmenuItem[] => {
-  return [
-    {
-      text: lang.ssCut,
-      subText: 'Ctrl + X',
-      handler: cutSlide,
-    },
-    {
-      text: lang.ssCopy,
-      subText: 'Ctrl + C',
-      handler: copySlide,
-    },
-    {
-      text: lang.ssPaste,
-      subText: 'Ctrl + V',
-      handler: pasteSlide,
-    },
-    {
-      text: lang.ssSelectAll,
-      subText: 'Ctrl + A',
-      handler: selectAllSlide,
+const showPageMenu = (event: MouseEvent, index: number) => {
+  event.stopPropagation()
+  
+  const menus = contextmenusThumbnailItem()
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    document.body.removeEventListener('scroll', removeContextmenu)
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  const options = {
+    axis: { x: event.clientX - 20, y: event.clientY - 20 },
+    el: event.currentTarget as HTMLElement,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+import { showConfirmDialog } from '@/utils/confirmDialog'
+
+const confirmDeleteSlide = () => {
+  showConfirmDialog({
+    title: lang.ssConfirmDel,
+    content: lang.ssConfirmDelContent,
+    confirmText: lang.ssConfirm,
+    cancelText: lang.ssCancel,
+    width: 400,
+    onConfirm: () => {
+      deleteSlide()
     },
-    { divider: true },
-    {
-      text: lang.ssNewPage,
-      subText: 'Enter',
-      handler: createSlide,
+    onCancel: () => {
+      console.log('取消删除')
     },
+  })
+
+}
+
+const contextmenusThumbnailItem = (): ContextmenuItem[] => {
+  return [
+    // {
+    //   text: lang.ssCut,
+    //   subText: 'Ctrl + X',
+    //   handler: cutSlide,
+    // },
+    // {
+    //   text: lang.ssCopy,
+    //   subText: 'Ctrl + C',
+    //   handler: copySlide,
+    // },
+    // {
+    //   text: lang.ssPaste,
+    //   subText: 'Ctrl + V',
+    //   handler: pasteSlide,
+    // },
+    // {
+    //   text: lang.ssSelectAll,
+    //   subText: 'Ctrl + A',
+    //   handler: selectAllSlide,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssNewPage,
+    //   subText: 'Enter',
+    //   handler: createSlide,
+    // },
+    // {
+    //   text: lang.ssDupPage,
+    //   subText: 'Ctrl + D',
+    //   handler: copyAndPasteSlide,
+    // },
+    // {
+    //   text: lang.ssDelPage,
+    //   subText: 'Delete',
+    //   handler: () => deleteSlide(),
+    // },
+    // {
+    //   text: lang.ssAddSect,
+    //   handler: createSection,
+    //   disable: !!currentSlide.value.sectionTag,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssPlayFromCur,
+    //   subText: 'Shift + F5',
+    //   handler: enterScreening,
+    // },
+
     {
-      text: lang.ssDupPage,
-      subText: 'Ctrl + D',
+      text: lang.ssDupPage2,
       handler: copyAndPasteSlide,
     },
     {
-      text: lang.ssDelPage,
-      subText: 'Delete',
-      handler: () => deleteSlide(),
-    },
-    {
-      text: lang.ssAddSect,
-      handler: createSection,
-      disable: !!currentSlide.value.sectionTag,
+      text: lang.ssNewPage2,
+      handler: createSlide,
     },
-    { divider: true },
     {
-      text: lang.ssPlayFromCur,
-      subText: 'Shift + F5',
-      handler: enterScreening,
+      text: lang.ssDelPage2,
+      handler: confirmDeleteSlide,
     },
   ]
 }
@@ -425,6 +500,18 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   display: flex;
   align-items: center;
   gap: 10px;
+
+  .thumbnail-container {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    &:hover {
+      .add-page-between {
+        display: flex;
+      }
+    }
+  }
 }
 .thumbnail-item {
   display: flex;
@@ -490,6 +577,10 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   color: #999;
   width: 20px;
   cursor: grab;
+  position: absolute;
+  z-index: 999;
+  top: 10px;
+  left: 10px;
 
   &.offset-left {
     position: relative;
@@ -500,6 +591,39 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     cursor: grabbing;
   }
 }
+
+.page_menu{
+  width: 24px;
+  height: 24px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  position: absolute;
+  z-index: 999;
+  bottom: 10px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e5e7eb;
+  transition: .3s;
+  border-radius: 5px;
+
+  svg{
+    color: #6b7280;
+    height: 15px;
+  }
+
+  .active,
+  &:hover{
+    border-color: $themeColor2;
+    background-color: $themeColor2;
+
+    svg{
+      color: #fff;
+    }
+  }
+}
+
 .page-number {
   /* height: 100%; */
   font-size: 12px;
@@ -557,4 +681,28 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     font-size: 12px;
   }
 }
+
+.add-page-between {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px dashed #d1d5db;
+    background: white;
+    cursor: pointer;
+    display: none;
+    // align-items: center;
+    justify-content: center;
+    color: #9ca3af;
+    font-size: 20px;
+    transition: all 0.2s;
+    flex-shrink: 0;
+    line-height: 23px;
+
+    &:hover {
+      border-color: #F78B22;
+      border-style: solid;
+      color: #F78B22;
+      background: #FFF8F0;
+    }
+}
 </style>

+ 1 - 0
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -37,6 +37,7 @@ const panelMap = {
 const { handleElement } = storeToRefs(useMainStore())
 
 const currentPanelComponent = computed<unknown>(() => {
+  console.log('handleElement.value', handleElement.value)
   return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
 })
 </script>

+ 59 - 0
src/views/Editor/Toolbar/common/ElementFlip2.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="element-flip">
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipV"
+        @click="updateFlip({ flipV: !flipV })"
+      ><IconFlipVertically /> {{lang.flipVertically}}</CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipH"
+        @click="updateFlip({ flipH: !flipH })"
+      ><IconFlipHorizontally /> {{lang.flipHorizontally}}</CheckboxButton>
+    </ButtonGroup>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { ImageOrShapeFlip } from '@/types/slides'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+
+import { lang } from '@/main'
+
+const slidesStore = useSlidesStore()
+const { handleElement } = storeToRefs(useMainStore())
+
+const flipH = ref(false)
+const flipV = ref(false)
+
+watch(handleElement, () => {
+  if (handleElement.value && (handleElement.value.type === 'image' || handleElement.value.type === 'shape')) {
+    flipH.value = !!handleElement.value.flipH
+    flipV.value = !!handleElement.value.flipV
+  }
+}, { deep: true, immediate: true })
+
+const { addHistorySnapshot } = useHistorySnapshot()
+
+const updateFlip = (flipProps: ImageOrShapeFlip) => {
+  if (!handleElement.value) return
+  slidesStore.updateElement({ id: handleElement.value.id, props: flipProps })
+  addHistorySnapshot()
+}
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+</style>

+ 476 - 0
src/views/Editor/Toolbar/common/RichTextBase2.vue

@@ -0,0 +1,476 @@
+<template>
+  <div class="rich-text-base">
+    <SelectGroup class="row">
+      <Select
+        style="width: 60%;"
+        :value="richTextAttrs.fontname"
+        search
+        :searchLabel="lang.searchFont"
+        @update:value="value => emitRichTextCommand('fontname', value as string)"
+        :options="FONTS"
+      >
+        <template #icon>
+          <IconFontSize />
+        </template>
+      </Select>
+      <Select
+        style="width: 40%;"
+        :value="richTextAttrs.fontsize"
+        search
+        :searchLabel="lang.searchFontSize"
+        @update:value="value => emitRichTextCommand('fontsize', value as string)"
+        :options="fontSizeOptions.map(item => ({
+          label: item, value: item
+        }))"
+      >
+        <template #icon>
+          <IconAddText />
+        </template>
+      </Select>
+    </SelectGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.color"
+            @update:modelValue="value => emitRichTextCommand('color', value)"
+          />
+        </template>
+        <TextColorButton first v-tooltip="lang.textColor" :color="richTextAttrs.color">
+          <IconText />
+        </TextColorButton>
+      </Popover>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.backcolor"
+            @update:modelValue="value => emitRichTextCommand('backcolor', value)"
+          />
+        </template>
+        <TextColorButton v-tooltip="lang.highlight" :color="richTextAttrs.backcolor">
+          <IconHighLight />
+        </TextColorButton>
+      </Popover>
+      <Button 
+        style="display: flex;align-items: center;"
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeAdd"
+        @click="emitRichTextCommand('fontsize-add')"
+      ><IconFontSize />+</Button>
+      <Button
+        style="display: flex;align-items: center;"
+        last
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeReduce" 
+        @click="emitRichTextCommand('fontsize-reduce')"
+      ><IconFontSize />-</Button>
+    </ButtonGroup>
+
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.bold"
+        v-tooltip="lang.bold"
+        @click="emitRichTextCommand('bold')"
+      ><IconTextBold /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.em"
+        v-tooltip="lang.italic"
+        @click="emitRichTextCommand('em')"
+      ><IconTextItalic /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.underline"
+        v-tooltip="lang.underline"
+        @click="emitRichTextCommand('underline')"
+      ><IconTextUnderline /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.strikethrough"
+        v-tooltip="lang.strikethrough"
+        @click="emitRichTextCommand('strikethrough')"
+      ><IconStrikethrough /></CheckboxButton>
+    </ButtonGroup>
+
+    <!-- <ButtonGroup class="row">
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.superscript"
+        v-tooltip="'上标'"
+        @click="emitRichTextCommand('superscript')"
+      >A²</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.subscript"
+        v-tooltip="'下标'"
+        @click="emitRichTextCommand('subscript')"
+      >A₂</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.code"
+        v-tooltip="'行内代码'"
+        @click="emitRichTextCommand('code')"
+      ><IconCode /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.blockquote"
+        v-tooltip="'引用'"
+        @click="emitRichTextCommand('blockquote')"
+      ><IconQuote /></CheckboxButton>
+    </ButtonGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" v-model:value="AIPopoverVisible" style="width: 25%;">
+        <template #content>
+          <PopoverMenuItem center @click="execAI('美化改写')">美化</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('扩写丰富')">扩写</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('精简提炼')">精简</PopoverMenuItem>
+        </template>
+        <CheckboxButton
+          first
+          style="width: 100%;"
+          v-tooltip="'AI辅助'"
+        ><span :class="{ 'ai-loading': isAIWriting }">{{ isAIWriting ? '' : 'AI' }}</span></CheckboxButton>
+      </Popover>
+      <CheckboxButton
+        style="flex: 1;"
+        v-tooltip="'清除格式'"
+        @click="emitRichTextCommand('clear')"
+      ><IconFormat /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="!!textFormatPainter"
+        v-tooltip="'格式刷(双击连续使用)'"
+        @click="toggleTextFormatPainter()"
+        @dblclick="toggleTextFormatPainter(true)"
+      ><IconFormatBrush /></CheckboxButton>
+      <Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 25%;">
+        <template #content>
+          <div class="link-popover">
+            <Input v-model:value="link" placeholder="请输入超链接" />
+            <div class="btns">
+              <Button size="small" :disabled="!richTextAttrs.link" @click="removeLink()" style="margin-right: 5px;">移除</Button>
+              <Button size="small" type="primary" @click="updateLink(link)">确认</Button>
+            </div>
+          </div>
+        </template>
+        <CheckboxButton
+          last
+          style="width: 100%;"
+          :checked="!!richTextAttrs.link"
+          v-tooltip="'超链接'"
+          @click="openLinkPopover()"
+        ><IconLinkOne /></CheckboxButton>
+      </Popover>
+    </ButtonGroup>
+    <Divider />
+
+    <RadioGroup 
+      class="row" 
+      button-style="solid" 
+      :value="richTextAttrs.align"
+      @update:value="value => emitRichTextCommand('align', value)"
+    >
+      <RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
+      <RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
+      <RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
+      <RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
+    </RadioGroup>
+
+    <div class="row" passive>
+      <ButtonGroup style="flex: 1;">
+        <Button
+          first
+          :type="richTextAttrs.bulletList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'项目符号'"
+          @click="emitRichTextCommand('bulletList')"
+        ><IconList /></Button>
+        <Popover trigger="click" v-model:value="bulletListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in bulletListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('bulletList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button
+          first
+          :type="richTextAttrs.orderedList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'编号'"
+          @click="emitRichTextCommand('orderedList')"
+        ><IconOrderedList /></Button>
+        <Popover trigger="click" v-model:value="orderedListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in orderedListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('orderedList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div>
+
+    <div class="row">
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'减小段落缩进'" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
+        <Popover trigger="click" v-model:value="indentLeftPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'增大段落缩进'" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
+        <Popover trigger="click" v-model:value="indentRightPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div> -->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import api from '@/services'
+import { useMainStore } from '@/store'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+import { FONTS } from '@/configs/font'
+import useTextFormatPainter from '@/hooks/useTextFormatPainter'
+import message from '@/utils/message'
+import { htmlToText } from '@/utils/common'
+
+import TextColorButton from '@/components/TextColorButton2.vue'
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import Input from '@/components/Input.vue'
+import Button from '@/components/Button.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+import Select from '@/components/Select.vue'
+import SelectGroup from '@/components/SelectGroup.vue'
+import Divider from '@/components/Divider.vue'
+import Popover from '@/components/Popover.vue'
+import RadioButton from '@/components/RadioButton.vue'
+import RadioGroup from '@/components/RadioGroup.vue'
+import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
+
+import { lang } from '@/main'
+
+
+
+const { handleElement, handleElementId, richTextAttrs, textFormatPainter } = storeToRefs(useMainStore())
+
+const { toggleTextFormatPainter } = useTextFormatPainter()
+
+const fontSizeOptions = [
+  '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
+  '36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
+  '80px', '88px', '96px', '104px', '112px', '120px',
+]
+
+const emitRichTextCommand = (command: string, value?: string) => {
+  emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
+}
+
+const bulletListPanelVisible = ref(false)
+const orderedListPanelVisible = ref(false)
+const indentLeftPanelVisible = ref(false)
+const indentRightPanelVisible = ref(false)
+
+const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
+const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
+
+const link = ref('')
+const linkPopoverVisible = ref(false)
+const AIPopoverVisible = ref(false)
+const isAIWriting = ref(false)
+
+watch(richTextAttrs, () => linkPopoverVisible.value = false)
+watch(handleElementId, () => {
+  if (isAIWriting.value) isAIWriting.value = false
+})
+
+const openLinkPopover = () => {
+  link.value = richTextAttrs.value.link
+}
+const updateLink = (link?: string) => {
+  const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
+  if (!link || !linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
+
+  emitRichTextCommand('link', link)
+  linkPopoverVisible.value = false
+}
+
+const removeLink = () => {
+  emitRichTextCommand('link')
+  linkPopoverVisible.value = false
+}
+
+const execAI = async (command: string) => {
+  AIPopoverVisible.value = false
+
+  if (!handleElement.value) return
+
+  let content = ''
+  if (handleElement.value.type === 'text' && handleElement.value.content) {
+    content = handleElement.value.content
+  }
+  if (handleElement.value.type === 'shape' && handleElement.value.text && handleElement.value.text.content) {
+    content = handleElement.value.text.content
+  }
+
+  if (!content) return message.error('没有可以执行的文本内容')
+
+  let resultText = ''
+
+  const stream = await api.AI_Writing({
+    content: htmlToText(content),
+    command,
+  })
+
+  isAIWriting.value = true
+
+  const reader: ReadableStreamDefaultReader = stream.body.getReader()
+  const decoder = new TextDecoder('utf-8')
+  
+  const readStream = () => {
+    reader.read().then(({ done, value }) => {
+      if (!isAIWriting.value) return
+      if (done) {
+        isAIWriting.value = false
+        return
+      }
+
+      const chunk = decoder.decode(value, { stream: true })
+      resultText += chunk
+      emitRichTextCommand('replace', resultText)
+
+      readStream()
+    })
+  }
+  readStream()
+}
+</script>
+
+<style lang="scss" scoped>
+.rich-text-base {
+  user-select: none;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  ::v-deep(.ai-loading) {
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+    margin-top: 8px;
+    border: 1px solid $themeColor;
+    border-top-color: transparent;
+    border-radius: 50%;
+    animation: spinner .8s linear infinite;
+  }
+}
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+.font-size-btn {
+  padding: 0 15px;
+}
+.link-popover {
+  width: 240px;
+
+  .btns {
+    margin-top: 10px;
+    text-align: right;
+  }
+}
+.list-wrap {
+  width: 176px;
+  color: #666;
+  padding: 8px;
+  margin: -12px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+}
+.list {
+  background-color: $lightGray;
+  padding: 4px 4px 4px 20px;
+  cursor: pointer;
+
+  &:not(:nth-child(3n)) {
+    margin-right: 8px;
+  }
+
+  &:nth-child(4),
+  &:nth-child(5),
+  &:nth-child(6) {
+    margin-top: 8px;
+  }
+
+  &:hover {
+    color: $themeColor;
+
+    span {
+      background-color: $themeColor;
+    }
+  }
+}
+.list-item {
+  width: 24px;
+  height: 12px;
+  position: relative;
+  font-size: 12px;
+  top: -3px;
+
+  span {
+    width: 100%;
+    height: 2px;
+    display: inline-block;
+    position: absolute;
+    top: 8px;
+    background-color: #666;
+  }
+}
+.popover-btn {
+  padding: 0 3px;
+}
+
+@keyframes spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 5 - 2
src/views/Editor/index.vue

@@ -43,7 +43,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore } from '@/store'
 import useGlobalHotkey from '@/hooks/useGlobalHotkey'
@@ -79,7 +79,10 @@ const closeAIPPTDialog = () => mainStore.setAIPPTDialogState(false)
 const remarkHeight = ref(0)
 
 useGlobalHotkey()
-usePasteEvent()
+onMounted(() => {
+  console.log('ed,onMounted')
+  usePasteEvent()
+})
 </script>
 
 <style lang="scss" scoped>

+ 11 - 5
src/views/Editor/index3.vue

@@ -162,9 +162,9 @@
     </div>
     <!-- <EditorHeader class="layout-header" /> -->
     <div class="layout-content">
-      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
+      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" :courseTitle="courseTitle" />
       <div class="layout-content-center">
-        <CanvasTool class="center-top" />
+        <CanvasTool class="center-top"  :userid="props.userid"/>
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
           :courseid="props.courseid" @course-loaded="handleCourseLoaded"  ref="canvas"/>
         <!-- <Remark
@@ -200,7 +200,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import useGlobalHotkey from '@/hooks/useGlobalHotkey'
@@ -222,7 +222,7 @@ import Modal from '@/components/Modal.vue'
 import CollapsibleToolbar from '@/components/CollapsibleToolbar/index2.vue'
 import CreateCourseDialog from '@/components/CreateCourseDialog.vue'
 import api from '@/services/course'
-import lang from '../lang/cn.json'
+import { lang } from '@/main'
 
 
 interface ParentWindowWithToolList extends Window {
@@ -246,6 +246,7 @@ const props = withDefaults(defineProps<Props>(), {
   userid: null,
 })
 
+
 const mainStore = useMainStore()
 const slidesStore = useSlidesStore()
 const { dialogForExport, showSelectPanel, showSearchPanel, showNotesPanel, showMarkupPanel, showAIPPTDialog } = storeToRefs(mainStore)
@@ -258,7 +259,9 @@ const sidebarCollapsed = ref(false)
 const showCreateCourseDialog = ref(false)
 const isDropdownOpen = ref(false)
 const courseTitle = ref(lang.ssNewCourse)
-
+watch(() => courseTitle.value, (newVal) => {
+  mainStore.setCourseTitle(newVal || '')
+})
 // 课程标题相关
 const editingTitle = ref(false)
 const titleInput = ref<HTMLInputElement | null>(null)
@@ -329,6 +332,7 @@ const handleDelete = () => {
 }
 
 const changeCourse = () => {
+  console.trace('[TRACE] changeCourse fired → parentWindow.save(3)')
   parentWindow.setCouresTitle?.(courseTitle.value)
   parentWindow.save?.(3)
 }
@@ -348,6 +352,7 @@ const handlePreview = () => {
 }
 
 const handleSave = () => {
+  console.trace('[TRACE] handleSave fired → parentWindow.save(1)')
   console.log('保存课程')
   // lastSaveTime.value = new Date().toLocaleString()
   parentWindow.save?.(1)
@@ -401,6 +406,7 @@ onMounted(() => {
     showCreateCourseDialog.value = true
   }
   ccourseid.value = props.courseid || ''
+  mainStore.setUserid(props.userid || '')
   // 添加点击外部关闭下拉菜单的事件监听
   document.addEventListener('click', handleClickOutside)
 })

+ 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,
+  }
+}

+ 21 - 4
src/views/Student/components/aiChat.vue

@@ -71,7 +71,7 @@
             </button>
           </div>
         </div>
-        <textarea class="ai-input" placeholder="请输入你的问题..." v-model="inputText" @keyup.enter.exact="sendMessage"
+        <textarea class="ai-input" :placeholder="lang.ssEnterQuestion" v-model="inputText" @keyup.enter.exact="sendMessage"
           />
 
       </div>
@@ -117,11 +117,10 @@ import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2, getAgentCh
 import { useSlidesStore } from '@/store'
 import { lang } from '@/main'
 import MarkdownIt from 'markdown-it'
-import { getWorkPageId } from '@/services/course'
+import { getWorkPageId, getClassById } from '@/services/course'
 import FileInput from '@/components/FileInput2.vue'
 import axios from '@/services/config'
 import message from '@/utils/message'
-import { getClassById } from '@/services/course'
 
 interface ChatMessage {
   uid?: string
@@ -463,6 +462,9 @@ const sendAction = async (action: string) => {
     case '15':
       choice = '问答题'
       break
+    case '78':
+      choice = '投票'
+      break
     default:
       choice = ''
       break
@@ -480,6 +482,13 @@ const sendAction = async (action: string) => {
       student_answer += `第${i + 1}题:${testJson[i].userAnswer}` + '\n'
     }
   }
+  else if (props.workJson.atool === '78') {
+    const testJson = workContent.testJson || []
+    for (let i = 0; i < testJson.length; i++) {
+      question_content += `第${i + 1}题:${testJson[i].teststitle}\n 选项:${testJson[i].checkList}`
+      student_answer += `第${i + 1}题:${testJson[i].userAnswer}` + '\n'
+    }
+  }
   else if (props.workJson.atool === '15') {
     question_content = workContent.answerQ || ''
     correct_answer = workContent.evaluationCriteria || '无'
@@ -492,10 +501,18 @@ const sendAction = async (action: string) => {
      当前题目信息:
     - 题目类型:${choice}
     - 题目内容:${question_content}
-    - 正确答案:${correct_answer}(0,1,3是选项的下标,学生的答案也是)
+    - 正确答案:${correct_answer}(0,1,3是选项的下标,数组的下标,相当于第一选项是0,第二选项是1,第三选项是2,第四选项是3,学生的答案也是数组的下标)
     - 学生提交的回答:${student_answer}
     `
   }
+  else if (props.workJson.atool === '78') {
+    promptText += `
+      当前题目信息:
+      - 题目类型:${choice}
+      - 投票题目:${question_content}
+      - 学生提交的投票:${student_answer}(0,1,2是选项的下标,数组的下标,相当于第一选项是0,第二选项是1,第三选项是2)
+    `
+  }
   else if (props.workJson.atool === '15') {
     promptText += `
       当前题目信息:

+ 8 - 3
src/views/Student/components/answerTheResult.vue

@@ -16,6 +16,11 @@
 				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
 			</div>
 
+			<div class="atr_d_msg" v-if="workDetail && workDetail.type === '78'">
+				<div>{{ lang.ssAccuracy }}</div>
+				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
+			</div>
+
 			<div class="atr_d_msg" v-if="choiceQuestionAnswer">
 				<div>{{ lang.ssCorrectAns }}</div>
 				<span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
@@ -74,7 +79,7 @@
 			class="atr_type45Area"
 			v-if="
 				workDetail &&
-				workDetail.type === '45' &&
+				(workDetail.type === '45' || workDetail.type === '78') &&
 				choiceQuestionListData[workIndex] &&
 				choiceQuestionListData[workIndex].choiceUser
 			"
@@ -244,7 +249,7 @@ const getWorkDetail = async () => {
 // 选择题题目数组
 const choiceQuestionList = () => {
   let _result: any[] = []
-  if (workDetail.value && workDetail.value.type === '45') {
+  if (workDetail.value && (workDetail.value.type === '45' || workDetail.value.type === '78')) {
     const _workData = workDetail.value.json.testJson
     _workData.forEach((item: any, index: number) => {
       // 修复 props.workArray 可能为 null 的问题
@@ -400,7 +405,7 @@ const changeShow = (op: any, idx: number) => {
 
 const lookDetail = () => {
   console.log(props.toolType)
-  if ([45, 15, 72, 73].includes(props.toolType)) {
+  if ([45, 15, 72, 73, 78, 79].includes(props.toolType)) {
     emit('openChoiceQuestionDetail', props.slideIndex)
   }
 }

Fichier diff supprimé car celui-ci est trop grand
+ 627 - 104
src/views/Student/components/choiceQuestionDetailDialog.vue


+ 4 - 1
src/views/Student/components/selectUserDialog.vue

@@ -55,7 +55,7 @@ defineExpose({
 })
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .modalArea{
   &>.modal-content {
     border-radius: 1rem;
@@ -101,6 +101,9 @@ defineExpose({
   display: flex;
   gap: .8rem;
   flex-wrap: wrap;
+
+  max-height: calc(100vh - 8.6rem);
+  overflow: auto;
   &>div{
     padding: .25rem .5rem;
     font-weight: 500;

Fichier diff supprimé car celui-ci est trop grand
+ 529 - 133
src/views/Student/index.vue


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff