#1 for-test

クローズ
lihongjun jack/beta から jack/feat/english-speaking-for-merge への 193 コミットのマージを希望しています
100 ファイル変更28228 行追加383 行削除
  1. 21 0
      .dockerignore
  2. 6 0
      .env
  3. 8 0
      .env.example
  4. 5 0
      .gitignore
  5. 6 0
      .trae/rules/git-commit-message.md
  6. 49 0
      Dockerfile
  7. 54 0
      README.md
  8. 54 0
      README_zh.md
  9. 11 0
      config.js.template
  10. 15 0
      env.d.ts
  11. 2 0
      index.html
  12. 36 0
      nginx.conf
  13. 11967 0
      package-lock.json
  14. 16 1
      package.json
  15. 7 0
      public/config.js
  16. 26 0
      public/pcm-recorder-worklet.js
  17. 3524 0
      public/webcodecs-worker.js
  18. 250 0
      scripts/test-article-auto-format-flow.mjs
  19. 70 0
      scripts/test-article-auto-format.mjs
  20. 238 0
      scripts/test-article-class-summary-auto.mjs
  21. 106 0
      scripts/test-article-errors.mjs
  22. 26 0
      scripts/test-article-reading-data.mjs
  23. 900 0
      scripts/test-article-reading-engine.mjs
  24. 79 0
      scripts/test-article-reading-i18n.mjs
  25. 52 0
      scripts/test-article-reading-model.mjs
  26. 137 0
      scripts/test-article-reading-routing.mjs
  27. 114 0
      scripts/test-article-reading-stream.mjs
  28. 116 0
      scripts/test-article-recorder-sample-rate.mjs
  29. 126 0
      scripts/test-dialogue-stream-fallback.mjs
  30. 79 0
      scripts/test-speaking-api-config.mjs
  31. 28 13
      src/App.vue
  32. BIN
      src/assets/img/kwlBanner.jpg
  33. BIN
      src/assets/img/tool_file.png
  34. BIN
      src/assets/img/tool_photo.png
  35. BIN
      src/assets/img/tool_vote.png
  36. 5 2
      src/assets/styles/prosemirror.scss
  37. 909 0
      src/components/CollapsibleToolbar/cocolearnCom/KwlDetail.vue
  38. 752 149
      src/components/CollapsibleToolbar/componets/aiChat.vue
  39. 181 0
      src/components/CollapsibleToolbar/componets/aiWeb.vue
  40. 11 2
      src/components/CollapsibleToolbar/index.vue
  41. 808 52
      src/components/CollapsibleToolbar/index2.vue
  42. 1656 0
      src/components/CollapsibleToolbar/index22.vue
  43. 1 0
      src/components/ColorButton.vue
  44. 143 0
      src/components/Contextmenu2/MenuContent.vue
  45. 80 0
      src/components/Contextmenu2/index.vue
  46. 14 0
      src/components/Contextmenu2/types.ts
  47. 42 23
      src/components/CreateCourseDialog.vue
  48. 27 2
      src/components/FileInput.vue
  49. 1 0
      src/components/Select.vue
  50. 6 4
      src/components/Switch.vue
  51. 38 0
      src/components/TextColorButton2.vue
  52. 17 0
      src/configs/englishSpeakingTools.ts
  53. 1 0
      src/configs/hotkey.ts
  54. 10 0
      src/hooks/useAddSlidesOrElements.ts
  55. 1 0
      src/hooks/useCopyAndPasteElement.ts
  56. 2 1
      src/hooks/useCreateElement.ts
  57. 12 1
      src/hooks/useGlobalHotkey.ts
  58. 103 39
      src/hooks/useImport.ts
  59. 46 1
      src/hooks/usePasteEvent.ts
  60. 21 0
      src/hooks/useSlideHandler.ts
  61. 1 0
      src/i18n/lang.ts
  62. 64 0
      src/plugins/directive/contextmenu2.ts
  63. 2 0
      src/plugins/directive/index.ts
  64. 4 0
      src/plugins/icon.ts
  65. 1 1
      src/services/config.ts
  66. 39 3
      src/services/course.ts
  67. 111 0
      src/services/speaking.ts
  68. 60 0
      src/store/articleReading.ts
  69. 29 1
      src/store/main.ts
  70. 11 0
      src/store/slides.ts
  71. 168 0
      src/store/speaking.ts
  72. 5 4
      src/tools/aiChat.ts
  73. 112 0
      src/types/articleReading.ts
  74. 348 0
      src/types/englishSpeaking.ts
  75. 15 1
      src/types/injectKey.ts
  76. 3 1
      src/types/slides.ts
  77. 22 0
      src/utils/clipboard.ts
  78. 162 0
      src/utils/components/messageInstruction.vue
  79. 4 0
      src/utils/confirmDialog.ts
  80. 137 18
      src/utils/prosemirror/schema/marks.ts
  81. 111 52
      src/utils/prosemirror/schema/nodes.ts
  82. 479 0
      src/views/Editor/Canvas/ChildPageDisplay.vue
  83. 11 2
      src/views/Editor/Canvas/EditableElement.vue
  84. 10 1
      src/views/Editor/Canvas/Operate/index.vue
  85. 15 0
      src/views/Editor/Canvas/index.vue
  86. 10 3
      src/views/Editor/CanvasTool/WebpageInput.vue
  87. 354 6
      src/views/Editor/CanvasTool/index2.vue
  88. 186 0
      src/views/Editor/EnglishSpeaking/SpeakingPanel.vue
  89. 239 0
      src/views/Editor/EnglishSpeaking/components/AIGenerationForm.vue
  90. 346 0
      src/views/Editor/EnglishSpeaking/components/ArticleFormatDialog.vue
  91. 123 0
      src/views/Editor/EnglishSpeaking/components/CreationModeSwitch.vue
  92. 116 0
      src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue
  93. 145 0
      src/views/Editor/EnglishSpeaking/components/ExerciseTypeGrid.vue
  94. 136 0
      src/views/Editor/EnglishSpeaking/components/RecommendCard.vue
  95. 62 0
      src/views/Editor/EnglishSpeaking/components/UnitInfoCard.vue
  96. 45 0
      src/views/Editor/EnglishSpeaking/composables/articleAutoFormatRules.ts
  97. 117 0
      src/views/Editor/EnglishSpeaking/composables/useArticleAutoFormat.ts
  98. 827 0
      src/views/Editor/EnglishSpeaking/composables/useArticleReadingEngine.ts
  99. 215 0
      src/views/Editor/EnglishSpeaking/composables/useAudioPlayer.ts
  100. 368 0
      src/views/Editor/EnglishSpeaking/composables/useAudioRecorder.ts

+ 21 - 0
.dockerignore

@@ -0,0 +1,21 @@
+node_modules
+dist
+.git
+.github
+.husky
+.idea
+.vscode
+.agent
+.claude
+.superpowers
+openspec
+doc
+scripts
+*.tsbuildinfo
+vite.config.ts.timestamp-*
+
+# 不把本机 .env 烧进镜像。需要构建期变量时用 --build-arg 显式传入,
+# 见 Dockerfile 与 API 仓库的 DEPLOY.md。
+.env
+.env.*
+!.env.example

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

+ 6 - 0
.trae/rules/git-commit-message.md

@@ -0,0 +1,6 @@
+---
+alwaysApply: true
+scene: git_message
+---
+
+在此处编写规则,自定义 AI 生成提交信息的风格。要中文,不能是繁体中文。

+ 49 - 0
Dockerfile

@@ -0,0 +1,49 @@
+# 本文件不指定平台。目标架构由构建命令决定:
+#   docker build --platform linux/arm64 ...
+# 交付构建请走 API 仓库的 scripts/build-release.sh,它统一了两个镜像的平台。
+
+# ---------- build ----------
+FROM node:20-alpine AS build
+
+WORKDIR /app
+
+# 镜像里没有 .git,husky install 会失败;HUSKY=0 让它跳过。
+ENV HUSKY=0
+
+COPY package.json package-lock.json ./
+RUN npm ci
+
+COPY . .
+
+# 这里刻意没有任何 ARG:所有环境相关的配置都走运行时注入(见文件末尾的 ENV)。
+# 镜像因此与具体环境无关,同一份产物可以在任何环境跑,换配置只需重启容器。
+
+# 用 build-only 而非 build:跳过 vue-tsc 类型检查,
+# 避免既有类型问题阻塞交付镜像的构建。类型检查请在 CI / 本地跑。
+RUN npm run build-only
+
+# ---------- runtime ----------
+FROM nginx:alpine
+
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# nginx 官方镜像自带的 docker-entrypoint 会在启动时对
+# /etc/nginx/templates/*.template 做 envsubst,输出到 NGINX_ENVSUBST_OUTPUT_DIR。
+# 这里把输出目录指向静态根,于是生成 /usr/share/nginx/html/config.js,
+# 覆盖构建产物里那份空的占位 config.js。不需要自定义 entrypoint。
+COPY config.js.template /etc/nginx/templates/config.js.template
+ENV NGINX_ENVSUBST_OUTPUT_DIR=/usr/share/nginx/html
+
+# 运行时可配置项。必须给默认值:envsubst 只替换"已定义"的变量,
+# 未定义时模板里的 ${XXX} 会被原样保留,变成一个非空的垃圾字符串,
+# 反而绕过了代码里的兜底逻辑。
+#
+# AZURE_SPEECH_* 供前端 TTS(speechService.ts)使用。放在这里而非构建期 ARG,
+# 是为了让镜像与环境解耦 —— 但要清楚:它最终会写进浏览器可读的 /config.js,
+# 并不比构建期烧入更安全,只是更灵活。详见 DEPLOY.md「已知问题」。
+ENV SPEAKING_API_HOST="" \
+    AZURE_SPEECH_KEY="" \
+    AZURE_SPEECH_REGION=""
+
+EXPOSE 80

+ 54 - 0
README.md

@@ -41,6 +41,60 @@ Browser access: http://127.0.0.1:5173/
 > Note: If you deploy this project on your own server and find that it fails to initialize, it's because the initialization data is stored in the author's private object storage and is not publicly accessible. You'll need to transfer the data to your own server, object storage service, database, or front-end local storage.
 
 
+# 🐳 Docker Deployment
+
+Packaged as a static nginx image. **The image contains no environment-specific values and no credentials** — all configuration is injected at container startup, so the same image runs in any environment and changing configuration never requires a rebuild.
+
+### Build
+
+```bash
+docker build --platform linux/arm64 -t pptist:2.0.0 .
+```
+
+`--platform` determines the target architecture. Check with `uname -m` first: `aarch64` → `linux/arm64`, `x86_64` → `linux/amd64`. A mismatch leaves the operator with nothing but `exec format error` after `docker load`.
+
+### Run
+
+```bash
+docker run -d --name pptist -p 8080:80 \
+  -e SPEAKING_API_HOST=https://your-api-host \
+  -e AZURE_SPEECH_KEY=xxxxxxxx \
+  -e AZURE_SPEECH_REGION=eastasia \
+  -e TZ=Asia/Shanghai \
+  pptist:2.0.0
+```
+
+### Configuration
+
+| Variable | Required | Description |
+| --- | :---: | --- |
+| `SPEAKING_API_HOST` | ✅ | Backend API address. **Must be reachable from the browser** (public domain or `http://server-ip:8000`) — a container name will not work. If omitted, falls back to the domain hardcoded in the source |
+| `AZURE_SPEECH_KEY` | ✅ | Azure Speech key, used by the frontend for TTS playback. Without it, playback throws |
+| `AZURE_SPEECH_REGION` | ✅ | Azure region, e.g. `eastasia` |
+| `TZ` | | Timezone. Defaults to UTC, affects nginx log timestamps only |
+
+> ⚠️ `AZURE_SPEECH_KEY` is written into `/config.js`, which is readable by anyone in the browser. This is inherited from the existing design (the frontend calls Azure directly); moving to runtime injection improved flexibility, not security.
+
+### Verify
+
+```bash
+curl http://localhost:8080/config.js
+```
+
+All three values should be non-empty. An empty value means that environment variable was not passed in; the output carries an explanatory comment.
+
+### Changing configuration
+
+Re-run the container with new `-e` flags — **no rebuild required**:
+
+```bash
+docker rm -f pptist
+docker run -d --name pptist -p 8080:80 -e SPEAKING_API_HOST=... pptist:2.0.0
+```
+
+How it works: on startup nginx renders `config.js.template` into `/config.js` via `envsubst`, and the page loads it synchronously before the app bundle. The lookup order in code is `window.__APP_CONFIG__` > `import.meta.env.VITE_*` > hardcoded fallback, so local `npm run dev` and the existing non-container build behave exactly as before.
+
+
 # 📚 Features
 ### Basic Features
 - History (undo, redo)

+ 54 - 0
README_zh.md

@@ -42,6 +42,60 @@ npm run dev
 > 注意:如果你将本项目部署在自己的服务器上,发现无法初始化成功,那是因为初始化的数据是放在作者私人对象存储中的,不对外开放,你需要把数据转移到自己的服务器/对象存储服务/数据库/前端本地
 
 
+# 🐳 Docker 部署
+
+打包成 nginx 静态镜像。**镜像不含任何环境相关的值和密钥**,所有配置在容器启动时注入,因此同一份镜像可以跑在任何环境,改配置不需要重新构建。
+
+### 构建
+
+```bash
+docker build --platform linux/arm64 -t pptist:2.0.0 .
+```
+
+`--platform` 决定目标机器架构,先用 `uname -m` 确认:`aarch64` → `linux/arm64`,`x86_64` → `linux/amd64`。架构不匹配的话,对方 `docker load` 之后只会得到一句 `exec format error`。
+
+### 运行
+
+```bash
+docker run -d --name pptist -p 8080:80 \
+  -e SPEAKING_API_HOST=https://your-api-host \
+  -e AZURE_SPEECH_KEY=xxxxxxxx \
+  -e AZURE_SPEECH_REGION=eastasia \
+  -e TZ=Asia/Shanghai \
+  pptist:2.0.0
+```
+
+### 配置项
+
+| 变量 | 必填 | 说明 |
+| --- | :---: | --- |
+| `SPEAKING_API_HOST` | ✅ | 后端 API 地址。**必须是浏览器能访问到的地址**(公网域名或 `http://服务器IP:8000`),不能填容器名。不传会回落到代码内置的默认域名 |
+| `AZURE_SPEECH_KEY` | ✅ | Azure Speech 密钥,前端 TTS 朗读用。不传则朗读功能直接报错 |
+| `AZURE_SPEECH_REGION` | ✅ | Azure 区域,如 `eastasia` |
+| `TZ` | | 时区。不传则容器为 UTC,只影响 nginx 日志时间 |
+
+> ⚠️ `AZURE_SPEECH_KEY` 会写进浏览器可读的 `/config.js`,任何人都能取走。这是既有设计(前端直连 Azure)遗留的问题;改成运行时注入只解决了灵活性,没有解决这一点。
+
+### 验证
+
+```bash
+curl http://localhost:8080/config.js
+```
+
+三个值都应该非空。为空说明对应的环境变量没传进去,输出里自带说明文字。
+
+### 改配置
+
+用新的 `-e` 参数重跑容器即可,**不需要重新构建镜像**:
+
+```bash
+docker rm -f pptist
+docker run -d --name pptist -p 8080:80 -e SPEAKING_API_HOST=... pptist:2.0.0
+```
+
+原理:nginx 启动时用 `envsubst` 把 `config.js.template` 渲染成 `/config.js`,页面在 app bundle 之前同步加载它。代码的读取优先级是 `window.__APP_CONFIG__` > `import.meta.env.VITE_*` > 硬编码兜底,所以本地 `npm run dev` 和原有的构建部署方式行为完全不变。
+
+
 # 📚 功能列表
 ### 基础功能
 - 历史记录(撤销、重做)

+ 11 - 0
config.js.template

@@ -0,0 +1,11 @@
+// 运行时配置。本文件由 nginx 启动时用 envsubst 从 config.js.template 生成,
+// 覆盖构建产物里那份空的占位 config.js。
+//
+// 下列值为空 = 未配置对应的环境变量,前端会回落到镜像内置的默认值
+// (API 地址会指向 cocorobo 的生产域名,多半不是你想要的)。
+// 容器部署请用 -e 提供:SPEAKING_API_HOST / AZURE_SPEECH_KEY / AZURE_SPEECH_REGION
+window.__APP_CONFIG__ = {
+  SPEAKING_API_HOST: "${SPEAKING_API_HOST}",
+  AZURE_SPEECH_KEY: "${AZURE_SPEECH_KEY}",
+  AZURE_SPEECH_REGION: "${AZURE_SPEECH_REGION}",
+}

+ 15 - 0
env.d.ts

@@ -1,2 +1,17 @@
 // eslint-disable-next-line spaced-comment
 /// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_SPEAKING_API_HOST?: string
+}
+
+// 容器部署时由 nginx 启动脚本生成 /config.js 注入;dev 与普通构建下是空对象。
+interface AppRuntimeConfig {
+  readonly SPEAKING_API_HOST?: string
+  readonly AZURE_SPEECH_KEY?: string
+  readonly AZURE_SPEECH_REGION?: string
+}
+
+interface Window {
+  __APP_CONFIG__?: AppRuntimeConfig
+}

+ 2 - 0
index.html

@@ -3,6 +3,8 @@
   <head>
     <meta charset="UTF-8">
     <link rel="icon" href="/favicon.ico">
+    <!-- 运行时配置。必须在 /src/main.ts 之前执行;同步 script 先于 defer 的 module script 运行。 -->
+    <script src="/config.js"></script>
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
     <meta name="renderer" content="webkit">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">

+ 36 - 0
nginx.conf

@@ -0,0 +1,36 @@
+server {
+    listen 80;
+    server_name _;
+
+    root /usr/share/nginx/html;
+    index index.html;
+
+    gzip on;
+    gzip_types text/plain text/css application/javascript application/json image/svg+xml;
+    gzip_min_length 1024;
+
+    # 运行时配置:绝不能被缓存,否则运维改了环境变量重启,
+    # 浏览器仍在用旧的 API 地址,且极难排查。
+    # 不要在这里再加 expires 指令:它会额外发一个 Cache-Control 头,
+    # 与下面的 add_header 重复。no-store 已经足够。
+    location = /config.js {
+        add_header Cache-Control "no-store" always;
+    }
+
+    # index.html 必须每次回源校验(通常是一个很便宜的 304)。
+    # 它是唯一指向 /assets/ 那些 hash 文件的地方 —— 一旦它被缓存,浏览器就会继续
+    # 请求旧的 asset URL,而那些 URL 是 immutable 的,用户会被钉死在旧版本且无法自愈。
+    location = /index.html {
+        add_header Cache-Control "no-cache" always;
+    }
+
+    # 构建产物文件名带内容 hash(改图片不改文件名,产物名一样会变),可永久缓存。
+    # 不要用 expires 指令:它会额外发一个 Cache-Control 头,与 add_header 重复。
+    location /assets/ {
+        add_header Cache-Control "public, max-age=31536000, immutable" always;
+    }
+
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+}

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

+ 16 - 1
package.json

@@ -7,10 +7,23 @@
     "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",
-    "prepare": "husky install"
+    "prepare": "husky install",
+    "test:article-reading-data": "node scripts/test-article-reading-data.mjs",
+    "test:article-reading-model": "node scripts/test-article-reading-model.mjs",
+    "test:article-reading-stream": "node scripts/test-article-reading-stream.mjs",
+    "test:article-reading-engine": "node scripts/test-article-reading-engine.mjs",
+    "test:article-recorder-sample-rate": "node scripts/test-article-recorder-sample-rate.mjs",
+    "test:article-reading-routing": "node scripts/test-article-reading-routing.mjs",
+    "test:article-reading-i18n": "node scripts/test-article-reading-i18n.mjs",
+    "test:article-auto-format": "node scripts/test-article-auto-format.mjs",
+    "test:article-auto-format-flow": "node scripts/test-article-auto-format-flow.mjs",
+    "test:article-errors": "node scripts/test-article-errors.mjs",
+    "test:article-class-summary-auto": "node scripts/test-article-class-summary-auto.mjs"
   },
   "author": "pipipi_pikachu@163.com",
   "homepage": "https://github.com/pipipi-pikachu/PPTist",
@@ -28,6 +41,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 +102,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"
   }
 }

+ 7 - 0
public/config.js

@@ -0,0 +1,7 @@
+// 运行时配置占位文件。保持空对象。
+//
+// 本地 dev / 现有的构建部署:走本文件,配置继续由 .env 的 VITE_* 变量提供。
+// 容器部署:nginx 启动时用 config.js.template + envsubst 生成同名文件覆盖它。
+//
+// 优先级说明见 speakingApiConfig.ts。
+window.__APP_CONFIG__ = {}

+ 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

+ 250 - 0
scripts/test-article-auto-format-flow.mjs

@@ -0,0 +1,250 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+const root = new URL('../', import.meta.url)
+const read = p => readFile(new URL(p, root), 'utf8')
+const transpile = source => ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+
+const base = 'src/views/Editor/EnglishSpeaking/composables/'
+
+// 纯规则模块先转译成 data URL,供 composable 原样 import(相对路径在 data: 里无法解析)
+const rulesUrl = `data:text/javascript,${encodeURIComponent(
+  transpile(await read(`${base}articleAutoFormatRules.ts`)),
+)}`
+
+let source = await read(`${base}useArticleAutoFormat.ts`)
+source = source
+  .replace(
+    "import { onScopeDispose, ref, type Ref } from 'vue'",
+    // onScopeDispose 桩:把清理回调存到 globalThis,供测试手动模拟"组件已卸载"
+    'const ref = value => ({ value }); const onScopeDispose = fn => { globalThis.__onDispose = fn }',
+  )
+  .replace(
+    "import { lang } from '@/main'",
+    "const lang = { ssArticleSegmentFailed: 'segment-failed' }",
+  )
+  .replace(
+    "import message from '@/utils/message'",
+    'const message = { error: text => globalThis.__errors.push(text) }',
+  )
+  .replace(
+    "import { segmentArticle } from '../services/articleReading'",
+    'const segmentArticle = (...args) => globalThis.__segment(...args)',
+  )
+  // 双引号做外层定界符:rulesUrl 是 encodeURIComponent 输出,单引号/反引号在其字符集里不转义,
+  // 若仍用单引号包裹会被规则文件注释里的反引号/`.join(' ')` 里的单引号提前截断字符串。
+  .replace("from './articleAutoFormatRules'", `from "${rulesUrl}"`)
+
+const { useArticleAutoFormat } = await import(
+  `data:text/javascript,${encodeURIComponent(transpile(source))}`
+)
+
+const words = n => Array.from({ length: n }, (_, i) => `w${i}`).join(' ')
+
+function setup({ segment } = {}) {
+  globalThis.__errors = []
+  globalThis.__segment = segment ?? (async () => ({ content: 'P1.\n\nP2.' }))
+  const content = { value: '' }
+  return { content, api: useArticleAutoFormat(content), errors: globalThis.__errors }
+}
+
+// 失败用例是故意的,composable 会如实 console.error。但它的栈里带着整个 data: URL
+// (被转译进去的源码),一条就是好几 KB,会把真正的测试输出淹掉。捕获掉,让通过时的
+// 输出保持干净;断言仍然验证 message.error 收到了什么。
+async function withSilencedConsoleError(fn) {
+  const original = console.error
+  console.error = () => {}
+  try {
+    await fn()
+  }
+  finally {
+    console.error = original
+  }
+}
+
+// ---------------- 粘贴触发 ----------------
+{
+  const { content, api } = setup()
+  api.onContentInput(words(60))
+  assert.equal(content.value, words(60), '内容恒写入(取消时保留原始粘贴)')
+  assert.equal(api.dialogVisible.value, true)
+  assert.equal(api.dialogMode.value, 'paste')
+}
+{
+  const { api } = setup()
+  api.onContentInput(words(40))
+  assert.equal(api.dialogVisible.value, false, '短文不弹')
+}
+{
+  // 已有长文再敲一个字符 —— 绝不能弹(prev 取自 content.value,不是陈旧变量)
+  const { content, api } = setup()
+  content.value = words(300)
+  api.onContentInput(`${words(300)} x`)
+  assert.equal(api.dialogVisible.value, false)
+}
+
+// ---------------- 粘贴 → 取消 → 内容不变(spec §3.2 step1「取消 → 保留原始粘贴」)----------------
+{
+  const { content, api } = setup()
+  api.onContentInput(words(60))
+  assert.equal(api.dialogVisible.value, true)
+  api.cancelDialog()
+  assert.equal(api.dialogVisible.value, false, '取消关框')
+  assert.equal(content.value, words(60), '取消不清空/不改写老师刚粘贴的内容')
+}
+
+// ---------------- 按钮触发 ----------------
+{
+  const { content, api } = setup()
+  content.value = words(80)
+  api.openOverwrite()
+  assert.equal(api.dialogMode.value, 'overwrite')
+  assert.equal(api.dialogVisible.value, true)
+}
+{
+  const { api } = setup() // 空文本
+  api.openOverwrite()
+  assert.equal(api.dialogVisible.value, false, '空文本不开框')
+}
+
+// ---------------- 应用配置守卫 ----------------
+{
+  const { content, api } = setup()
+  content.value = 'A one.\nB   two.\n\n\nC three.'
+  assert.equal(api.guardBeforeApply(), true, '无超长段 → 放行')
+  assert.equal(content.value, 'A one.\n\nB two.\n\nC three.', '归一化回写 textarea')
+  assert.equal(api.dialogVisible.value, false)
+}
+{
+  // 拦截路径也要归一化回写(脏空白不能留在 textarea)——通行路径已断言,这里补上拦截路径
+  const { content, api } = setup()
+  const messyOverlong = words(301).replace(/ /g, '   ') // 词间三倍空格
+  content.value = `A   one.\n\n\n\n${messyOverlong}`
+  assert.equal(api.guardBeforeApply(), false, '超长段 → 拦截')
+  assert.equal(content.value, `A one.\n\n${words(301)}`, '拦截也归一化回写')
+  assert.equal(api.dialogMode.value, 'guard')
+  assert.equal(api.dialogVisible.value, true)
+}
+{
+  // 请求进行中点「应用配置」:guardBeforeApply 必须早退,程序化调用也绕不过去(不止按钮 disabled)
+  let release
+  const pending = new Promise(resolve => { release = resolve })
+  const { content, api } = setup({
+    segment: async () => { await pending; return { content: 'Done.' } },
+  })
+  content.value = words(60)
+  api.openOverwrite()
+  const confirming = api.confirmDialog() // isFormatting → true
+  assert.equal(api.guardBeforeApply(), false, '格式化进行中,guardBeforeApply 必须早退')
+  assert.equal(content.value, words(60), '早退不应触碰内容(不归一化、不误判超长)')
+  assert.equal(api.dialogMode.value, 'overwrite', 'dialogMode 不应被早退的 guardBeforeApply 改成 guard')
+  release()
+  await confirming
+}
+
+// ---------------- 确认键:文本清空后点确认 ----------------
+{
+  // 可达路径(非 UI-unreachable 早退):老师清空文本框再点确认,弹框不能卡住
+  const { content, api } = setup()
+  content.value = words(60)
+  api.openOverwrite()
+  content.value = '   '
+  await api.confirmDialog()
+  assert.equal(api.dialogVisible.value, false, '空文本点确认 —— 关框而非静默无反应')
+  assert.equal(api.isFormatting.value, false)
+}
+
+// ---------------- 分段成功 ----------------
+{
+  const calls = []
+  const { content, api } = setup({
+    segment: async text => { calls.push(text); return { content: 'One.\n\n\nTwo.' } },
+  })
+  content.value = words(60)
+  api.openOverwrite()
+  await api.confirmDialog()
+  assert.deepEqual(calls, [words(60)], '整篇送后端')
+  assert.equal(content.value, 'One.\n\nTwo.', '回填并归一化')
+  assert.equal(api.dialogVisible.value, false)
+  assert.equal(api.isFormatting.value, false)
+}
+
+// ---------------- 分段失败:非守卫框关闭、守卫框保持打开 ----------------
+{
+  const { content, api, errors } = setup({ segment: async () => { throw new Error('down') } })
+  content.value = words(60)
+  api.openOverwrite()
+  await withSilencedConsoleError(() => api.confirmDialog())
+  assert.equal(content.value, words(60), '失败保留原文本')
+  assert.equal(api.dialogVisible.value, false)
+  assert.deepEqual(errors, ['segment-failed'])
+}
+{
+  const { content, api } = setup({ segment: async () => { throw new Error('down') } })
+  content.value = words(400)
+  api.guardBeforeApply()
+  await withSilencedConsoleError(() => api.confirmDialog())
+  assert.equal(api.dialogVisible.value, true, '守卫框失败后保持打开,可原地重试')
+  assert.equal(api.isFormatting.value, false)
+  assert.equal(content.value, words(400), '守卫框失败同样保留内容(非守卫失败已断言,这里补上守卫路径)')
+}
+
+// ---------------- 防重复点击 ----------------
+{
+  let calls = 0
+  let release
+  const pending = new Promise(resolve => { release = resolve })
+  const { content, api } = setup({
+    segment: async () => { calls += 1; await pending; return { content: 'Done.' } },
+  })
+  content.value = words(60)
+  api.openOverwrite()
+  const first = api.confirmDialog()
+  await api.confirmDialog()          // 进行中再点 —— 必须早退
+  assert.equal(calls, 1)
+  api.cancelDialog()                  // 进行中取消 —— 必须无效
+  assert.equal(api.dialogVisible.value, true)
+  release()
+  await first
+  assert.equal(calls, 1)
+  assert.equal(api.dialogVisible.value, false)
+}
+
+// ---------------- 陈旧性守卫:等待期间内容被改,响应作废不回写 ----------------
+{
+  let release
+  const pending = new Promise(resolve => { release = resolve })
+  const { content, api } = setup({
+    segment: async () => { await pending; return { content: 'Stale.' } },
+  })
+  content.value = words(60)
+  api.openOverwrite()
+  const confirming = api.confirmDialog()
+  content.value = 'teacher typed something newer' // 30 秒等待期间老师又改了文本
+  release()
+  await confirming
+  assert.equal(content.value, 'teacher typed something newer', '内容中途被改 → 响应作废,绝不覆盖')
+  assert.equal(api.dialogVisible.value, false, '仍然关框,不让弹框卡住')
+}
+
+// ---------------- 组件卸载(effect scope 销毁)后到达的响应是 no-op ----------------
+{
+  let release
+  const pending = new Promise(resolve => { release = resolve })
+  const { content, api } = setup({
+    segment: async () => { await pending; return { content: 'Late.' } },
+  })
+  content.value = words(60)
+  api.openOverwrite()
+  const confirming = api.confirmDialog()
+  globalThis.__onDispose() // 模拟宿主组件在响应落地前已卸载
+  release()
+  await confirming
+  assert.equal(content.value, words(60), '销毁后到达的响应不得写 store')
+  assert.equal(api.dialogVisible.value, true, '销毁后不再触碰任何本地状态')
+}
+
+console.log('article auto-format flow OK')

+ 70 - 0
scripts/test-article-auto-format.mjs

@@ -0,0 +1,70 @@
+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/articleAutoFormatRules.ts',
+  import.meta.url,
+)
+const compiled = ts.transpileModule(await readFile(sourceUrl, 'utf8'), {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+const {
+  countWords,
+  normalizeParagraphs,
+  hasOverlongParagraph,
+  shouldPromptOnPaste,
+  MAX_PARAGRAPH_WORDS,
+  PASTE_PROMPT_MIN_WORDS,
+} = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+
+/** 生成 n 个单词的文本 */
+const words = n => Array.from({ length: n }, (_, i) => `w${i}`).join(' ')
+
+// ---------------- countWords ----------------
+assert.equal(countWords(''), 0)
+assert.equal(countWords('   \n\n  '), 0)
+assert.equal(countWords('one two   three'), 3)
+assert.equal(countWords('  leading and trailing  '), 3)
+assert.equal(countWords(words(300)), 300)
+
+// ---------------- normalizeParagraphs ----------------
+// 与后端 split_paragraphs 同规则:单 \n 与空行皆为段界
+assert.equal(normalizeParagraphs('A one.\n\nB two.'), 'A one.\n\nB two.')
+assert.equal(normalizeParagraphs('A one.\nB two.'), 'A one.\n\nB two.')
+assert.equal(normalizeParagraphs('A one.\n\n\n\n   \nB two.'), 'A one.\n\nB two.')
+assert.equal(normalizeParagraphs('  A   one    here.  '), 'A one here.')
+assert.equal(normalizeParagraphs(''), '')
+assert.equal(normalizeParagraphs('   \n\n  '), '')
+// 幂等:归一化结果再归一化不变(会回写 textarea,必须稳定)
+assert.equal(normalizeParagraphs(normalizeParagraphs('A.\nB.\n\n\nC.')), 'A.\n\nB.\n\nC.')
+
+// ---------------- hasOverlongParagraph(299 / 300 / 301 边界)----------------
+assert.equal(MAX_PARAGRAPH_WORDS, 300)
+assert.equal(hasOverlongParagraph(''), false)
+assert.equal(hasOverlongParagraph(words(299)), false)
+assert.equal(hasOverlongParagraph(words(300)), false)
+assert.equal(hasOverlongParagraph(words(301)), true)
+// 多段:只要有一段超标就 true
+assert.equal(hasOverlongParagraph(`${words(10)}\n\n${words(301)}`), true)
+assert.equal(hasOverlongParagraph(`${words(200)}\n\n${words(200)}`), false)
+// 单 \n 也是段界,故 200+200 不算一段 400
+assert.equal(hasOverlongParagraph(`${words(200)}\n${words(200)}`), false)
+
+// ---------------- shouldPromptOnPaste ----------------
+assert.equal(PASTE_PROMPT_MIN_WORDS, 50)
+// spec §3.2 step 1 只有两条:原内容 trim 后为空,且新内容 > 50 词。
+assert.equal(shouldPromptOnPaste('', words(60)), true)       // 空框粘贴长文 → 弹
+assert.equal(shouldPromptOnPaste('   ', words(51)), true)    // 纯空白算空框;51 词过阈值
+assert.equal(shouldPromptOnPaste('\n\n  \n', words(60)), true) // 只剩换行的框同样算空
+assert.equal(shouldPromptOnPaste('', words(50)), false)      // 恰好 50 词不弹(严格 >)
+assert.equal(shouldPromptOnPaste('', words(40)), false)      // 短文不弹
+assert.equal(shouldPromptOnPaste(words(300), words(400)), false) // 原文非空 → 不打断编辑
+assert.equal(shouldPromptOnPaste('x', words(400)), false)    // 哪怕只有一个字符也不算空框
+// 粘贴内容自带空行照样提示:带 \n\n ≠ 分得适合朗读(三段各 400 词就是反例)
+assert.equal(shouldPromptOnPaste('', `${words(30)}\n\n${words(30)}`), true)
+assert.equal(shouldPromptOnPaste('', `${words(400)}\n\n${words(400)}`), true)
+// enspeak 的 sentenceCount>=2 条件已弃用:无标点长文照样提示
+assert.equal(shouldPromptOnPaste('', words(60).replace(/\./g, '')), true)
+
+console.log('article auto-format rules OK')

+ 238 - 0
scripts/test-article-class-summary-auto.mjs

@@ -0,0 +1,238 @@
+/**
+ * 老师端班级总结的自动刷新(spec §8.2)
+ *
+ * 覆盖:班级状态指纹的口径;定时 tick 的四道闸门(重入 / 分页隐藏 / 无人完成 /
+ * 指纹未变);失败不记指纹于是下次 tick 重试;手动刷新同样更新指纹;计时器
+ * 间隔与卸载清理。手法承 test-article-auto-format-flow.mjs —— 转译 composable,
+ * 把 vue 与 service 换成桩。
+ */
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+const root = new URL('../', import.meta.url)
+const read = p => readFile(new URL(p, root), 'utf8')
+const transpile = source => ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+
+let source = await read(
+  'src/views/Student/components/ArticleReadingClassPanel/useArticleClassSummary.ts',
+)
+source = source
+  .replace(
+    "import { ref, watch, onMounted, onUnmounted, type Ref } from 'vue'",
+    [
+      'const ref = value => ({ value })',
+      // watch 的 immediate 由测试自己触发(__watch()),好精确控制每一次 tick
+      'const watch = (_src, cb) => { globalThis.__watch = cb }',
+      'const onMounted = fn => { globalThis.__mounted = fn }',
+      'const onUnmounted = fn => { globalThis.__unmounted = fn }',
+    ].join('; '),
+  )
+  .replace(
+    /import \{\s*listArticleSessionsByConfig,\s*generateArticleClassSummary,\s*\} from '@\/views\/Editor\/EnglishSpeaking\/services\/articleReading'/,
+    [
+      'const listArticleSessionsByConfig = (...a) => globalThis.__list(...a)',
+      'const generateArticleClassSummary = (...a) => globalThis.__summary(...a)',
+    ].join('; '),
+  )
+
+const { useArticleClassSummary, classStateKey } = await import(
+  `data:text/javascript,${encodeURIComponent(transpile(source))}`
+)
+
+const flush = () => new Promise(resolve => setTimeout(resolve, 0))
+const row = (userId, status, overallScore = null) => ({
+  userId, name: userId, status, overallScore, dims: null, sessionId: 's-' + userId, completedAt: null,
+})
+
+/** 组件挂载:驱动 watch 的 immediate 那一次,并跑 onMounted */
+function setup({ rows = [], summary } = {}) {
+  globalThis.document = { hidden: false, addEventListener() {}, removeEventListener() {} }
+  globalThis.__summaryCalls = 0
+  globalThis.__list = async () => ({ summaries: current.rows })
+  globalThis.__summary = async () => {
+    globalThis.__summaryCalls += 1
+    if (summary) return summary()
+    return { summary: 'text-' + globalThis.__summaryCalls, generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
+  }
+  const current = { rows }
+  const api = useArticleClassSummary({
+    configId: { value: 'cfg-1' },
+    studentArray: { value: [{ userid: 'u1', name: 'u1' }, { userid: 'u2', name: 'u2' }] },
+  })
+  globalThis.__mounted()
+  return { api, current, tick: async () => { globalThis.__watch(); await flush(); await flush() } }
+}
+
+function teardown() {
+  globalThis.__unmounted()
+}
+
+// ---------------- 指纹口径 ----------------
+{
+  const a = [row('u1', 'completed', 80), row('u2', 'reading')]
+  const b = [row('u2', 'reading'), row('u1', 'completed', 80)]
+  assert.equal(classStateKey('c', a), classStateKey('c', b), '与数组顺序无关')
+
+  assert.notEqual(
+    classStateKey('c', [row('u1', 'reading')]),
+    classStateKey('c', [row('u1', 'completed')]),
+    '状态变化要进指纹',
+  )
+  assert.notEqual(
+    classStateKey('c', [row('u1', 'completed', 80)]),
+    classStateKey('c', [row('u1', 'completed', 81)]),
+    '分数变化要进指纹(分数晚于 status 落地)',
+  )
+  assert.notEqual(
+    classStateKey('c', [row('u1', 'completed', null)]),
+    classStateKey('c', [row('u1', 'completed', 0)]),
+    'null 与 0 不可混同',
+  )
+  assert.notEqual(
+    classStateKey('cfg-1', a),
+    classStateKey('cfg-2', a),
+    'configId 进指纹 —— 换配置自动失效',
+  )
+}
+
+// ---------------- 闸门:全班都没开始 ----------------
+{
+  const { current, tick } = setup({ rows: [row('u1', 'not_started'), row('u2', 'not_started')] })
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 0, '一人都没开始 → 不生成(只能写「未开始 N 人」)')
+
+  current.rows = [row('u1', 'reading'), row('u2', 'not_started')]
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 1, '有人开始朗读就生成,不必等到有人完成')
+
+  current.rows = [row('u1', 'completed', 70), row('u2', 'not_started')]
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 2, '同一人从进行中变完成 → 再刷新一次')
+  teardown()
+}
+
+// ---------------- 闸门:指纹未变 ----------------
+{
+  const { api, current, tick } = setup({ rows: [row('u1', 'completed', 70)] })
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 1)
+  assert.equal(api.aiSummary.value, 'text-1')
+
+  await tick()
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 1, '班级状态没变,定时 tick 不烧 LLM')
+  assert.equal(api.aiSummary.value, 'text-1', '措辞不会无故换掉')
+
+  current.rows = [row('u1', 'completed', 70), row('u2', 'completed', 90)]
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 2, '有新完成者就刷新')
+  teardown()
+}
+
+// 分数晚于 status 落地:同一批人、只有分数补上,也算新东西
+{
+  const { current, tick } = setup({ rows: [row('u1', 'completed', null)] })
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 1)
+  current.rows = [row('u1', 'completed', 76)]
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 2, '分数回填后重算')
+  teardown()
+}
+
+// ---------------- 失败不记指纹 → 下次 tick 自动重试 ----------------
+{
+  let attempt = 0
+  const { api, tick } = setup({
+    rows: [row('u1', 'completed', 70)],
+    summary: () => {
+      attempt += 1
+      if (attempt === 1) throw new Error('boom')
+      return { summary: 'recovered', generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
+    },
+  })
+  const originalError = console.error
+  console.error = () => {}
+  try {
+    await tick()
+    assert.equal(api.aiError.value, 'SUMMARY_FAILED')
+    assert.equal(api.aiSummary.value, '', '从未成功 → 正文仍为空,由弹框显示错误块')
+    await tick()
+  }
+  finally {
+    console.error = originalError
+  }
+  assert.equal(globalThis.__summaryCalls, 2, '失败不记指纹,下次 tick 重试')
+  assert.equal(api.aiSummary.value, 'recovered')
+  assert.equal(api.aiError.value, null)
+  teardown()
+}
+
+// ---------------- 手动刷新也更新指纹 ----------------
+{
+  const { api, tick } = setup({ rows: [row('u1', 'completed', 70)] })
+  await api.fetchClassSummary()
+  await api.refreshAISummary()
+  assert.equal(globalThis.__summaryCalls, 1, '手动点按钮照常生成')
+  await tick()
+  assert.equal(globalThis.__summaryCalls, 1, '紧接着的 tick 是 no-op,不必重置计时器')
+  teardown()
+}
+
+// ---------------- 分页隐藏时跳过,切回来立刻补 ----------------
+{
+  const listeners = {}
+  globalThis.document = {
+    hidden: true,
+    addEventListener: (type, fn) => { listeners[type] = fn },
+    removeEventListener: (type) => { delete listeners[type] },
+  }
+  globalThis.__summaryCalls = 0
+  globalThis.__list = async () => ({ summaries: [row('u1', 'completed', 70)] })
+  globalThis.__summary = async () => {
+    globalThis.__summaryCalls += 1
+    return { summary: 'x', generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
+  }
+  useArticleClassSummary({
+    configId: { value: 'cfg-1' },
+    studentArray: { value: [{ userid: 'u1', name: 'u1' }] },
+  })
+  globalThis.__mounted()
+  globalThis.__watch()
+  await flush(); await flush()
+  assert.equal(globalThis.__summaryCalls, 0, '老师切走的分页不烧 LLM')
+
+  globalThis.document.hidden = false
+  listeners.visibilitychange()
+  await flush(); await flush()
+  assert.equal(globalThis.__summaryCalls, 1, '切回来立刻补一次,不必再等 60 秒')
+
+  globalThis.__unmounted()
+  assert.equal(listeners.visibilitychange, undefined, '卸载摘掉监听')
+}
+
+// ---------------- 计时器:间隔与清理 ----------------
+{
+  const originalSet = globalThis.setInterval
+  const originalClear = globalThis.clearInterval
+  const created = []
+  const cleared = []
+  globalThis.setInterval = (_fn, ms) => { created.push(ms); return { id: created.length } }
+  globalThis.clearInterval = handle => { cleared.push(handle) }
+  try {
+    const { api } = setup({ rows: [] })
+    assert.deepEqual(created, [60_000], '定时间隔 60 秒')
+    assert.ok(api.scheduleRefetch, 'WS 去抖刷新仍然对外暴露')
+    teardown()
+    assert.deepEqual(cleared, [{ id: 1 }], '卸载清掉定时器')
+  }
+  finally {
+    globalThis.setInterval = originalSet
+    globalThis.clearInterval = originalClear
+  }
+}
+
+console.log('✓ test-article-class-summary-auto: all assertions passed')

+ 106 - 0
scripts/test-article-errors.mjs

@@ -0,0 +1,106 @@
+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/articleErrors.ts', import.meta.url)
+let source = await readFile(sourceUrl, 'utf8')
+
+// `lang` 是 PPT 运行时的响应式对象;测试里换成 identity 桩,直接断言「返回的是哪个 key」,
+// 而不是断言译文 —— 译文归 test-article-reading-i18n.mjs 管,这里只管映射对不对。
+source = source
+  // 桩返回 key 本身,于是断言的是「映射到哪个 key」而不是译文。带占位符的那条例外:
+  // 它多挂一个 `{n}`,好让「段号有没有被填进去」也能被断言到。
+  .replace(
+    "import { lang } from '@/main'",
+    "const lang = new Proxy({}, { get: (_, k) => k === 'ssArticleErrDemoAudio' ? 'ssArticleErrDemoAudio:{n}' : String(k) })",
+  )
+  .replace(/import \{[^}]*\} from '\.\/articleReading'/, 'class ArticleApiError extends Error { constructor(m, s) { super(m); this.status = s } }')
+
+const compiled = ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+const { friendlyArticleError, articleActiveCountFromError } = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+
+const cases = [
+  ['Config not found', 'ssArticleErrConfigNotFound'],
+  ['Config is not an article config', 'ssArticleErrNotArticleConfig'],
+  ['Article content is empty', 'ssArticleErrContentEmpty'],
+  ['Session not found', 'ssArticleErrSessionNotFound'],
+  ['Paragraph not found', 'ssArticleErrParagraphNotFound'],
+  ['Session is abandoned', 'ssArticleSessionExpired'],
+  ['Session has ended', 'ssArticleErrSessionEnded'],
+  ['Report already generated', 'ssArticleErrReportGenerated'],
+  ['Segmentation failed', 'ssArticleSegmentFailed'],
+  ['Segmentation returned empty output', 'ssArticleSegmentFailed'],
+  ['content is required', 'ssArticleErrBadRequest'],
+  ['configId is required', 'ssArticleErrBadRequest'],
+  ['userId is required', 'ssArticleErrBadRequest'],
+  ['users is required', 'ssArticleErrBadRequest'],
+  ['users capped at 100', 'ssArticleErrBadRequest'],
+]
+
+for (const [detail, key] of cases) {
+  assert.equal(friendlyArticleError(new Error(detail)), key, `bare detail: ${detail}`)
+}
+
+// request() 万一没拆干净、或者别的 service 抛了带包装的 message,也要吃得下
+assert.equal(
+  friendlyArticleError(new Error('{"detail":"Session not found"}')),
+  'ssArticleErrSessionNotFound',
+  'raw JSON body',
+)
+assert.equal(
+  friendlyArticleError(new Error('[404] {"detail":"Config not found"}')),
+  'ssArticleErrConfigNotFound',
+  '@/services/speaking parse() 的 `[404] {...}` 形状',
+)
+
+// 409「有人在练」:人数照旧要能摘出来(弹框靠它),但它**不该**被当成错误文案 ——
+// 2026-07-24 起那是问句:handleApply 截住它去弹确认框(spec §4.3 B)。真漏到 toast 只能是
+// 别处误用,那就走通用兜底,绝不出现「暂时无法修改」这种已经不成立的话。
+assert.equal(
+  friendlyArticleError(new Error('Students are practising: 3')),
+  'ssArticleErrGeneric',
+  '409 不再映射成一句「无法修改」',
+)
+assert.equal(articleActiveCountFromError(new Error('Students are practising: 3')), 3)
+assert.equal(articleActiveCountFromError(new Error('Students are practising: 0')), 0)
+assert.equal(articleActiveCountFromError(new Error('Session not found')), null)
+assert.equal(articleActiveCountFromError(null), null)
+
+// 保存配置时整篇示范音频同步合成,失败的 detail 带着段号(`[2]` / `[1, 3]`)。
+// 段号是老师唯一能据以行动的信息,必须填进文案,不能只回一句笼统的失败。
+assert.equal(
+  friendlyArticleError(new Error('Demo audio synthesis failed: [2]')),
+  'ssArticleErrDemoAudio:2',
+  'single failing paragraph',
+)
+assert.equal(
+  friendlyArticleError(new Error('Demo audio synthesis failed: [1, 3]')),
+  'ssArticleErrDemoAudio:1、3',
+  'several failing paragraphs',
+)
+assert.equal(
+  friendlyArticleError(new Error('{"detail":"Demo audio synthesis failed: [4]"}')),
+  'ssArticleErrDemoAudio:4',
+  'wrapped in a JSON body',
+)
+
+// 兜底:绝不把原始内容漏出去
+const leaks = [
+  new Error('{"detail":"some brand new backend error"}'),
+  new Error('TypeError: Failed to fetch'),
+  new Error(''),
+  'a bare string',
+  null,
+  undefined,
+  { nope: true },
+]
+for (const cause of leaks) {
+  const out = friendlyArticleError(cause)
+  assert.equal(out, 'ssArticleErrGeneric', `fallback for ${JSON.stringify(String(cause))}`)
+  assert.ok(!out.includes('detail'), 'never leak the response body')
+  assert.ok(!out.includes('{'), 'never leak JSON')
+}
+
+console.log(`article errors: ${cases.length + 2} mappings + ${leaks.length} fallbacks OK`)

+ 26 - 0
scripts/test-article-reading-data.mjs

@@ -0,0 +1,26 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+
+const unitId = 'SHEP-5-上-Unit2'
+const topicUrl = new URL('../src/views/Editor/EnglishSpeaking/data/topicDiscussionTasks.json', import.meta.url)
+const articleUrl = new URL('../src/views/Editor/EnglishSpeaking/data/articleReadingTasks.json', import.meta.url)
+const topics = JSON.parse(await readFile(topicUrl, 'utf8'))
+const articles = JSON.parse(await readFile(articleUrl, 'utf8'))
+
+assert.equal(topics[unitId].length, 4)
+assert.ok(topics[unitId].every(task => task.taskType === 'topic-discussion'))
+assert.equal(articles[unitId].length, 1)
+
+const article = articles[unitId][0]
+assert.equal(article.id, 'ar-unit2-zoo')
+assert.equal(article.taskType, 'article-reading')
+assert.equal(article.titleEn, 'A Day at the Zoo')
+assert.equal(article.durationMinutes, 10)
+// 标题只住在 titleEn 里,**不再重复进正文**(2026-07-26 起)——
+// 正文第一段就是第一段朗读内容,否则学生第一次录音录的是标题。
+assert.ok(!article.content.startsWith(article.titleEn), 'title must not be duplicated into content')
+assert.ok(article.content.startsWith('Last Sunday'))
+assert.ok(article.content.endsWith('I learned many things about animals.'))
+// 首尾由上面两条断言守住;这条只兜住"中段被删",故用宽松下限而非精确值——
+// 正文允许正常修订,但不该缩水到只剩几段。(enspeak 原文实测 829 字符。)
+assert.ok(article.content.length > 700, 'full demo article must not be truncated')

+ 900 - 0
scripts/test-article-reading-engine.mjs

@@ -0,0 +1,900 @@
+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/useArticleReadingEngine.ts', import.meta.url)
+let source = await readFile(sourceUrl, 'utf8')
+
+// Vue 运行时替换为最小 shim;watch 只需支持 getter + 立即回调语义
+source = source.replace(
+  "import { ref, computed, watch, onUnmounted, toValue, type MaybeRefOrGetter } from 'vue'",
+  `
+const __watchers = []
+const ref = value => {
+  const r = { _v: value, get value() { return this._v }, set value(next) { this._v = next; __runWatchers() } }
+  return r
+}
+const computed = getter => ({ get value() { return getter() } })
+const watch = (getter, cb) => { __watchers.push({ getter, cb, last: getter() }) }
+const onUnmounted = () => {}
+const toValue = v => (typeof v === 'function' ? v() : (v && typeof v === 'object' && 'value' in v ? v.value : v))
+globalThis.__runWatchers = () => {
+  for (const w of __watchers) {
+    const next = w.getter()
+    if (next !== w.last) { const prev = w.last; w.last = next; w.cb(next, prev) }
+  }
+}
+`,
+)
+
+// 运行时依赖全部替换为注入桩;测试永远传完整 overrides,这些默认值不会被执行
+source = source
+  .replace("import { useAudioRecorder } from './useAudioRecorder'", 'const useAudioRecorder = () => globalThis.__deps.recorder')
+  .replace("import { useAudioPlayer } from './useAudioPlayer'", 'const useAudioPlayer = () => globalThis.__deps.player')
+  .replace(
+    /import \{[\s\S]*?\} from '\.\.\/services\/articleReading'/,
+    `
+const createArticleSession = (...a) => globalThis.__deps.createSession(...a)
+const getArticleReport = (...a) => globalThis.__deps.getReport(...a)
+const completeArticleSession = (...a) => globalThis.__deps.completeSession(...a)
+const getArticleDemoAudio = (...a) => globalThis.__deps.getDemoAudio(...a)
+const abandonArticleSession = (...a) => globalThis.__deps.abandonSession(...a)
+`,
+  )
+  .replace(
+    /import \{[\s\S]*?\} from '\.\.\/services\/articleErrors'/,
+    `
+const friendlyArticleError = cause => {
+  const raw = cause instanceof Error ? cause.message : String(cause ?? '')
+  return raw === 'Session not found' ? '__localized_session_not_found__' : '__localized_generic__'
+}
+const isArticleSessionGone = cause => cause?.status === 409
+`,
+  )
+  .replace(
+    "import { openArticleParagraphStream, type ArticleParagraphStream } from '../services/articleReadingStream'",
+    'const openArticleParagraphStream = (...a) => globalThis.__deps.createStream(...a)',
+  )
+  .replace(
+    "import { isFinalReportReady, pollDelayMs } from '../preview/articleReadingModel'",
+    `
+const isFinalReportReady = r => !r.paragraphs.some(p => p.status === 'generating')
+  && (r.overallStatus === 'completed' || r.overallStatus === 'failed' || r.overallStatus === null)
+const pollDelayMs = attempt => Math.min(8000, 1000 * 2 ** Math.max(0, attempt))
+`,
+  )
+
+const compiled = ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+
+// ---------------- fakes ----------------
+
+function makeRecorder(sampleRate = 48000) {
+  const rec = {
+    recordingDuration: { _v: 0, get value() { return this._v }, set value(v) { this._v = v; globalThis.__runWatchers() } },
+    sampleRate: { value: sampleRate },
+    onChunk: { value: null },
+    started: 0,
+    cleaned: 0,
+    async startRecording() { this.started += 1 },
+    async stopRecording() { return { wav: true, size: 1 } },
+    // 真实实现在这一步停 mediaStream track、关 AudioContext、断开 worklet、清时长 interval。
+    // 不拆的后果不是泄漏一点内存:孤儿 worklet 会继续把音频灌进下一段录音。
+    cleanup() { this.cleaned += 1 },
+  }
+  return rec
+}
+
+function makePlayer() {
+  return {
+    playingId: { value: null },
+    loadingId: { value: null },
+    errorId: { value: null },
+    plays: [],
+    // lazy-url 的 URL 解析发生在 play() 内部(与真 player 一致):示范音频那条
+    // 失败路径要经过这里,引擎的 error.value 才立得起来
+    async play(id, source) {
+      this.plays.push({ id, source })
+      this.loadingId.value = id
+      if (source.kind === 'lazy-url') {
+        try { await source.resolve(new AbortController().signal) }
+        catch {
+          this.loadingId.value = null
+          this.errorId.value = id
+          return
+        }
+      }
+      this.loadingId.value = null
+      this.playingId.value = id
+    },
+    stop() { this.playingId.value = null; this.loadingId.value = null },
+  }
+}
+
+function makeStream() {
+  const stream = {
+    chunks: [],
+    stopped: false,
+    aborted: null,
+    resolveAck: null,
+    pushChunk(c) { this.chunks.push(c) },
+    stop() { this.stopped = true; return new Promise(res => { this.resolveAck = res }) },
+    abort(reason) { this.aborted = reason },
+  }
+  return stream
+}
+
+/** 手控时钟:测试自己决定何时推进 timeout/interval。 */
+function makeClock() {
+  let nextId = 1
+  let nowMs = Date.UTC(2026, 0, 1, 0, 0, 0)
+  const timeouts = new Map()
+  const intervals = new Map()
+  return {
+    timeouts,
+    intervals,
+    now() { return nowMs },
+    /** 推进墙上时钟(秒)。不自动触发 interval —— 用 tickInterval 显式驱动。 */
+    advance(seconds) { nowMs += seconds * 1000 },
+    setTimeout(fn, delay) { const id = nextId++; timeouts.set(id, { fn, delay }); return id },
+    clearTimeout(id) { timeouts.delete(id) },
+    setInterval(fn, delay) { const id = nextId++; intervals.set(id, { fn, delay }); return id },
+    clearInterval(id) { intervals.delete(id) },
+    async runTimeouts() {
+      const pending = [...timeouts.entries()]
+      timeouts.clear()
+      for (const [, t] of pending) { t.fn(); await flush() }
+    },
+    async tickInterval(times = 1) {
+      for (let i = 0; i < times; i++) {
+        for (const [, t] of [...intervals.entries()]) t.fn()
+        await flush()
+      }
+    },
+  }
+}
+
+function makeDoc(hidden = false) {
+  return {
+    hidden,
+    listeners: {},
+    addEventListener(type, fn) { (this.listeners[type] ||= []).push(fn) },
+    removeEventListener(type, fn) {
+      this.listeners[type] = (this.listeners[type] || []).filter(f => f !== fn)
+    },
+    fire(type) { for (const fn of [...(this.listeners[type] || [])]) fn() },
+  }
+}
+
+const flush = () => new Promise(resolve => setImmediate(resolve))
+
+function paragraphs(statuses) {
+  return statuses.map((status, idx) => ({
+    idx, text: `p${idx}`, status, audioUrl: null, overallScore: null, dims: null, words: [],
+  }))
+}
+
+function reportOf(statuses, overallStatus = null, extra = {}) {
+  return {
+    sessionId: 'sess-1', title: 'T', status: 'reading', totalDurationSeconds: null,
+    // §4.4:绝对截止时刻。默认 null = 不限时,既有用例的行为因此不变。
+    expiresAt: null,
+    paragraphs: paragraphs(statuses), overall: null, overallStatus, ...extra,
+  }
+}
+
+function setup({
+  statuses = ['pending', 'pending'], reports = [],
+  showReport = true, durationMinutes = 1, expiresAt = null, userId = 'u1', abandonSession, completeSession,
+  getReport, createSession, getDemoAudio, onSessionAbandoned, doc, clock,
+} = {}) {
+  const recorder = makeRecorder()
+  const player = makePlayer()
+  const streams = []
+  const calls = { complete: 0, getReport: 0, demo: 0, abandon: 0 }
+  let reportQueue = [...reports]
+
+  const deps = {
+    recorder,
+    player,
+    document: doc ?? makeDoc(),
+    setTimeout: (...a) => clock.setTimeout(...a),
+    clearTimeout: id => clock.clearTimeout(id),
+    setInterval: (...a) => clock.setInterval(...a),
+    clearInterval: id => clock.clearInterval(id),
+    now: () => clock.now(),
+    async createSession(...a) {
+      if (createSession) return createSession(...a)
+      return reportOf(statuses, null, { expiresAt })
+    },
+    async getReport() {
+      calls.getReport += 1
+      if (getReport) return getReport()
+      return reportQueue.length > 1 ? reportQueue.shift() : (reportQueue[0] ?? reportOf(statuses, null, { expiresAt }))
+    },
+    completeSession: completeSession ?? (async () => { calls.complete += 1 }),
+    async getDemoAudio(...a) {
+      calls.demo += 1
+      if (getDemoAudio) return getDemoAudio(...a)
+      return { url: 'https://s3/demo.mp3' }
+    },
+    abandonSession: abandonSession ?? (async id => { calls.abandon += 1; return { sessionId: id, status: 'abandoned' } }),
+    createStream(opts) { const s = makeStream(); s.opts = opts; streams.push(s); return s },
+  }
+  globalThis.__deps = deps
+
+  const completed = []
+  const engine = mod.useArticleReadingEngine(
+    {
+      configId: 'c1', userId, durationMinutes, showReport,
+      onSessionCompleted: id => completed.push(id),
+      onSessionAbandoned,
+    },
+    deps,
+  )
+  return { engine, recorder, player, streams, calls, completed, deps, setReports: r => { reportQueue = [...r] } }
+}
+
+// ---- 1. stream 建立前的 PCM 不丢;handshake 用 recorder 真实 sampleRate ----
+{
+  const clock = makeClock()
+  const { engine, recorder, streams } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  const startPromise = engine.startRecording()
+  const early = new Uint8Array([7, 7]).buffer
+  recorder.onChunk.value?.(early)
+  await startPromise
+
+  assert.equal(streams.length, 1)
+  assert.equal(streams[0].opts.sampleRate, 48000, 'handshake must use the recorder sample rate')
+  assert.equal(streams[0].chunks[0], early, 'PCM produced before the stream existed must still be first')
+}
+
+// ---- 2. finishRecording:先取 WAV 再 stop;ack 后立刻前进,不等分数 ----
+{
+  const clock = makeClock()
+  const { engine, streams } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.startRecording()
+
+  const finishing = engine.finishRecording()
+  await flush()
+  assert.equal(streams[0].stopped, true, 'stop must be sent after the WAV blob is taken')
+  assert.equal(engine.recordingState.value, 'processing')
+
+  streams[0].resolveAck({ type: 'ack', status: 'generating', attempt: 1, truncated: false })
+  await finishing
+
+  assert.equal(engine.currentParagraphIdx.value, 1, 'must advance on ack without waiting for scores')
+  assert.ok(engine.localAudioByParagraph.value.get(0), 'own recording must be replayable immediately')
+}
+
+// ---- 3. 单段 180s 只停当前段;练习计时归零才 complete ----
+{
+  const clock = makeClock()
+  const { engine, recorder, streams, calls } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.startRecording()
+
+  recorder.recordingDuration.value = 180
+  await flush()
+  streams[0].resolveAck?.({ type: 'ack', status: 'generating', attempt: 1, truncated: false })
+  await flush()
+  assert.equal(calls.complete, 0, '180s paragraph cap must not complete the whole practice')
+}
+// ---- 3a. 计时器按时间戳算:后台标签页节流也不会少算(spec §9.2.3)----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()  // 10 分钟后
+  const { engine } = setup({ clock, expiresAt, durationMinutes: 10 })
+  await engine.startSession()
+  await clock.runTimeouts()
+  assert.equal(engine.elapsedPracticeSeconds.value, 0, 'count-up timer starts at zero')
+
+  // 墙上时钟走了 300 秒,但 interval 只被浏览器放行了一次
+  clock.advance(300)
+  await clock.tickInterval(1)
+  assert.equal(engine.elapsedPracticeSeconds.value, 300,
+    'tick accumulation would say 1; only a timestamp reading says 300')
+}
+
+// ---- 3b. 到点:冻结在上限,且绝不发 complete ----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()
+  const { engine, calls } = setup({ clock, expiresAt, durationMinutes: 10 })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  clock.advance(700)               // 越过截止时刻
+  await clock.tickInterval(1)
+  assert.equal(engine.elapsedPracticeSeconds.value, 600, 'must freeze at the cap, not overshoot')
+  assert.equal(calls.complete, 0, 'the deadline belongs to the backend; the FE never completes')
+}
+
+// ---- 3c. 到点只问一次后端,不重复 ----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()
+  const { engine, calls } = setup({
+    clock, expiresAt, durationMinutes: 10,
+    getReport: async () => reportOf(['completed', 'completed'], 'completed', { expiresAt, status: 'completed' }),
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  const before = calls.getReport
+
+  clock.advance(700)
+  await clock.tickInterval(1)
+  await clock.runTimeouts()
+  assert.equal(calls.getReport, before + 1, 'exactly one query at the local deadline')
+
+  await clock.tickInterval(3)
+  await clock.runTimeouts()
+  assert.equal(calls.getReport, before + 1, 'and only one — the interval is already stopped')
+}
+
+// ---- 3d. 没有截止时刻(不限时):不起 interval,不显示计时 ----
+{
+  const clock = makeClock()
+  const { engine } = setup({ clock, expiresAt: null })
+  await engine.startSession()
+  await clock.runTimeouts()
+  assert.equal(engine.elapsedPracticeSeconds.value, null, 'no deadline means no timer to show')
+  assert.equal(clock.intervals.size, 0, 'no interval may be started without a deadline')
+}
+
+// ---- 3e. 后端单方面结束,showReport 开:转结果页流程(spec §9.2.3 第 2 条)----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()
+  const { engine, completed } = setup({
+    clock, expiresAt, durationMinutes: 10, showReport: true,
+    getReport: async () => reportOf(['completed', 'completed'], 'generating', { expiresAt, status: 'completed' }),
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  clock.advance(700)
+  await clock.tickInterval(1)
+  await clock.runTimeouts()
+
+  assert.equal(engine.stage.value, 'report-loading', 'a server-side end must open the report gate')
+  assert.deepEqual(completed, ['sess-1'], 'the teacher must still get the broadcast')
+}
+
+// ---- 3f. 后端单方面结束,showReport 关:留在练习页 + 合上只读锁(§9.2.1)----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()
+  const { engine } = setup({
+    clock, expiresAt, durationMinutes: 10, showReport: false,
+    getReport: async () => reportOf(['completed', 'completed'], 'completed', { expiresAt, status: 'completed' }),
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  clock.advance(700)
+  await clock.tickInterval(1)
+  await clock.runTimeouts()
+
+  assert.equal(engine.stage.value, 'reading', 'hidden reports keep the student on the practice page')
+  assert.equal(engine.sessionEnded.value, true, 'the read-only lock must engage')
+  assert.equal(engine.error.value, null, 'a server-side end is not an error')
+}
+
+// ---- 3g. 本地已 complete 过:后端的结束信号不得再广播一次 ----
+{
+  const clock = makeClock()
+  const expiresAt = new Date(clock.now() + 600_000).toISOString()
+  const { engine, completed } = setup({
+    clock, expiresAt, durationMinutes: 10, showReport: true,
+    getReport: async () => reportOf(['completed', 'completed'], 'completed', { expiresAt, status: 'completed' }),
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+
+  clock.advance(700)
+  await clock.tickInterval(1)
+  await clock.runTimeouts()
+
+  assert.deepEqual(completed, ['sess-1'], 'exactly one completion broadcast')
+}
+
+// ---- 3h. resume 重启显示计时器,算出的是真实已用时长 ----
+{
+  const clock = makeClock()
+  // 截止时刻在 4 分钟后,配置 10 分钟 → 已经过了 6 分钟
+  const expiresAt = new Date(clock.now() + 240_000).toISOString()
+  const { engine } = setup({ clock, expiresAt, durationMinutes: 10 })
+
+  engine.resumeFromReport(reportOf(['pending', 'pending'], null, { expiresAt }))
+  assert.equal(engine.elapsedPracticeSeconds.value, 360,
+    'resume must show real elapsed time, not reset to zero')
+}
+
+// ---- 4. 后台标签页不发 GET;回到前台继续;退避不因隐藏而推进 ----
+{
+  const clock = makeClock()
+  const doc = makeDoc(true)
+  const { engine, calls } = setup({ clock, doc, statuses: ['generating'], reports: [reportOf(['generating'])] })
+  await engine.startSession()
+  await clock.runTimeouts()
+  assert.equal(calls.getReport, 0, 'hidden tab must not issue GET')
+
+  doc.hidden = false
+  doc.fire('visibilitychange')
+  await clock.runTimeouts()
+  assert.ok(calls.getReport >= 1, 'returning to foreground must resume polling')
+}
+{
+  // 退避序列 1s/2s/4s/8s 封顶
+  assert.equal(Math.min(8000, 1000 * 2 ** 0), 1000)
+  assert.equal(Math.min(8000, 1000 * 2 ** 5), 8000)
+}
+
+// ---- 5. overallStatus=null 且无 generating 段可进 report;ready 前保持 report-loading ----
+{
+  const clock = makeClock()
+  const { engine } = setup({
+    clock,
+    statuses: ['completed'],
+    reports: [reportOf(['generating'], null), reportOf(['completed'], null)],
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+  assert.equal(engine.stage.value, 'report-loading', 'must not reveal a partial report while generating')
+
+  await clock.runTimeouts()
+  assert.equal(engine.stage.value, 'report')
+}
+
+// ---- 5b. 段落全 completed 但 overallStatus='generating'(真实 complete 窗口):
+//         必须持续轮询直到 overall 落定,不得卡在 report-loading ----
+{
+  const clock = makeClock()
+  const { engine, calls } = setup({
+    clock,
+    statuses: ['completed'],
+    reports: [
+      reportOf(['completed'], 'generating'),
+      reportOf(['completed'], 'generating'),
+      reportOf(['completed'], 'completed'),
+    ],
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+  assert.equal(engine.stage.value, 'report-loading', 'overallStatus=generating must keep the loading gate closed')
+
+  const midGets = calls.getReport
+  await clock.runTimeouts()
+  assert.ok(calls.getReport > midGets, 'must keep polling while overallStatus is still generating')
+
+  await clock.runTimeouts()
+  assert.equal(engine.stage.value, 'report', 'report must appear once overallStatus settles to completed')
+}
+
+// ---- 6. 全段评完静默 complete 一次,stage 仍是 reading;随后看报告不再 POST ----
+{
+  const clock = makeClock()
+  const { engine, calls, completed } = setup({
+    clock,
+    statuses: ['completed'],
+    reports: [reportOf(['completed'], 'completed')],
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+
+  const afterFirst = calls.complete
+  assert.equal(afterFirst, 1, 'completion must be issued exactly once')
+  assert.equal(completed.length, 1, 'onSessionCompleted must fire once')
+
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+  assert.equal(calls.complete, 1, 'viewing the report again must not re-POST complete')
+}
+
+// ---- 7. POST 之前抓到的 report 不得满足最终 gate ----
+{
+  const clock = makeClock()
+  const { engine, calls } = setup({
+    clock,
+    statuses: ['completed'],
+    reports: [reportOf(['completed'], null)],
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  // 没有 generating 段就不启动轮询,所以此刻静默预热还没有机会跑
+  assert.equal(calls.complete, 0)
+  assert.equal(calls.getReport, 0, 'a report fetched before the POST cannot satisfy the final gate')
+
+  await engine.completeAndShowReport()
+  await clock.runTimeouts()
+  assert.equal(engine.stage.value, 'report')
+  assert.ok(calls.getReport >= 1, 'at least one GET must be issued after the completion POST')
+  assert.equal(calls.complete, 1, 'completion is issued exactly once')
+}
+
+// ---- 8. 取消录音:abort 流、回到 idle、不产生评分任务 ----
+{
+  const clock = makeClock()
+  const { engine, streams } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.startRecording()
+
+  await engine.cancelRecording()
+
+  assert.equal(streams[0].stopped, false, 'cancel must not send stop, so no scoring task is created')
+  assert.match(streams[0].aborted, /cancel/i)
+  assert.equal(engine.recordingState.value, 'idle')
+  assert.equal(engine.currentParagraphIdx.value, 0, 'cancel must not advance to the next paragraph')
+  assert.equal(engine.localAudioByParagraph.value.size, 0, 'a cancelled attempt leaves no replayable audio')
+}
+
+// ---- showReport=false:completed 的 session 不进结果页,留在练习页 ----
+{
+  const { engine } = setup({ showReport: false })
+  engine.resumeFromReport({ ...reportOf(['completed'], 'completed'), status: 'completed' })
+  assert.equal(engine.stage.value, 'reading', 'showReport 关闭时 completed 也留在 reading')
+  assert.equal(engine.sessionEnded.value, true, 'sessionEnded 仍为 true —— 只读锁靠它')
+}
+
+// ---- showReport=true:completed 的 session 照旧进结果页 ----
+{
+  const { engine } = setup({ showReport: true })
+  engine.resumeFromReport({ ...reportOf(['completed'], 'completed'), status: 'completed' })
+  assert.equal(engine.stage.value, 'report', 'showReport 打开时行为不变')
+}
+
+// ---- restartSession:先 abandon 再回起始页 ----
+{
+  const abandoned = []
+  const { engine } = setup({
+    abandonSession: async id => { abandoned.push(id); return { sessionId: id, status: 'abandoned' } },
+  })
+  engine.resumeFromReport(reportOf(['pending', 'pending']))
+  assert.equal(engine.stage.value, 'reading')
+
+  const ok = await engine.restartSession()
+  assert.equal(ok, true)
+  assert.deepEqual(abandoned, ['sess-1'], '把旧 session 送去 abandon')
+  assert.equal(engine.stage.value, 'ready', '回起始页')
+  assert.equal(engine.report.value, null, 'report 已清空')
+}
+
+// ---- restartSession 失败:不重置,返回 false,旧 session 仍然有效 ----
+{
+  const { engine } = setup({
+    abandonSession: async () => { throw new Error('network down') },
+  })
+  engine.resumeFromReport(reportOf(['pending', 'pending']))
+
+  const ok = await engine.restartSession()
+  assert.equal(ok, false)
+  assert.equal(engine.stage.value, 'reading', '失败不能把学生踢回起始页')
+  assert.equal(engine.report.value?.sessionId, 'sess-1', '旧 session 仍在')
+  assert.equal(engine.error.value, null, '失败只回 false,文案由调用方决定')
+}
+
+// ---- 录音中「重新开始」:录音器必须真的拆掉 ----
+// 只摘 onChunk 不够:mediaStream track、AudioContext、worklet、时长 interval 都还活着,
+// 孤儿 worklet 会继续把音频灌进下一段录音(上传的 WAV 与 WS 流都成了两套采集图交织)。
+{
+  const clock = makeClock()
+  const { engine, recorder, streams } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.startRecording()
+  assert.equal(recorder.cleaned, 0, '录音进行中,还没到拆的时候')
+
+  const ok = await engine.restartSession()
+  assert.equal(ok, true)
+  assert.equal(recorder.cleaned, 1, '重新开始必须拆掉录音器,不能只摘 onChunk')
+  assert.equal(recorder.onChunk.value, null, 'onChunk 也要摘')
+  assert.ok(streams[0].aborted, 'WS 流一并中止')
+  assert.equal(engine.stage.value, 'ready')
+}
+
+// ---- 「重新开始」成功后广播作废,老师端才会把「已完成」刷回「未开始」----
+{
+  const abandonedBroadcasts = []
+  const { engine } = setup({
+    onSessionAbandoned: id => abandonedBroadcasts.push(id),
+  })
+  engine.resumeFromReport(reportOf(['pending', 'pending']))
+  await engine.restartSession()
+  assert.deepEqual(abandonedBroadcasts, ['sess-1'], '成功作废要通知调用方去广播')
+}
+
+// ---- 作废失败不广播 ----
+{
+  const abandonedBroadcasts = []
+  const { engine } = setup({
+    abandonSession: async () => { throw new Error('network down') },
+    onSessionAbandoned: id => abandonedBroadcasts.push(id),
+  })
+  engine.resumeFromReport(reportOf(['pending', 'pending']))
+  await engine.restartSession()
+  assert.deepEqual(abandonedBroadcasts, [], '没作废成功就不能报「这一轮不算了」')
+}
+
+console.log('article reading engine: all cases passed')
+
+// ---- 错误本地化:每个出口都是本地化文案,绝不是响应体(spec §9.2.5 A1)----
+
+const LEAK = 'Session not found'
+
+// 1. startSession
+{
+  const clock = makeClock()
+  const { engine } = setup({ clock, createSession: async () => { throw new Error(LEAK) } })
+  await engine.startSession()
+  assert.equal(engine.error.value, '__localized_session_not_found__',
+    'a failed start must not show the backend detail')
+}
+
+// 5. pollOnce
+{
+  const clock = makeClock()
+  const { engine } = setup({
+    clock,
+    statuses: ['generating'],
+    getReport: async () => { throw new Error(LEAK) },
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  assert.equal(engine.error.value, '__localized_session_not_found__')
+}
+
+// 6. playDemoAudio
+{
+  const clock = makeClock()
+  // 必须经 setup 注入:引擎把 overrides 展开进一个**新对象**,
+  // 构造之后再改外面那个 deps 是改不到它的。
+  const { engine } = setup({ clock, getDemoAudio: async () => { throw new Error(LEAK) } })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.playDemoAudio(0)
+  assert.equal(engine.error.value, '__localized_session_not_found__')
+}
+
+// 4. completeAndShowReport
+{
+  const clock = makeClock()
+  const { engine } = setup({ clock, completeSession: async () => { throw new Error(LEAK) } })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.completeAndShowReport()
+  assert.equal(engine.error.value, '__localized_session_not_found__')
+}
+
+// 2. startRecording(麦克风;不是后端 detail → 走兜底)
+{
+  const clock = makeClock()
+  const { engine, recorder } = setup({ clock })
+  await engine.startSession()
+  await clock.runTimeouts()
+  recorder.startRecording = async () => { throw new Error('NotAllowedError') }
+  await engine.startRecording()
+  assert.equal(engine.error.value, '__localized_generic__',
+    'an unmapped cause must fall back, never surface raw')
+}
+
+// 409 的分支逻辑保留(spec §9.2.5 第 4 条),同时文案也本地化
+{
+  const clock = makeClock()
+  const gone = Object.assign(new Error('Session is abandoned'), { status: 409 })
+  const { engine } = setup({ clock, completeSession: async () => { throw gone } })
+  await engine.startSession()
+  await clock.runTimeouts()
+  await engine.completeAndShowReport()
+  assert.equal(engine.sessionInvalid.value, true, '409 must still flag sessionInvalid')
+  assert.ok(!String(engine.error.value).includes('{'), 'never any JSON in user-facing copy')
+}
+
+console.log('article reading engine: error localization cases passed')
+
+// ---- 轮询失败上限(spec §9.2.5 待办 Unsure-1)----
+
+// 后端持续不应答 → 敲满上限就停手,不再每 8 秒一次敲到学生关页面
+{
+  const clock = makeClock()
+  const { engine, calls } = setup({
+    clock,
+    statuses: ['generating'],
+    getReport: async () => { throw new Error('boom') },
+  })
+  await engine.startSession()
+  for (let i = 0; i < 30; i++) await clock.runTimeouts()
+
+  assert.equal(calls.getReport, 8, 'must give up after MAX_POLL_FAILURES consecutive failures')
+  assert.equal(clock.timeouts.size, 0, 'no timer may be left armed after giving up')
+  assert.ok(engine.error.value, 'the banner must stay so the student knows why it stopped')
+}
+
+// 中途成功一次 → 失败计数归零,健康的长轮询不会被误掐
+{
+  const clock = makeClock()
+  let n = 0
+  const clockRef = clock
+  const { engine, calls } = setup({
+    clock: clockRef,
+    statuses: ['generating'],
+    getReport: async () => {
+      n += 1
+      // 前 5 次失败,第 6 次成功(仍 generating),之后继续失败
+      if (n === 6) return reportOf(['generating'])
+      throw new Error('boom')
+    },
+  })
+  await engine.startSession()
+  for (let i = 0; i < 40; i++) await clock.runTimeouts()
+
+  // 5 次失败 + 1 次成功(归零)+ 8 次失败 = 14
+  assert.equal(calls.getReport, 14, 'a successful response must reset the failure budget')
+}
+
+// 停手之后,新一轮(schedulePoll(0))要拿到新的失败预算
+{
+  const clock = makeClock()
+  const { engine, calls } = setup({
+    clock,
+    statuses: ['generating'],
+    getReport: async () => { throw new Error('boom') },
+  })
+  await engine.startSession()
+  for (let i = 0; i < 30; i++) await clock.runTimeouts()
+  assert.equal(calls.getReport, 8)
+
+  // 学生做了点什么(录完一段 / 点重试)→ 重新起一轮
+  engine.resumeFromReport(reportOf(['generating']))
+  for (let i = 0; i < 30; i++) await clock.runTimeouts()
+  assert.equal(calls.getReport, 16, 'a fresh chain must get a fresh budget, not stop on failure #1')
+}
+
+console.log('article reading engine: poll failure cap cases passed')
+
+// ---- 轮询横幅不滞留(spec §9.2.5 待办 Unsure-3)----
+
+// 失败一次 → 横幅出现;下一次成功 → 横幅收走,不留在学生眼前
+{
+  const clock = makeClock()
+  let n = 0
+  const { engine } = setup({
+    clock,
+    statuses: ['generating'],
+    getReport: async () => {
+      n += 1
+      if (n === 1) throw new Error('boom')
+      return reportOf(['generating'])
+    },
+  })
+  await engine.startSession()
+  await clock.runTimeouts()
+  assert.ok(engine.error.value, 'a transient poll failure must raise the banner')
+
+  await clock.runTimeouts()
+  assert.equal(engine.error.value, null, 'a later successful poll must take its own banner down')
+}
+
+// 但**只**收自己竖的那面:录音被拒的解释必须活过随后那次成功轮询。
+// finishRecording 的 catch 设完 error 就 schedulePoll(0)(§4.4 到期收口),
+// 无差别清 error 会让学生在唯读锁合上的同一瞬间失去「为什么录不了」的唯一说明。
+{
+  const clock = makeClock()
+  const { engine } = setup({ clock, getReport: async () => reportOf(['pending', 'pending']) })
+  await engine.startSession()
+  await clock.runTimeouts()
+
+  engine.error.value = '__recording_rejected__'
+  await clock.runTimeouts()   // 成功轮询跑一轮
+  assert.equal(engine.error.value, '__recording_rejected__',
+    'a non-poll error must survive a successful poll')
+}
+
+console.log('article reading engine: stale banner cases passed')
+
+// ---- 编辑态预览:userId 原样透传给 createSession,null 不得被改写成空串(spec §4.5)----
+{
+  const clock = makeClock()
+  const seen = []
+  const { engine } = setup({
+    clock,
+    userId: null,
+    createSession: async (configId, uid) => {
+      seen.push([configId, uid])
+      return reportOf(['pending', 'pending'])
+    },
+  })
+  await engine.startSession()
+
+  assert.deepEqual(seen, [['c1', null]],
+    'editor preview must reach the backend as null, not "" — an empty string is a student named ""')
+}
+
+{
+  const clock = makeClock()
+  const seen = []
+  const { engine } = setup({
+    clock,
+    userId: 'u9',
+    createSession: async (configId, uid) => {
+      seen.push([configId, uid])
+      return reportOf(['pending', 'pending'])
+    },
+  })
+  await engine.startSession()
+
+  assert.deepEqual(seen, [['c1', 'u9']], 'a real student id must still be forwarded verbatim')
+}
+
+// ---- 双击「开始朗读」只发一个 POST /session ----
+// 后端 get-or-create 无锁、无唯一约束:两个并发请求会双双查不到既有 session 然后各插一行。
+// 编辑态预览(userId=null)必然如此 —— `_latest_session` 对空 user 早退返回 None(spec §4.5)。
+// 多出来那一行会算进 §4.3 B 的「N 名学生正在练习中」直到过期,还会多写一笔作业记录(§9.2.4)。
+{
+  const clock = makeClock()
+  let calls = 0
+  let release
+  const gate = new Promise(resolve => { release = resolve })
+  const { engine } = setup({
+    clock,
+    userId: null,
+    createSession: async () => {
+      calls += 1
+      await gate
+      return reportOf(['pending', 'pending'])
+    },
+  })
+
+  const first = engine.startSession()
+  await flush()
+  assert.equal(engine.starting.value, true, 'POST 在途时 starting 必须为真,视图据此置灰')
+
+  const second = engine.startSession()   // 第二次点击:必须被闩早退吃掉
+  release()
+  await Promise.all([first, second])
+
+  assert.equal(calls, 1, 'double click must not create a second session')
+  assert.equal(engine.starting.value, false, '结束后闩必须放开,失败重试才点得动')
+}
+
+// ---- 开始失败后闩要放开:学生还能再点一次(承 §9.2.5 A2 的 readyError 出口)----
+{
+  const clock = makeClock()
+  let calls = 0
+  const { engine } = setup({
+    clock,
+    createSession: async () => {
+      calls += 1
+      if (calls === 1) throw new Error('Config not found')
+      return reportOf(['pending', 'pending'])
+    },
+  })
+
+  await engine.startSession()
+  assert.equal(engine.stage.value, 'ready', '失败停在起始页')
+  assert.ok(engine.error.value, '失败必须有文案')
+  assert.equal(engine.starting.value, false, 'finally 放闩,不是只在成功路径上放')
+
+  await engine.startSession()
+  assert.equal(calls, 2, '第二次点击必须真的发出去')
+  assert.equal(engine.stage.value, 'reading')
+}

+ 79 - 0
scripts/test-article-reading-i18n.mjs

@@ -0,0 +1,79 @@
+import assert from 'node:assert/strict'
+import { readFile, readdir } from 'node:fs/promises'
+import path from 'node:path'
+
+const root = new URL('../', import.meta.url)
+const read = p => readFile(new URL(p, root), 'utf8')
+
+const locales = {}
+for (const loc of ['cn', 'en', 'hk']) {
+  locales[loc] = JSON.parse(await read(`src/views/lang/${loc}.json`))
+}
+
+// 扫描源码中真正引用到的 ssArticle* 键,而不是维护一张会过期的手写清单
+const scanRoots = [
+  'src/views/Editor/EnglishSpeaking',
+  'src/views/Student/components/ArticleReadingClassPanel',
+  'src/views/components/element/FrameElement',
+  'src/views/Editor/CanvasTool',
+  'src/components/CollapsibleToolbar',
+  'src/views/Student',
+]
+
+async function collectFiles(dir) {
+  const out = []
+  let entries
+  try {
+    entries = await readdir(new URL(dir + '/', root), { withFileTypes: true })
+  } catch {
+    return out
+  }
+  for (const entry of entries) {
+    const child = path.posix.join(dir, entry.name)
+    if (entry.isDirectory()) out.push(...await collectFiles(child))
+    else if (/\.(vue|ts)$/.test(entry.name)) out.push(child)
+  }
+  return out
+}
+
+const files = (await Promise.all(scanRoots.map(collectFiles))).flat()
+const used = new Set()
+for (const file of files) {
+  const source = await read(file)
+  for (const match of source.matchAll(/\blang\.(ssArticle[A-Za-z0-9]*)/g)) used.add(match[1])
+}
+
+assert.ok(used.size > 20, `expected many article keys in use, found ${used.size}`)
+
+// 每个被引用的键在三个语言包里都必须存在、非空、无首尾空白
+const missing = []
+for (const key of [...used].sort()) {
+  for (const loc of ['cn', 'en', 'hk']) {
+    const value = locales[loc][key]
+    if (typeof value !== 'string' || value.length === 0) {
+      missing.push(`${loc}.json missing ${key}`)
+      continue
+    }
+    assert.equal(value, value.trim(), `${loc}.json ${key} has surrounding whitespace`)
+  }
+}
+assert.deepEqual(missing, [], `unresolved article i18n keys:\n${missing.join('\n')}`)
+
+// 三个语言包的 ssArticle* 键集合必须完全一致,防止某个语言漏翻
+const keySets = Object.fromEntries(
+  Object.entries(locales).map(([loc, pack]) => [
+    loc,
+    Object.keys(pack).filter(key => key.startsWith('ssArticle')).sort(),
+  ]),
+)
+assert.deepEqual(keySets.en, keySets.cn, 'en.json article keys differ from cn.json')
+assert.deepEqual(keySets.hk, keySets.cn, 'hk.json article keys differ from cn.json')
+
+// en 与 cn 的文案不应逐字相同(除少数本就一致的符号型文案)
+const identical = keySets.cn.filter(key => locales.en[key] === locales.cn[key])
+assert.ok(
+  identical.length <= 2,
+  `en.json looks untranslated for: ${identical.join(', ')}`,
+)
+
+console.log(`article reading i18n: ${used.size} keys used, ${keySets.cn.length} defined, all three locales consistent`)

+ 52 - 0
scripts/test-article-reading-model.mjs

@@ -0,0 +1,52 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+const unitId = 'SHEP-5-上-Unit2'
+const topics = JSON.parse(
+  await readFile(new URL('../src/views/Editor/EnglishSpeaking/data/topicDiscussionTasks.json', import.meta.url), 'utf8'),
+)
+const articles = JSON.parse(
+  await readFile(new URL('../src/views/Editor/EnglishSpeaking/data/articleReadingTasks.json', import.meta.url), 'utf8'),
+)
+const article = articles[unitId][0]
+
+// 模型的所有导入都是 `import type`,转译后被抹除,因此模块自足、可直接在 Node 里跑。
+const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/preview/articleReadingModel.ts', import.meta.url)
+const source = await readFile(sourceUrl, 'utf8')
+const compiled = ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 },
+}).outputText
+const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}#${Math.random()}`)
+
+const empty = mod.buildEmptyArticleConfig('五年级')
+assert.deepEqual(empty, {
+  type: 'article',
+  grade: '五年级',
+  title: '',
+  content: '',
+  practice: { mode: 'time', duration: 10 },
+  evaluation: { showReport: true },
+})
+
+const fromTask = mod.buildArticleConfig(article, '五年级')
+assert.equal(fromTask.type, 'article')
+assert.equal(fromTask.grade, '五年级')
+assert.equal(fromTask.title, 'A Day at the Zoo')
+assert.equal(fromTask.content, article.content)
+
+assert.equal(mod.scoreToStars(0), 0)
+assert.equal(mod.scoreToStars(79), 3.95)
+assert.equal(mod.scoreToStars(100), 5)
+
+const allTasks = [...topics[unitId], article]
+assert.equal(mod.filterSpeakingTasks(allTasks, 'all').length, 5)
+assert.equal(mod.filterSpeakingTasks(allTasks, 'topic-discussion').length, 4)
+assert.equal(mod.filterSpeakingTasks(allTasks, 'article-reading').length, 1)
+
+assert.equal(mod.isFinalReportReady({ paragraphs: [{ status: 'generating' }], overallStatus: null }), false)
+assert.equal(mod.isFinalReportReady({ paragraphs: [{ status: 'completed' }], overallStatus: null }), true)
+assert.equal(mod.isFinalReportReady({ paragraphs: [{ status: 'failed' }], overallStatus: 'failed' }), true)
+
+assert.equal(mod.pollDelayMs(0), 1000)
+assert.equal(mod.pollDelayMs(5), 8000)

+ 137 - 0
scripts/test-article-reading-routing.mjs

@@ -0,0 +1,137 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+
+const read = path => readFile(new URL(`../${path}`, import.meta.url), 'utf8')
+
+const frameEditor = await read('src/views/components/element/FrameElement/index.vue')
+const frameBase = await read('src/views/components/element/FrameElement/BaseFrameElement.vue')
+const operate = await read('src/views/Editor/Canvas/Operate/index.vue')
+const canvasTool = await read('src/views/Editor/CanvasTool/index2.vue')
+const canvas = await read('src/views/Editor/Canvas/index.vue')
+const toolbars = await Promise.all([
+  read('src/components/CollapsibleToolbar/index.vue'),
+  read('src/components/CollapsibleToolbar/index2.vue'),
+  read('src/components/CollapsibleToolbar/index22.vue'),
+])
+
+// 编辑态与只读/学生态都必须把 772 路由到文章预览
+assert.match(frameEditor, /ArticleReadingPreview/)
+assert.match(frameEditor, /isArticleReadingTool/)
+assert.match(frameBase, /ArticleReadingPreview/)
+assert.match(frameBase, /isArticleReadingTool/)
+assert.match(frameBase, /ARTICLE_READING_TOOL_TYPE/)
+
+// 锁定、重置、滚轮放行
+assert.match(operate, /isArticleReadingTool/)
+assert.match(canvasTool, /ARTICLE_READING_TOOL_TYPE/)
+assert.match(canvasTool, /articleStore\.requestResetPreview/)
+assert.match(canvas, /article-reading-preview/)
+
+// 三个工具栏名称映射
+for (const toolbar of toolbars) {
+  assert.match(toolbar, /ARTICLE_READING_TOOL_TYPE/)
+  assert.match(toolbar, /ssArticleReadingTool/)
+}
+
+// 编号只能来自命名常量:代码里不得出现 772 字面量(注释中说明编号是允许的)
+function stripComments(source) {
+  return source
+    .replace(/<!--[\s\S]*?-->/g, '')
+    .replace(/\/\*[\s\S]*?\*\//g, '')
+    .replace(/(^|[^:])\/\/.*$/gm, '$1')
+}
+
+const codeFiles = {
+  frameEditor, frameBase, operate, canvasTool, canvas,
+  toolbar0: toolbars[0], toolbar1: toolbars[1], toolbar2: toolbars[2],
+}
+for (const [name, source] of Object.entries(codeFiles)) {
+  assert.doesNotMatch(
+    stripComments(source),
+    /\b772\b/,
+    `${name} must reference ARTICLE_READING_TOOL_TYPE, not the literal 772`,
+  )
+}
+
+// 话题讨论与句子跟读的既有编号不得被改写
+assert.match(frameBase, /77: 'ssEnglishSpeakingTool'/)
+assert.match(frameBase, /81: 'ssH5Page'/)
+
+// 文章分支不得回落到话题讨论组件
+const articleBranch = frameEditor.slice(frameEditor.indexOf('ArticleReadingPreview'))
+assert.doesNotMatch(articleBranch.slice(0, 200), /TopicDiscussionPreview/)
+
+// 老师端 Student runtime 按工具类型分流
+const studentView = await read('src/views/Student/index.vue')
+const articleClassComposable = await read(
+  'src/views/Student/components/ArticleReadingClassPanel/useArticleClassSummary.ts',
+)
+
+assert.match(studentView, /ArticleReadingClassPanel/)
+assert.match(studentView, /ARTICLE_READING_TOOL_TYPE/)
+assert.match(studentView, /TOPIC_DISCUSSION_TOOL_TYPE/)
+assert.match(articleClassComposable, /listArticleSessionsByConfig/)
+assert.match(articleClassComposable, /generateArticleClassSummary/)
+
+// 作业记录必须带真实工具号,不能写死 77
+assert.match(studentView, /atool: String\(activityToolType\)/)
+assert.doesNotMatch(studentView, /atool: '77'/)
+
+// 通用结果弹窗/提交按钮对文章与话题一视同仁地抑制
+assert.doesNotMatch(studentView, /currentSlideToolType !== 77/)
+assert.match(studentView, /articleReadingPanelRef\.value\?\.scheduleRefetch/)
+
+// 三个语言包都要有工具名与班级面板文案
+for (const loc of ['cn', 'en', 'hk']) {
+  const pack = JSON.parse(await read(`src/views/lang/${loc}.json`))
+  for (const key of [
+    'ssArticleReadingTool', 'ssArticleReading', 'ssArticleClassSummary',
+    'ssArticleFilterCompleted', 'ssArticleFilterReading',
+  ]) {
+    assert.ok(pack[key], `${loc}.json is missing ${key}`)
+  }
+}
+
+// ---- 编辑态老师预览不进统计(spec §4.5):三道门都要在 ArticleReadingPreview 里 ----
+const articlePreview = await read('src/views/Editor/EnglishSpeaking/preview/ArticleReadingPreview.vue')
+
+// 门 1:runtime 判定不能只看 userid —— 编辑器 URL 里也有 userid。
+// 身份判据用项目既有的 type(Student/index.vue:625:老师端 1 / 学生端 2)。
+assert.match(articlePreview, /isStudentRuntime/)
+assert.match(articlePreview, /params\.get\('type'\)/)
+assert.match(articlePreview, /role === '2'/)
+
+// 门 2:编辑态把 null 传给引擎,不是空串
+assert.match(articlePreview, /sessionUserId/)
+
+// 门 3:resume 探针与进度广播只在学生态跑
+assert.match(articlePreview, /if \(!isStudentRuntime\.value\) return/)
+
+// 反向:不得再出现「只要有 userid 就当学生」的旧判据
+assert.doesNotMatch(articlePreview, /startDisabled = computed\(\(\) => !hasArticle\.value \|\| !runtimeUserId\.value\)/)
+
+// 反向:不得拿 mode 当身份 —— mode=student 是视图,老师上课时也在这个视图里,
+// 用它会把上课页的老师当成学生(编辑态那个 bug 换个地方复发),还得再去和
+// App.vue 的 localStorage viewMode 对账。type 是身份,与渲染哪个视图无关。
+assert.doesNotMatch(articlePreview, /'student'/)
+assert.doesNotMatch(articlePreview, /currentViewMode/)
+assert.doesNotMatch(await read('src/App.vue'), /currentViewMode/)
+
+// ---- 报告的词级形状必须跟住后端契约(spec §6):段落下面是**扁平的 words**。
+// 后端 c62d35a「flatten scoring/report to paragraph→words」把句层删了,前端的类型和
+// 取值路径没跟着改,于是任何一张已完成的段落卡片一展开就 `paragraph.sentences` 是
+// undefined、`.flatMap` 抛异常。类型说了谎,所以 type-check 也发现不了 —— 只能在这里钉。
+const articleTypes = await read('src/types/articleReading.ts')
+assert.match(articleTypes, /words: ArticleWordResult\[\]/)
+assert.doesNotMatch(articleTypes, /sentences/)
+assert.doesNotMatch(articleTypes, /ArticleSentenceResult/)
+
+const paragraphCard = await read('src/views/Editor/EnglishSpeaking/preview/ArticleParagraphCard.vue')
+assert.match(paragraphCard, /props\.paragraph\.words/)
+assert.doesNotMatch(paragraphCard, /flattenParagraphWords/)
+assert.doesNotMatch(
+  await read('src/views/Editor/EnglishSpeaking/preview/articleReadingModel.ts'),
+  /sentences/,
+)
+
+console.log('article reading routing: all assertions passed')

+ 114 - 0
scripts/test-article-reading-stream.mjs

@@ -0,0 +1,114 @@
+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/articleReadingStream.ts', import.meta.url)
+let source = await readFile(sourceUrl, 'utf8')
+
+source = source.replace(
+  "import { buildArticleWsUrl } from './speakingApiConfig'",
+  "const buildArticleWsUrl = path => `wss://example.test/api/speaking/article${path}`",
+)
+
+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 mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+
+// ---- 1. 正常时序:start → PCM → stop → ack ----
+// chunk 先于 open 抵达,必须排队而不是丢弃(录音器早于 socket 就绪是常态)
+const stream = mod.openArticleParagraphStream({
+  sessionId: 's1',
+  paragraphIdx: 2,
+  sampleRate: 48000,
+})
+const firstChunk = new Uint8Array([1, 2, 3, 4]).buffer
+stream.pushChunk(firstChunk)
+const ackPromise = stream.stop()
+
+const ws = FakeWebSocket.instances[0]
+assert.equal(ws.url, 'wss://example.test/api/speaking/article/session/s1/paragraph/2/stream')
+ws.readyState = FakeWebSocket.OPEN
+ws.onopen()
+
+assert.deepEqual(JSON.parse(ws.sent[0]), {
+  type: 'start', sampleRate: 48000, bits: 16, channels: 1,
+})
+assert.equal(ws.sent[1], firstChunk)
+assert.deepEqual(JSON.parse(ws.sent[2]), { type: 'stop' })
+
+ws.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
+assert.deepEqual(await ackPromise, {
+  type: 'ack', status: 'generating', attempt: 1, truncated: false,
+})
+
+// ---- 2. error frame 以其 message reject ----
+const errStream = mod.openArticleParagraphStream({ sessionId: 's2', paragraphIdx: 0, sampleRate: 16000 })
+const errAck = errStream.stop()
+const errWs = FakeWebSocket.instances[1]
+errWs.readyState = FakeWebSocket.OPEN
+errWs.onopen()
+errWs.onmessage({ data: JSON.stringify({ type: 'error', message: 'assessment failed' }) })
+await assert.rejects(errAck, /assessment failed/)
+
+// ---- 3. abort() 关闭连接并 reject 未决的 stop ----
+const abortStream = mod.openArticleParagraphStream({ sessionId: 's3', paragraphIdx: 1, sampleRate: 16000 })
+const abortWs = FakeWebSocket.instances[2]
+abortWs.readyState = FakeWebSocket.OPEN
+abortWs.onopen()
+const abortAck = abortStream.stop()
+abortStream.abort('user left the paragraph')
+assert.equal(abortWs.readyState, FakeWebSocket.CLOSED)
+await assert.rejects(abortAck, /user left the paragraph/)
+
+// ---- 4. 连接在 ack 之前关闭必须 reject,不能静默挂起 ----
+const deadStream = mod.openArticleParagraphStream({ sessionId: 's4', paragraphIdx: 0, sampleRate: 16000 })
+const deadWs = FakeWebSocket.instances[3]
+deadWs.readyState = FakeWebSocket.OPEN
+deadWs.onopen()
+const deadAck = deadStream.stop()
+deadWs.close()
+await assert.rejects(deadAck, /closed before acknowledgement/)
+
+// ---- 5. stop 之后再推 PCM 是编程错误 ----
+const lateStream = mod.openArticleParagraphStream({ sessionId: 's5', paragraphIdx: 0, sampleRate: 16000 })
+const lateWs = FakeWebSocket.instances[4]
+lateWs.readyState = FakeWebSocket.OPEN
+lateWs.onopen()
+const lateAck = lateStream.stop()
+assert.throws(() => lateStream.pushChunk(new Uint8Array([9]).buffer), /Cannot push PCM after stop/)
+lateWs.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
+await lateAck

+ 116 - 0
scripts/test-article-recorder-sample-rate.mjs

@@ -0,0 +1,116 @@
+/**
+ * useAudioRecorder 的采样率协商。
+ *
+ * 钉的是一件事:**上报的永远是实际拿到的采样率,不是我们要的那个**。
+ * 这条不变量一破,后端就会按 16kHz 去解 48kHz 的数据 —— 回放慢三倍、评分全错,
+ * 而整条链路没有任何一层会报错。三种浏览器行为都要走一遍。
+ */
+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/useAudioRecorder.ts',
+  import.meta.url,
+)
+let source = await readFile(sourceUrl, 'utf8')
+
+// Vue runtime → 本地 shim(同 test-article-reading-engine.mjs 的做法)
+source = source.replace(
+  "import { ref, onUnmounted } from 'vue'",
+  `const ref = v => ({ value: v })
+   const onUnmounted = () => {}`,
+)
+
+const compiled = ts.transpileModule(source, {
+  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
+}).outputText
+
+const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
+
+// ---- 浏览器环境替身 ----
+
+const HARDWARE_RATE = 48000
+
+function installEnvironment({ audioContextFactory }) {
+  const track = {
+    label: 'fake mic', enabled: true, muted: false, readyState: 'live',
+    getSettings: () => ({}), stop() { this.readyState = 'ended' },
+  }
+  // Node 22 的 globalThis.navigator 只有 getter,赋值会抛 —— 用 defineProperty 盖掉
+  Object.defineProperty(globalThis, 'navigator', {
+    configurable: true,
+    value: {
+      mediaDevices: { getUserMedia: async () => ({ getTracks: () => [track], getAudioTracks: () => [track] }) },
+      permissions: { query: async () => { throw new Error('unsupported') } },
+    },
+  })
+  globalThis.AudioContext = audioContextFactory
+  globalThis.AudioWorkletNode = class {
+    constructor() { this.port = { onmessage: null, postMessage() {} } }
+    connect() {}
+    disconnect() {}
+  }
+  globalThis.DOMException = globalThis.DOMException ?? class extends Error {}
+}
+
+/** 造一个 AudioContext 类,`resolveRate` 决定它对 sampleRate 提示的反应。 */
+function makeAudioContextClass(resolveRate) {
+  return class FakeAudioContext {
+    static requested = []
+    constructor(options) {
+      FakeAudioContext.requested.push(options?.sampleRate ?? null)
+      this.sampleRate = resolveRate(options?.sampleRate)   // 可以抛
+      this.state = 'running'
+      this.destination = {}
+      this.audioWorklet = { addModule: async () => {} }
+    }
+    createMediaStreamSource() { return { connect() {} } }
+    createAnalyser() {
+      return { fftSize: 0, frequencyBinCount: 256, connect() {}, getByteFrequencyData() {} }
+    }
+    async resume() { this.state = 'running' }
+    async close() { this.state = 'closed' }
+  }
+}
+
+async function startAndReadRate(AudioContextClass) {
+  installEnvironment({ audioContextFactory: AudioContextClass })
+  const recorder = mod.useAudioRecorder()
+  await recorder.startRecording()
+  const rate = recorder.sampleRate.value
+  recorder.cleanup()
+  return rate
+}
+
+// ---- 1. 浏览器支持:拿到 16kHz ----
+{
+  const Ctx = makeAudioContextClass(hint => hint ?? HARDWARE_RATE)
+  const rate = await startAndReadRate(Ctx)
+  assert.equal(rate, 16000, '支持自定义采样率时应该拿到 16kHz')
+  assert.deepEqual(Ctx.requested, [16000], '应该只构造一次,并且带着 16000 的提示')
+}
+
+// ---- 2. 浏览器拒绝(旧 iOS Safari):退回硬件默认,录音照常 ----
+{
+  const Ctx = makeAudioContextClass(hint => {
+    if (hint) throw new Error('NotSupportedError: sample rate not supported')
+    return HARDWARE_RATE
+  })
+  const rate = await startAndReadRate(Ctx)
+  assert.equal(rate, HARDWARE_RATE, '构造抛出时必须退回硬件默认,而不是整个录音失败')
+  assert.deepEqual(Ctx.requested, [16000, null], '先试 16000,抛了再试不带提示的')
+}
+
+// ---- 3. 浏览器**默默无视**提示:上报真值,不是我们要的值 ----
+// 1 是「照办」、2 是「明着拒绝」,这条是第三种行为:收下提示、返回硬件值、不抛。
+// 前两条都覆盖不到它,而它正是最坏的那种失败 —— 没有异常、没有日志,只是后端
+// 从此按 16kHz 去解 48kHz 的数据。
+{
+  const Ctx = makeAudioContextClass(() => HARDWARE_RATE)   // 提示照收,返回值不变
+  const rate = await startAndReadRate(Ctx)
+  assert.equal(rate, HARDWARE_RATE, '浏览器无视提示时必须上报实际采样率')
+  assert.notEqual(rate, 16000)
+}
+
+console.log('✓ test-article-recorder-sample-rate: 3 cases passed')

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

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

@@ -0,0 +1,79 @@
+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',
+)
+
+assert.equal(
+  defaults.SPEAKING_ARTICLE_API_BASE_URL,
+  'https://ppt-english-speaking-api.cocorobo.cn/api/speaking/article',
+)
+
+assert.equal(
+  defaults.buildArticleWsUrl('/session/s1/paragraph/0/stream'),
+  'wss://ppt-english-speaking-api.cocorobo.cn/api/speaking/article/session/s1/paragraph/0/stream',
+)
+
+assert.equal(
+  custom.SPEAKING_ARTICLE_API_BASE_URL,
+  'https://example.com/api/speaking/article',
+)

+ 28 - 13
src/App.vue

@@ -18,6 +18,12 @@
         :userid="urlParams.userid"
         key="editor3"
       />
+      <EditorEn
+        v-else-if="viewMode === 'editor-en' && _isPC && !screening"
+        :courseid="urlParams.courseid"
+        :userid="urlParams.userid"
+        key="editor-en"
+      />
       <Student
         v-else-if="viewMode === 'student'"
         :courseid="urlParams.courseid"
@@ -54,6 +60,7 @@ import api from '@/services'
 import Editor from './views/Editor/index.vue'
 import Editor2 from './views/Editor/index2.vue'
 import Editor3 from './views/Editor/index3.vue'
+import EditorEn from './views/Editor/indexEn.vue'
 import Screen from './views/Screen/index.vue'
 import Mobile from './views/Mobile/index.vue'
 import Student from './views/Student/index.vue'
@@ -86,6 +93,11 @@ const getInitialViewMode = () => {
   if (modeFromUrl === 'editor3') {
     return 'editor3'
   }
+  
+  if (modeFromUrl === 'editor-en') {
+    return 'editor-en'
+  }
+
   // 检查localStorage
   const modeFromStorage = localStorage.getItem('viewMode')
   if (modeFromStorage) {
@@ -154,15 +166,15 @@ onMounted(async () => {
 })
 
 // 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
-window.addEventListener('beforeunload', () => {
-  const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
-  const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
+// window.addEventListener('beforeunload', () => {
+//   const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
+//   const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
 
-  discardedDBList.push(databaseId.value)
+//   discardedDBList.push(databaseId.value)
 
-  const newDiscardedDB = JSON.stringify(discardedDBList)
-  localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
-})
+//   const newDiscardedDB = JSON.stringify(discardedDBList)
+//   localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
+// })
 </script>
 
 <style lang="scss">
@@ -188,24 +200,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/kwlBanner.jpg


BIN
src/assets/img/tool_file.png


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 {

+ 909 - 0
src/components/CollapsibleToolbar/cocolearnCom/KwlDetail.vue

@@ -0,0 +1,909 @@
+<template>
+  <div class="kwl-detail">
+    <!-- 头部导航 -->
+    <div class="detail-header">
+      <div class="back-btn" @click="$emit('back')">
+        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <path d="M15 18l-6-6 6-6" />
+        </svg>
+      </div>
+      <div class="detail-title">{{ lang.ssKwlTitle }}</div>
+    </div>
+
+    <!-- Banner 区域 -->
+    <div class="detail-banner">
+      <img :src="kwl" :alt="lang.ssKwlTitle" />
+      <div class="tool-overly"></div>
+        <div class="tool-icon-badge">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M9 18h6M10 22h4M12 2a7 7 0 00-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0012 2z"></path>
+          </svg>
+        </div>
+    </div>
+
+    <!-- 描述与标签 -->
+    <div class="detail-desc-row">
+      <div class="detail-desc-text">{{ lang.ssKwlDesc }}</div>
+      <div class="help-icon" @mouseenter="showHelp = true" @mouseleave="showHelp = false">
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <circle cx="12" cy="12" r="10" />
+          <path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" />
+          <line x1="12" y1="17" x2="12.01" y2="17" />
+        </svg>
+        <!-- 帮助气泡 -->
+        <div v-if="showHelp" class="help-tooltip">
+          <div class="help-title">{{ lang.ssKwlUsageScenario }}</div>
+          <div class="help-text">{{ lang.ssKwlScenarioText }}</div>
+          <div class="help-title">{{ lang.ssKwlSteps }}</div>
+          <div class="help-list">
+            <div>{{ lang.ssKwlStep1 }}</div>
+            <div>{{ lang.ssKwlStep2 }}</div>
+            <div>{{ lang.ssKwlStep3 }}</div>
+            <div>{{ lang.ssKwlStep4 }}</div>
+          </div>
+          <div class="help-source">{{ lang.ssKwlSource }}</div>
+          <div class="help-purpose">{{ lang.ssKwlPurpose }}</div>
+        </div>
+      </div>
+    </div>
+    <div class="detail-tags">
+      <span class="detail-tag">{{ lang.ssKwlGradeRange }}</span>
+    </div>
+
+    <!-- 工具配置区 -->
+    <div class="config-section">
+      <div class="config-title">{{ lang.ssKwlToolConfig }}</div>
+      <div class="config-subtitle">{{ lang.ssKwlConfigSubtitle }}</div>
+    </div>
+
+    <!-- 情境/任务说明 -->
+    <div class="form-group">
+      <div class="form-label">{{ lang.ssKwlTaskLabel }}</div>
+      <div class="form-textarea">
+        <textarea v-model="formData.taskDescription"
+          :placeholder="lang.ssKwlTaskPlaceholder"></textarea>
+      </div>
+    </div>
+
+    <!-- 添加配图/视频 -->
+    <div class="form-group">
+      <div class="upload-area" @click="triggerFileSelect">
+        <svg width="20" height="20" 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" />
+          <circle cx="8.5" cy="8.5" r="1.5" />
+          <polyline points="21 15 16 10 5 21" />
+        </svg>
+        <span>{{ lang.ssKwlUploadMedia }}</span>
+      </div>
+      <input
+        ref="fileInputRef"
+        type="file"
+        accept="image/*,video/*"
+        multiple
+        style="display: none"
+        @change="handleFileChange"
+      />
+      <!-- 已上传预览列表 -->
+      <div v-if="uploadedFiles.length > 0" class="upload-preview-list">
+        <div
+          v-for="(file, index) in uploadedFiles"
+          :key="index"
+          class="upload-preview-item"
+        >
+          <img v-if="file.type.startsWith('image/')" :src="file.url" :alt="file.name" @click="openPreview(index)" />
+          <video v-else :src="file.url" muted @click="openPreview(index)"></video>
+          <div class="preview-delete" @click="removeFile(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>
+          </div>
+          <div v-if="file.uploading" class="preview-loading">
+            <div class="loading-spinner"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 默认展开操作提示 -->
+    <div class="form-row">
+      <div class="form-text-block">
+        <div class="form-label">{{ lang.ssKwlShowHowTo }}</div>
+        <div class="form-sublabel">{{ lang.ssKwlShowHowToDesc }}</div>
+      </div>
+      <div class="toggle-switch" :class="{ active: formData.showHowTo }"
+        @click="formData.showHowTo = !formData.showHowTo">
+        <div class="toggle-thumb"></div>
+      </div>
+    </div>
+
+    <!-- 高级配置 -->
+    <div class="advanced-section">
+      <div class="advanced-header" @click="showAdvanced = !showAdvanced">
+        <span class="advanced-title">{{ lang.ssKwlAdvanced }}</span>
+        <svg class="advanced-arrow" :class="{ expanded: showAdvanced }" width="16" height="16" viewBox="0 0 24 24"
+          fill="none" stroke="currentColor" stroke-width="2">
+          <polyline points="6 9 12 15 18 9" />
+        </svg>
+      </div>
+
+      <div v-if="showAdvanced" class="advanced-body">
+        <div class="form-group">
+          <div class="form-label">{{ lang.ssKwlKPrompt }}</div>
+          <div class="form-input">
+            <input v-model="formData.kplaceholder" type="text" :placeholder="lang.ssKwlKDefault" />
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="form-label">{{ lang.ssKwlWPrompt }}</div>
+          <div class="form-input">
+            <input v-model="formData.wplaceholder" type="text" :placeholder="lang.ssKwlWDefault" />
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="form-label">{{ lang.ssKwlLPrompt }}</div>
+          <div class="form-input">
+            <input v-model="formData.lplaceholder" type="text" :placeholder="lang.ssKwlLDefault" />
+          </div>
+        </div>
+
+        <div class="form-row">
+          <div class="form-text-block">
+            <div class="form-label">{{ lang.ssKwlShowReflection }}</div>
+          </div>
+          <div class="toggle-switch" :class="{ active: formData.isShowReflection }"
+            @click="formData.isShowReflection = !formData.isShowReflection">
+            <div class="toggle-thumb"></div>
+          </div>
+        </div>
+
+        <!-- <div class="form-group">
+          <div class="form-row-with-value">
+            <div class="form-label">{{ lang.ssKwlMinEntries }}</div>
+            <div class="range-value">{{ formData.minEntries }}</div>
+          </div>
+          <div class="slider-wrapper">
+            <input v-model.number="formData.minEntries" type="range" min="0" max="10" step="1" class="range-slider" />
+          </div>
+        </div> -->
+      </div>
+    </div>
+
+    <!-- 添加到幻灯片按钮 -->
+    <div class="footer-btn">
+      <div class="add-btn" @click="handleAddToSlide">{{ lang.ssKwlAddToSlide }}</div>
+    </div>
+
+    <!-- 图片预览:复用项目已有组件 -->
+    <PreviewImageTool ref="previewImageToolRef" />
+
+    <!-- 视频预览弹窗 -->
+    <Teleport to="body">
+      <div v-if="videoPreviewVisible" class="kwl-preview-modal" @click.self="closeVideoPreview">
+        <div class="kwl-preview-close" @click="closeVideoPreview">
+          <svg width="24" height="24" 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>
+        </div>
+        <video
+          :src="videoPreviewUrl"
+          class="kwl-preview-video"
+          controls
+          autoplay
+        ></video>
+      </div>
+    </Teleport>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import kwl from '@/assets/img/kwlBanner.jpg'
+import useImport from '@/hooks/useImport'
+import { lang } from '@/main'
+
+const { uploadFileToS3 } = useImport()
+
+interface UploadedFile {
+  name: string
+  url: string
+  type: string
+  uploading: boolean
+}
+
+const emit = defineEmits<{
+  (e: 'back'): void
+  (e: 'add', data: any): void
+}>()
+
+const showHelp = ref(false)
+const showAdvanced = ref(false)
+
+const fileInputRef = ref<HTMLInputElement | null>(null)
+const uploadedFiles = ref<UploadedFile[]>([])
+
+const triggerFileSelect = () => {
+  fileInputRef.value?.click()
+}
+
+const handleFileChange = async (event: Event) => {
+  const target = event.target as HTMLInputElement
+  const files = target.files
+  if (!files || files.length === 0) return
+
+  const validTypes = ['image/', 'video/']
+  const validFiles: File[] = []
+
+  for (let i = 0; i < files.length; i++) {
+    const file = files[i]
+    if (validTypes.some(type => file.type.startsWith(type))) {
+      validFiles.push(file)
+    }
+  }
+
+  for (const file of validFiles) {
+    const item: UploadedFile = {
+      name: file.name,
+      url: '',
+      type: file.type,
+      uploading: true
+    }
+    uploadedFiles.value.push(item)
+    // 通过索引获取响应式对象进行修改
+    const idx = uploadedFiles.value.length - 1
+
+    try {
+      const url = await uploadFileToS3(file)
+      uploadedFiles.value[idx].url = url
+    }
+    catch (err) {
+      console.error(lang.ssKwlUploadFail + ':', file.name, err)
+      uploadedFiles.value.splice(idx, 1)
+    }
+    finally {
+      if (uploadedFiles.value[idx]) {
+        uploadedFiles.value[idx].uploading = false
+      }
+    }
+  }
+
+  // 清空 input value,允许重复选择相同文件
+  if (fileInputRef.value) {
+    fileInputRef.value.value = ''
+  }
+}
+
+const removeFile = (index: number) => {
+  uploadedFiles.value.splice(index, 1)
+}
+
+// 预览相关:图片使用项目已有 previewImageTool 组件
+import PreviewImageTool from '@/views/components/tool/previewImageTool.vue'
+
+const previewImageToolRef = ref<InstanceType<typeof PreviewImageTool> | null>(null)
+const videoPreviewVisible = ref(false)
+const videoPreviewUrl = ref('')
+
+const openPreview = (index: number) => {
+  const file = uploadedFiles.value[index]
+  if (!file || file.uploading) return
+
+  if (file.type.startsWith('image/')) {
+    // 使用项目已有的图片预览组件
+    previewImageToolRef.value?.previewImage(file.url)
+  }
+  else {
+    // 视频预览:使用简单弹窗
+    videoPreviewUrl.value = file.url
+    videoPreviewVisible.value = true
+    document.body.style.overflow = 'hidden'
+  }
+}
+
+const closeVideoPreview = () => {
+  videoPreviewVisible.value = false
+  document.body.style.overflow = ''
+}
+
+const formData = reactive({
+  know: [],
+  want: [],
+  learned: [],
+  reflection: '',
+  taskDescription: '',
+  taskUrl: [] as string[],
+  showHowTo: true,
+  kplaceholder: lang.ssKwlKDefault,
+  wplaceholder: lang.ssKwlWDefault,
+  lplaceholder: lang.ssKwlLDefault,
+  isShowReflection: true
+})
+
+const handleAddToSlide = () => {
+  formData.taskUrl = uploadedFiles.value.map(f => f.url).filter(Boolean)
+  emit('add', { ...formData })
+}
+</script>
+
+<style lang="scss" scoped>
+.kwl-detail {
+  display: flex;
+  flex-direction: column;
+  height: calc(100% - 75px);
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0 0 20px;
+}
+
+/* 头部 */
+.detail-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 0 14px;
+  position: sticky;
+  top: 0;
+  background: #fff;
+  z-index: 10;
+  margin-bottom: 20px;
+
+  .back-btn {
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 8px;
+    cursor: pointer;
+    color: #6b7280;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: #f3f4f6;
+      color: #374151;
+    }
+  }
+
+  .detail-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+}
+
+/* Banner */
+.detail-banner {
+  margin: 0 14px 14px;
+  border-radius: 12px;
+  overflow: hidden;
+  // width: 100%;
+  height: 140px;
+  position: relative;
+  box-sizing: border-box;
+
+  .tool-overly {
+    position: absolute;
+    inset: 0;
+    background: rgba(37, 99, 235, 0.15);
+    opacity: .45;
+    pointer-events: none;
+  }
+
+
+  .tool-icon-badge {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    width: 32px;
+    height: 32px;
+    border-radius: 8px;
+    background: rgba(255, 255, 255, 0.95);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
+
+    svg {
+      width: 18px;
+      height: 18px;
+      // color: #FF9300;
+    }
+  }
+
+  .tool-icon-badge svg {
+    color: #2563eb;
+  }
+
+  .tool-overly {
+    background-color: #2563eb;
+  }
+
+  img {
+    width: 100%;
+    height: 140px;
+    object-fit: cover;
+    display: block;
+  }
+}
+
+/* 描述 */
+.detail-desc-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  padding: 0 14px;
+
+  .detail-desc-text {
+    flex: 1;
+    font-size: 13px;
+    color: #4b5563;
+    line-height: 1.6;
+  }
+
+  .help-icon {
+    position: relative;
+    color: #9ca3af;
+    cursor: pointer;
+    flex-shrink: 0;
+    margin-top: 2px;
+
+    &:hover {
+      color: #FF9300;
+    }
+
+    .help-tooltip {
+      position: absolute;
+      top: 100%;
+      right: 0;
+      margin-top: 8px;
+      width: 240px;
+      background: #fff;
+      border: 1px solid #e5e7eb;
+      border-radius: 10px;
+      padding: 14px;
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+      z-index: 100;
+
+      .help-title {
+        font-size: 13px;
+        font-weight: 600;
+        color: #1f2937;
+        margin-bottom: 4px;
+        margin-top: 8px;
+
+        &:first-child {
+          margin-top: 0;
+        }
+      }
+
+      .help-text {
+        font-size: 12px;
+        color: #6b7280;
+        margin-bottom: 4px;
+      }
+
+      .help-list {
+        font-size: 12px;
+        color: #6b7280;
+        line-height: 1.8;
+      }
+
+      .help-source,
+      .help-purpose {
+        font-size: 11px;
+        color: #9ca3af;
+        margin-top: 8px;
+        line-height: 1.5;
+      }
+    }
+  }
+}
+
+.detail-tags {
+  display: flex;
+  gap: 6px;
+  padding: 10px 14px 0;
+
+  .detail-tag {
+    display: inline-block;
+    padding: 3px 10px;
+    font-size: 11px;
+    color: #3b82f6;
+    background: #eff6ff;
+    border-radius: 4px;
+    font-weight: 500;
+  }
+}
+
+/* 配置区 */
+.config-section {
+  padding: 20px 14px 12px;
+
+  .config-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+    margin-bottom: 6px;
+  }
+
+  .config-subtitle {
+    font-size: 12px;
+    color: #6b7280;
+    line-height: 1.6;
+  }
+}
+
+/* 表单 */
+.form-group {
+  padding: 0 14px;
+  margin-bottom: 16px;
+
+  .form-label {
+    font-size: 13px;
+    font-weight: 500;
+    color: #374151;
+    margin-bottom: 8px;
+  }
+
+  .form-textarea {
+    textarea {
+      width: 100%;
+      min-height: 80px;
+      padding: 10px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 10px;
+      font-size: 13px;
+      color: #374151;
+      resize: none;
+      background: #fff;
+      outline: none;
+      transition: border-color 0.2s ease;
+      box-sizing: border-box;
+
+      &::placeholder {
+        color: #9ca3af;
+      }
+
+      &:focus {
+        border-color: #FF9300;
+      }
+    }
+  }
+
+  .form-input {
+    input {
+      width: 100%;
+      padding: 10px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 10px;
+      font-size: 13px;
+      color: #374151;
+      background: #fff;
+      outline: none;
+      transition: border-color 0.2s ease;
+      box-sizing: border-box;
+
+      &::placeholder {
+        color: #9ca3af;
+      }
+
+      &:focus {
+        border-color: #FF9300;
+      }
+    }
+  }
+}
+
+/* 上传区 */
+.upload-area {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 14px;
+  border: 1px dashed #d1d5db;
+  border-radius: 10px;
+  color: #6b7280;
+  font-size: 13px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+
+  svg {
+    color: #9ca3af;
+  }
+
+  &:hover {
+    border-color: #FF9300;
+    color: #FF9300;
+
+    svg {
+      color: #FF9300;
+    }
+  }
+}
+
+/* 上传预览列表 */
+.upload-preview-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 12px;
+}
+
+.upload-preview-item {
+  position: relative;
+  width: 80px;
+  height: 80px;
+  border-radius: 8px;
+  overflow: hidden;
+  background: #f3f4f6;
+  border: 1px solid #e5e7eb;
+
+  img, video {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    display: block;
+  }
+
+  .preview-delete {
+    position: absolute;
+    top: 4px;
+    right: 4px;
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    background: rgba(0, 0, 0, 0.55);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    cursor: pointer;
+    transition: background 0.2s ease;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.75);
+    }
+  }
+
+  .preview-loading {
+    position: absolute;
+    inset: 0;
+    background: rgba(255, 255, 255, 1);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .loading-spinner {
+    width: 24px;
+    height: 24px;
+    border: 3px solid #e5e7eb;
+    border-top-color: #FF9300;
+    border-radius: 50%;
+    animation: kwlSpin 0.8s linear infinite;
+  }
+}
+
+@keyframes kwlSpin {
+  to { transform: rotate(360deg); }
+}
+
+/* 预览弹窗 */
+.kwl-preview-modal {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.85);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+  cursor: zoom-out;
+}
+
+.kwl-preview-close {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.15);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  cursor: pointer;
+  transition: background 0.2s ease;
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.25);
+  }
+}
+
+.kwl-preview-video {
+  max-width: 90vw;
+  max-height: 85vh;
+  object-fit: contain;
+  border-radius: 8px;
+}
+
+/* 表单项行 */
+.form-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 14px;
+  margin-bottom: 16px;
+
+  .form-text-block {
+    .form-label {
+      font-size: 13px;
+      font-weight: 500;
+      color: #374151;
+    }
+
+    .form-sublabel {
+      font-size: 12px;
+      color: #9ca3af;
+      margin-top: 2px;
+    }
+  }
+}
+
+.form-row-with-value {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .form-label {
+    font-size: 13px;
+    font-weight: 500;
+    color: #374151;
+  }
+
+  .range-value {
+    font-size: 13px;
+    color: #FF9300;
+    font-weight: 600;
+  }
+}
+
+/* 开关 */
+.toggle-switch {
+  width: 44px;
+  height: 24px;
+  border-radius: 12px;
+  background: #e5e7eb;
+  position: relative;
+  cursor: pointer;
+  transition: background 0.25s ease;
+  flex-shrink: 0;
+
+  .toggle-thumb {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    background: #fff;
+    position: absolute;
+    top: 2px;
+    left: 2px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+    transition: transform 0.25s ease;
+  }
+
+  &.active {
+    background: #FF9300;
+
+    .toggle-thumb {
+      transform: translateX(20px);
+    }
+  }
+}
+
+/* 高级配置 */
+.advanced-section {
+  padding: 0 14px;
+  margin-bottom: 16px;
+
+  .advanced-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 0;
+    cursor: pointer;
+
+    .advanced-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #374151;
+    }
+
+    .advanced-arrow {
+      color: #9ca3af;
+      transition: transform 0.2s ease;
+
+      &.expanded {
+        transform: rotate(180deg);
+      }
+    }
+  }
+
+  .advanced-body {
+    display: flex;
+    flex-direction: column;
+    padding-top: 8px;
+    border-top: 1px solid #f3f4f6;
+
+    .form-group {
+      padding: 0;
+    }
+  }
+}
+
+/* 滑块 */
+.slider-wrapper {
+  padding: 8px 0;
+
+  .range-slider {
+    -webkit-appearance: none;
+    appearance: none;
+    width: 100%;
+    height: 4px;
+    border-radius: 2px;
+    background: #e5e7eb;
+    outline: none;
+
+    &::-webkit-slider-thumb {
+      -webkit-appearance: none;
+      appearance: none;
+      width: 18px;
+      height: 18px;
+      border-radius: 50%;
+      background: #FF9300;
+      cursor: pointer;
+      box-shadow: 0 1px 3px rgba(255, 147, 0, 0.4);
+    }
+
+    &::-moz-range-thumb {
+      width: 18px;
+      height: 18px;
+      border-radius: 50%;
+      background: #FF9300;
+      cursor: pointer;
+      border: none;
+      box-shadow: 0 1px 3px rgba(255, 147, 0, 0.4);
+    }
+  }
+}
+
+/* 底部按钮 */
+.footer-btn {
+  padding: 8px 14px 0;
+
+  .add-btn {
+    width: 100%;
+    padding: 14px;
+    background: #FF9300;
+    color: #fff;
+    font-size: 15px;
+    font-weight: 600;
+    text-align: center;
+    border-radius: 12px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: #f57c00;
+      transform: translateY(-1px);
+      box-shadow: 0 4px 12px rgba(255, 147, 0, 0.3);
+    }
+
+    &:active {
+      transform: translateY(0);
+    }
+  }
+}
+</style>

ファイルの差分が大きいため隠しています
+ 752 - 149
src/components/CollapsibleToolbar/componets/aiChat.vue


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

@@ -0,0 +1,181 @@
+<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
+  tx: string
+}>()
+
+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${props.tx}`
+})
+
+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>

+ 11 - 2
src/components/CollapsibleToolbar/index.vue

@@ -131,6 +131,7 @@ import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import { useSlidesStore } from '@/store'
 import { lang } from '@/main'
+import { ARTICLE_READING_TOOL_TYPE } from '@/configs/englishSpeakingTools'
 
 interface ContentItem {
   tool?: number
@@ -283,9 +284,16 @@ 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,
+    [ARTICLE_READING_TOOL_TYPE]: lang.ssArticleReadingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
+    84: lang.ssFile,
+    831: lang.ssKwlTitle,
   }
   return typeMap[type || 0] || lang.ssUnknown
 }
@@ -298,7 +306,8 @@ const getTypeClass = (type?: number) => {
     73: 'type-h5',
     74: 'type-video',
     75: 'type-bilibili',
-    76: 'type-app-center'
+    76: 'type-app-center',
+    831: 'type-kwl'
   }
   return classMap[type || 0] || 'type-default'
 }

ファイルの差分が大きいため隠しています
+ 808 - 52
src/components/CollapsibleToolbar/index2.vue


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

@@ -0,0 +1,1656 @@
+<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 { ARTICLE_READING_TOOL_TYPE } from '@/configs/englishSpeakingTools'
+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 toolTypeArray = ref([45, 15, 78, 79, 84, 831])
+
+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' && toolTypeArray.value.includes(el.toolType))
+    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,
+    [ARTICLE_READING_TOOL_TYPE]: lang.ssArticleReadingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
+    84: lang.ssFile,
+    831: lang.ssKwlTitle,
+  }
+  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',
+    831: 'type-kwl'
+  }
+  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
+}

+ 42 - 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>
@@ -100,6 +100,8 @@ import useImport from '@/hooks/useImport'
 import FileInput from '@/components/FileInput.vue'
 import message from '@/utils/message'
 import { lang } from '@/main'
+import { showConfirmDialog } from '@/utils/confirmDialog'
+import { useMainStore } from '@/store'
 
 const emit = defineEmits<{
   (e: 'close'): void
@@ -108,6 +110,8 @@ const emit = defineEmits<{
 }>()
 
 const { importPPTXFile, exporting } = useImport()
+const mainStore = useMainStore()
+const { setRunningState } = mainStore
 const currentFileName = ref('')
 const parsingStatus = ref<'parsing' | 'success'>('parsing')
 const parsingAbortController = ref<AbortController | null>(null)
@@ -137,7 +141,22 @@ 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(/\.[^/.]+$/, ''), 2); emit('close')
+      showConfirmDialog({
+        title: lang.ssUploadSuccess,
+        content: lang.ssBatchInteractiveWebConfirm,
+        confirmText: lang.ssBatchInteractiveWebConfirmDesc,
+        cancelText: lang.ssBatchInteractiveWebConfirmDesc2,
+        width: 400,
+        onConfirm: () => {
+          setRunningState(true)
+        },
+        onCancel: () => {
+          console.log('取消删除')
+        },
+      })
+    } })
   }
   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>

+ 1 - 0
src/components/Select.vue

@@ -156,6 +156,7 @@ const handleSelect = (option: SelectOption) => {
 
   .selector {
     min-width: 50px;
+    max-width: 100px;
     height: 30px;
     line-height: 30px;
     padding-left: 10px;

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

+ 17 - 0
src/configs/englishSpeakingTools.ts

@@ -0,0 +1,17 @@
+/**
+ * 口语活动的画布工具编号(唯一来源)。
+ *
+ * 772 刻意跳出 72–81 这段顺序区间:82 是该区间的下一个空位,可能被并行开发的
+ * 其他功能占用。所有判定必须走这里的常量与函数,不得写字面量或范围比较。
+ */
+export const TOPIC_DISCUSSION_TOOL_TYPE = 77 as const
+export const SENTENCE_READING_TOOL_TYPE = 80 as const
+export const ARTICLE_READING_TOOL_TYPE = 772 as const
+
+export function isArticleReadingTool(type: unknown): boolean {
+  return Number(type) === ARTICLE_READING_TOOL_TYPE
+}
+
+export function isTopicDiscussionTool(type: unknown): boolean {
+  return Number(type) === TOPIC_DISCUSSION_TOOL_TYPE
+}

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

+ 10 - 0
src/hooks/useAddSlidesOrElements.ts

@@ -1,7 +1,9 @@
+import { inject } from 'vue'
 import { storeToRefs } from 'pinia'
 import { nanoid } from 'nanoid'
 import { useSlidesStore, useMainStore } from '@/store'
 import type { PPTElement, Slide } from '@/types/slides'
+import { injectKeySingleSlideMode } from '@/types/injectKey'
 import { createSlideIdMap, createElementIdMap, getElementRange } from '@/utils/element'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 
@@ -10,6 +12,9 @@ export default () => {
   const slidesStore = useSlidesStore()
   const { currentSlide } = storeToRefs(slidesStore)
 
+  // 由 indexEn.vue provide;其余编辑器没有 provide,这里拿到 default false
+  const singleSlideMode = inject(injectKeySingleSlideMode, false)
+
   const { addHistorySnapshot } = useHistorySnapshot()
 
   /**
@@ -64,6 +69,11 @@ export default () => {
    * @param slide 页面数据
    */
   const addSlidesFromData = (slides: Slide[]) => {
+    // 单页模式下唯一还能加出第二页的入口:Ctrl+V 粘贴整页数据(Thumbnails 的右键
+    // 菜单已经随缩略图一起隐藏,但 usePasteEvent 仍然活着),以及 useAIPPT 的批量加页。
+    // 两条路都经过这里,一道守卫全封住。
+    if (singleSlideMode) return
+
     const slideIdMap = createSlideIdMap(slides)
     const newSlides = slides.map(slide => {
       const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)

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

+ 103 - 39
src/hooks/useImport.ts

@@ -15,6 +15,32 @@ import { showConfirmDialog } from '@/utils/confirmDialog'
 import { EMFJS, WMFJS } from 'rtf.js'
 import * as UTIF from 'utif2'
 
+// 模块级共享状态,确保所有 useImport() 调用共用同一个 ref
+const _imgExporting = ref(false)
+
+/**
+ * 获取 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,
@@ -37,10 +63,11 @@ const convertFontSizePtToPx = (html: string, ratio: number, autoFit: any) => {
     ratio = ratio * autoFit.fontScale / 100
   }
   // return html;
-  return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
-    return `${Math.floor(parseFloat(p1) * ratio)}px `
-  })
-
+  return html
+    .replace(/href="(ppt\/slides\/)/g, 'href="')
+    .replace(/\s*([\d.]+)pt/g, (match, p1) => {
+      return `${Math.floor(parseFloat(p1) * ratio)}px `
+    })
 }
 
 const getStyle = (htmlString: string) => {
@@ -50,9 +77,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 || ''
 
@@ -67,7 +94,7 @@ export default () => {
   const { isEmptySlide } = useSlideHandler()
 
   const exporting = ref(false)
-  const imgExporting = ref(false)
+  const imgExporting = _imgExporting
 
   // 导入JSON文件
   const importJSON = (files: FileList, cover = false) => {
@@ -223,12 +250,15 @@ export default () => {
     return json
   }
 
-  // 优化暴露到 window 对象的方式,避免重复赋值
+  // 暴露到 window 对象
   if (typeof window !== 'undefined') {
     const win = window as any
     if (!win.exportJSON) win.exportJSON = exportJSON2
     if (!win.readJSON) win.readJSON = readJSON
-    if (!win.imgExporting) win.imgExporting = () => imgExporting.value
+    win.imgExporting = () => {
+      console.log('imgExporting:', _imgExporting.value)
+      return _imgExporting.value
+    }
   }
 
   // 导入pptist文件
@@ -384,29 +414,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 +920,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 +929,8 @@ export default () => {
           height = parseFloat(svgHeight)
         }
       }
-    } catch (e) {
+    }
+    catch (e) {
       console.warn('Failed to get dimensions from SVG, using default', e)
     }
 
@@ -948,7 +992,8 @@ export default () => {
           'image/png'
         )
       })
-    } finally {
+    }
+    finally {
       URL.revokeObjectURL(url)
     }
   }
@@ -1605,13 +1650,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 +1668,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 +1723,7 @@ export default () => {
             exporting.value = false
             imgExporting.value = false
             onclose?.()
+            confirmOnclose?.()
             return Promise.reject(new Error('用户取消导入'))
           }
         }
@@ -1694,10 +1740,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 +1830,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 +2031,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 +2058,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 +2160,7 @@ export default () => {
               }
 
               if (element.path) slide.elements.push(element)
-              //}
+              // }
             }
 
             // ---------- 表格 ----------
@@ -2271,7 +2331,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 +2344,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
               })
@@ -2331,8 +2392,10 @@ export default () => {
       // 等待当前幻灯片内所有上传任务完成
       // await Promise.all(uploadTasks)
       Promise.all(uploadTasks).then(() => {
+        console.log('上传任务完成')
         imgExporting.value = false
       }).catch(() => {
+        console.log('上传任务完成')
         imgExporting.value = false
       })
 
@@ -2487,6 +2550,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))
   }
 
   /**

+ 21 - 0
src/hooks/useSlideHandler.ts

@@ -91,6 +91,26 @@ export default () => {
     addHistorySnapshot()
   }
 
+  // 用一页全新的空白页替换掉当前全部页面(单页模式下代替 createSlide 使用)
+  //
+  // 刻意做成一次赋值而不是「先 createSlide 再 deleteSlide 旧页」:
+  // 1. setSlides 是原子的,不存在「新页建好了但旧页没删掉」这种中间态;
+  // 2. 不在这里存快照,让后续 createElement 那一笔成为唯一的一笔,Ctrl+Z 一次回到原状;
+  // 3. 新页保证是空的,createFrameElement 里「当前页已有 frame 就拒绝插入」的守卫必定放行。
+  const replaceAllSlidesWithNew = () => {
+    const emptySlide: Slide = {
+      id: nanoid(10),
+      elements: [],
+      background: {
+        type: 'solid',
+        color: theme.value.backgroundColor,
+      },
+    }
+    mainStore.setActiveElementIdList([])
+    slidesStore.setSlides([emptySlide])
+    slidesStore.updateSlideIndex(0)
+  }
+
   // 根据模板创建新页面
   const createSlideByTemplate = (slide: Slide) => {
     const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
@@ -207,6 +227,7 @@ export default () => {
     copySlide,
     pasteSlide,
     createSlide,
+    replaceAllSlidesWithNew,
     createSlideByTemplate,
     copyAndPasteSlide,
     deleteSlide,

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

+ 39 - 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])
+}
+
 /**
  * 
  * 获取年级
@@ -210,6 +221,27 @@ export const getClassById = (params: any): Promise<any> => {
   return axios.get(`${API_URL}getClassById`, { params: params })
 }
 
+/**
+ * 
+ * 查询操作记录
+ * @param any id VARCHAR(3000), type VARCHAR(36), `index` int
+ * @returns Promise<any>
+ */
+
+export const selectPPTOp = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}selectPPTOp`, [params])
+}
+
+/**
+ * 
+ * 新增操作记录
+ * @param any id VARCHAR(3000), type VARCHAR(36), `index` INT, `json` LONGTEXT
+ * @returns Promise<any>
+ */
+
+export const addPPTOp = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}addPPTOp`, [params])
+}
 
 
 export default {
@@ -229,6 +261,10 @@ export default {
   getAgentData,
   clearDialogue,
   getWorkDetail,
-  getWorkPageId
+  getWorkPageId,
+  likeWork,
+  getClassById,
+  selectPPTOp,
+  addPPTOp,
 }
 

+ 111 - 0
src/services/speaking.ts

@@ -0,0 +1,111 @@
+import type { ArticleReadingConfig } from '@/types/articleReading'
+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
+
+/** 话题讨论与文章朗读共用同一套 config endpoint,靠 config.type 判别。 */
+export type SpeakingActivityConfig = TopicDiscussionConfig | ArticleReadingConfig
+
+export interface SpeakingConfigRecord<T extends SpeakingActivityConfig = SpeakingActivityConfig> {
+  id: string
+  config: T
+  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<T extends SpeakingActivityConfig>(
+  config: T,
+  ownerUid?: string | null,
+): Promise<SpeakingConfigRecord<T>> {
+  const res = await fetch(API_BASE, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ config, ownerUid: ownerUid ?? null }),
+  })
+  return parse<SpeakingConfigRecord<T>>(res)
+}
+
+/** 读取口语配置 */
+export async function getSpeakingConfig<T extends SpeakingActivityConfig = SpeakingActivityConfig>(
+  id: string,
+): Promise<SpeakingConfigRecord<T>> {
+  const res = await fetch(`${API_BASE}/${encodeURIComponent(id)}`)
+  return parse<SpeakingConfigRecord<T>>(res)
+}
+
+/** 更新口语配置 */
+export async function updateSpeakingConfig<T extends SpeakingActivityConfig>(
+  id: string,
+  config: T,
+): Promise<SpeakingConfigRecord<T>> {
+  const res = await fetch(`${API_BASE}/${encodeURIComponent(id)}`, {
+    method: 'PUT',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ config }),
+  })
+  return parse<SpeakingConfigRecord<T>>(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)
+}

+ 60 - 0
src/store/articleReading.ts

@@ -0,0 +1,60 @@
+import { defineStore } from 'pinia'
+import { reactive, ref } from 'vue'
+import type { ArticleReadingConfig, ArticleReadingTask } from '@/types/articleReading'
+import type { ArticlePreviewStage } from '@/views/Editor/EnglishSpeaking/composables/useArticleReadingEngine'
+import { buildArticleConfig, buildEmptyArticleConfig } from '@/views/Editor/EnglishSpeaking/preview/articleReadingModel'
+
+/** 只持有文章朗读的表单状态,绝不混入话题讨论字段。 */
+export const useArticleReadingStore = defineStore('articleReading', () => {
+  const config = reactive<ArticleReadingConfig>(buildEmptyArticleConfig('五年级'))
+  const activeConfigId = ref('')
+  const openConfigSignal = ref(0)
+  const resetSignal = ref(0)
+  // 预览当前阶段(由 ArticleReadingPreview 同步)。CanvasTool 据此决定「重置预览」显不显示
+  // —— 与话题讨论的 speakingStore.previewState 同一套做法:停在起始页时没什么可重置的。
+  const previewStage = ref<ArticlePreviewStage>('ready')
+
+  function replaceConfig(next: ArticleReadingConfig) {
+    Object.assign(config, next)
+  }
+
+  function prefillFromTask(task: ArticleReadingTask, grade: string) {
+    replaceConfig(buildArticleConfig(task, grade))
+  }
+
+  function resetConfigEmpty(grade: string) {
+    replaceConfig(buildEmptyArticleConfig(grade))
+  }
+
+  function setActiveConfigId(configId: string) {
+    activeConfigId.value = configId
+  }
+
+  function setPreviewStage(stage: ArticlePreviewStage) {
+    previewStage.value = stage
+  }
+
+  function openConfigPanel(configId: string) {
+    activeConfigId.value = configId
+    openConfigSignal.value += 1
+  }
+
+  function requestResetPreview() {
+    resetSignal.value += 1
+  }
+
+  return {
+    config,
+    activeConfigId,
+    openConfigSignal,
+    resetSignal,
+    previewStage,
+    replaceConfig,
+    prefillFromTask,
+    resetConfigEmpty,
+    setActiveConfigId,
+    setPreviewStage,
+    openConfigPanel,
+    requestResetPreview,
+  }
+})

+ 29 - 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,12 @@ export interface MainState {
   showNotesPanel: boolean
   showMarkupPanel: boolean
   showAIPPTDialog: boolean
+  childPageVisible: boolean
+  childPageToolsArray: any[]
+  childPageIndex: number
+  userid: string
+  courseTitle: string
+  running: boolean
 }
 
 const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
@@ -73,6 +79,12 @@ export const useMainStore = defineStore('main', {
     showNotesPanel: false, // 打开批注面板
     showMarkupPanel: false, // 打开类型标注面板
     showAIPPTDialog: false, // 打开AIPPT创建窗口
+    childPageVisible: false, // 打开带选页
+    childPageToolsArray: [], // 带选页工具数组
+    childPageIndex: 0, // 带选页当前页面索引
+    userid: '', // 当前用户ID
+    courseTitle: '', // 课程标题
+    running: false, // 是否正在运行AI任务
   }),
 
   getters: {
@@ -206,5 +218,21 @@ export const useMainStore = defineStore('main', {
     setAIPPTDialogState(show: boolean) {
       this.showAIPPTDialog = show
     },
+    setChildPageState(show: boolean, tools: any[], pageIndex: number) {
+      const slidesStore = useSlidesStore()
+      slidesStore.panAllToolsArray()
+      this.childPageVisible = show
+      this.childPageToolsArray = tools
+      this.childPageIndex = pageIndex
+    },
+    setUserid(userid: string) {
+      this.userid = userid
+    },
+    setCourseTitle(courseTitle: string) {
+      this.courseTitle = courseTitle
+    },
+    setRunningState(state: boolean) {
+      this.running = state
+    }
   },
 })

+ 11 - 0
src/store/slides.ts

@@ -189,6 +189,7 @@ export const useSlidesStore = defineStore('slides', {
     },
   
     updateSlideIndex(index: number) {
+      this.panAllToolsArray()
       this.slideIndex = index
     },
   
@@ -230,5 +231,15 @@ export const useSlidesStore = defineStore('slides', {
       })
       this.slides[slideIndex].elements = (elements as PPTElement[])
     },
+    panAllToolsArray() {
+      this.slides.forEach(slide => {
+        if (slide.toolsArray) {
+          const allToolsSet = slide.toolsArray.every(t => t.isSet)
+          if (allToolsSet) {
+            slide.toolsArray = []
+          }
+        }
+      })
+    },
   },
 })

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

+ 112 - 0
src/types/articleReading.ts

@@ -0,0 +1,112 @@
+/**
+ * 文章朗读契约(对齐后端 spec §6)。
+ *
+ * 班级两个 DTO 的可空字段是 required-nullable 而非 optional:后端
+ * _collect_class_rows 对每个学生都发全部键,not_started 行发 null。
+ */
+export type ArticlePracticeMode = 'time' | 'unlimited'
+/**
+ * `abandoned` 是「重新开始」的标记(后端 spec §4.2)。「哪个 session」类查询一律排除它,
+ * 所以 resume 探针与 get-or-create 都不会返回它 —— 但 `GET /report` 原样回 `session.status`,
+ * 拿着旧 sessionId 直接查就会拿到,契约里必须写全三个值。
+ */
+export type ArticleSessionStatus = 'reading' | 'completed' | 'abandoned'
+export type ArticleParagraphStatus = 'pending' | 'generating' | 'completed' | 'failed'
+export type ArticleOverallStatus = 'generating' | 'completed' | 'failed' | null
+export type ArticleWordStatus = 'good' | 'warn' | 'bad' | 'misread' | 'omission' | 'insertion'
+
+export interface ArticleReadingConfig {
+  type: 'article'
+  grade: string
+  title: string
+  content: string
+  practice: { mode: ArticlePracticeMode; duration: number }
+  evaluation: { showReport: boolean }
+}
+
+export interface ArticleReadingTask {
+  id: string
+  taskType: 'article-reading'
+  titleZh: string
+  titleEn: string
+  subtitle: string
+  difficulty: number
+  count: 1
+  unit: '篇'
+  durationMinutes: number
+  content: string
+}
+
+export interface ArticleScoreDimensions {
+  accuracy: number
+  fluency: number
+  completeness: number
+  prosody: number
+}
+
+export interface ArticleWordResult {
+  word: string
+  status: ArticleWordStatus
+  accuracy: number | null
+}
+
+export interface ArticleParagraphResult {
+  idx: number
+  text: string
+  status: ArticleParagraphStatus
+  audioUrl: string | null
+  overallScore: number | null
+  dims: ArticleScoreDimensions | null
+  /** 段内扁平词级,按对齐顺序(spec §6)。句层已在后端 c62d35a 删除,别再加回来。 */
+  words: ArticleWordResult[]
+}
+
+export interface ArticleOverallResult {
+  aiComment: string
+  highlights: string[]
+  suggestions: string[]
+  overallScore: number
+  dims: ArticleScoreDimensions
+}
+
+export interface ArticleReadingReport {
+  sessionId: string
+  title: string
+  status: ArticleSessionStatus
+  totalDurationSeconds: number | null
+  /**
+   * 绝对截止时刻(ISO8601,后端 spec §4.4);null = 不限时。
+   * **只用来显示计时、以及决定本地时钟什么时候去问一次后端** —— 到了没由后端说了算。
+   */
+  expiresAt: string | null
+  paragraphs: ArticleParagraphResult[]
+  overall: ArticleOverallResult | null
+  overallStatus: ArticleOverallStatus
+}
+
+export interface ArticleStreamAck {
+  type: 'ack'
+  status: 'generating'
+  attempt: number
+  truncated: boolean
+}
+
+export interface ArticleClassUser {
+  userId: string
+  name: string
+}
+
+export interface ArticleClassSessionSummary extends ArticleClassUser {
+  status: 'completed' | 'reading' | 'not_started'
+  sessionId: string | null
+  overallScore: number | null
+  dims: ArticleScoreDimensions | null
+  completedAt: string | null
+}
+
+export interface ArticleClassSummaryResponse {
+  summary: string
+  generatedAt: string
+  fromCache: boolean
+  llmStatus: 'ok' | 'fallback'
+}

+ 348 - 0
src/types/englishSpeaking.ts

@@ -0,0 +1,348 @@
+import type { ArticleReadingTask } from '@/types/articleReading'
+
+// 练习方式
+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
+  taskType: 'topic-discussion'
+  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'
+
+// 推荐卡片的判别联合:按 taskType 区分,不把文章数据伪装成话题讨论
+export type SpeakingRecommendationTask = TopicDiscussionTask | ArticleReadingTask
+
+// 页面层级
+export type PageMode = 'layer1' | 'layer2' | 'config' | 'config-article-reading'
+
+// 练习类型
+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'

+ 15 - 1
src/types/injectKey.ts

@@ -9,4 +9,18 @@ export type RadioGroupValue = {
 
 export const injectKeySlideScale: InjectionKey<SlideScale> = Symbol()
 export const injectKeySlideId: InjectionKey<SlideId> = Symbol()
-export const injectKeyRadioGroupValue: InjectionKey<RadioGroupValue> = Symbol()
+export const injectKeyRadioGroupValue: InjectionKey<RadioGroupValue> = Symbol()
+
+/**
+ * 单页模式。EditorEn(英语展示页)专用:整个编辑器只允许存在一页,
+ * 插入口语工具时用「替换当前全部页面」代替「新增一页」。
+ *
+ * provide:views/Editor/indexEn.vue
+ * inject :EnglishSpeaking/layers/Layer2Speaking.vue —— 建页分支
+ *          hooks/useAddSlidesOrElements.ts —— 挡住 Ctrl+V 贴页 / AIPPT 批量加页
+ *
+ * 其余编辑器(index / index2 / index3)不 provide,inject 一律拿到 default false,
+ * 行为与今天完全一致。因为是 provide 而不是全局 store,值随 EditorEn 一起消失,
+ * 不存在「切走之后旗标还留着」的问题。
+ */
+export const injectKeySingleSlideMode: InjectionKey<boolean> = Symbol()

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

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

@@ -0,0 +1,479 @@
+<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" v-if="toolsJson?.url === toolsArray[selectedToolIndex]?.url">
+      <div class="question-area">
+        <template v-for="(tool, index) in toolsArray" :key="tool.tool_id">
+          <iframe
+          v-if="index === selectedToolIndex && tool.url"
+          :src="tool.url + '&isSet=2' + '&timestamp=' + new Date().getTime()"
+          frameborder="0"></iframe>
+        </template>
+      </div>
+
+      <div class="side-panel" v-if="!toolsJson.isSet">
+         <button class="side-btn primary" @click="saveTool(toolsJson.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 toolsJson = computed(() => {
+  return toolsArray[selectedToolIndex.value] || {}
+})
+
+const formatHint = (str: string, count: number) => {
+  return str.replace('{0}', String(count))
+}
+
+const selectTool = (index: number) => {
+  selectedToolIndex.value = index
+  console.log('👶 选中工具:', toolsArray[index])
+}
+
+onMounted(() => {
+  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, (newVal) => {
+  // async 
+  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
+}, { immediate: true, deep: true })
+
+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}&userid=${userid.value || ''}`
+  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 (window.location.href.includes('beta')) {
+    url = 'https://beta.pbl.cocorobo.cn/'
+  }
+  else 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) => {
+  console.log('保留工具')
+  
+  createSlide()
+  createFrameElement(url, type)
+  changeSlideIndex(childPageIndex.value)
+  toolsArray[index].isSet = true
+  setTool()
+  setTimeout(() => {
+    changeSlideIndex(childPageIndex.value + 1)
+    setChildPageState(false, [], slideIndex.value || 0)
+  }, 100)
+  console.log('保留工具', toolsArray)
+}
+
+// 放弃工具
+const discardTool = (index: number) => {
+  toolsArray[index].isSet = true
+  setTool()
+  console.log('放弃工具', toolsArray)
+}
+
+// 保留所有工具
+const keepAllTools = () => {
+  toolsArray.forEach(tool => {
+    if (!tool.isSet) {
+      createSlide()
+      createFrameElement(tool.url, getToolType(tool.tool_id))
+      tool.isSet = true
+    }
+  })
+  changeSlideIndex(childPageIndex.value)
+  setTool()
+  setTimeout(() => {
+    changeSlideIndex(childPageIndex.value + 1)
+    setChildPageState(false, [], slideIndex.value || 0)
+  }, 100)
+}
+
+// 判断工具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

+ 10 - 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)"
@@ -51,6 +51,7 @@ import {
   type PPTChartElement,
 } from '@/types/slides'
 import type { OperateLineHandlers, OperateResizeHandlers } from '@/types/edit'
+import { isArticleReadingTool, isTopicDiscussionTool } from '@/configs/englishSpeakingTools'
 
 import ImageElementOperate from './ImageElementOperate.vue'
 import TextElementOperate from './TextElementOperate.vue'
@@ -103,6 +104,14 @@ 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 / 772)产品设计上占满整页不可变形,
+// 强制隐藏 resize / rotate 把手,用户仍可选中+删除+开配置面板,但看不到任何"拖动感"。
+const isLockedInteractiveWidget = computed(() => {
+  if (props.elementInfo.type !== ElementTypes.FRAME) return false
+  const toolType = (props.elementInfo as any).toolType
+  return isTopicDiscussionTool(toolType) || isArticleReadingTool(toolType)
+})
 </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 / 772(英语口语预览)这类可交互 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, .article-reading-preview')) return
+
   e.preventDefault()
 
   // 按住Ctrl键时:缩放画布

+ 10 - 3
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -103,9 +103,15 @@ 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,
+    84: lang.ssFile,
+    831: lang.ssKwlTitle,
   }
   return typeMap[type] || lang.ssUnknown
 }
@@ -119,7 +125,8 @@ const getTypeClass = (type: number) => {
     73: 'type-h5',
     74: 'type-video',
     75: 'type-bilibili',
-    76: 'type-app-center'
+    76: 'type-app-center',
+    831: 'type-kwl',
   }
   return classMap[type] || 'type-default'
 }

+ 354 - 6
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,27 @@
             </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>
+          <div class="popover-item" @click="editContent(84)" v-if="frametype != 84">
+            <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.ssFile }}</span>
+          </div>
         </template>
         <div class="handler-item" :class="{ active: toolVisible }">
           <span class="svg-icon">
@@ -29,6 +52,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"
@@ -39,6 +70,23 @@
           </svg>
         </div>
       </Popover>
+      <!-- 工具iframe字号缩放:比例1~24,每±1对应字号±1px,初始16px -->
+      <div class="handler-item font-scale-ctrl" v-if="hasInteractiveTool">
+        <span class="scale-btn" :class="{ disable: fontScale <= MIN_FONT_SCALE }" @click="changeFontScale(-1)">
+          <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+            stroke-linecap="round">
+            <line x1="5" y1="12" x2="19" y2="12" />
+          </svg>
+        </span>
+        <span class="scale-value">{{ fontScale }}</span>
+        <span class="scale-btn" :class="{ disable: fontScale >= MAX_FONT_SCALE }" @click="changeFontScale(1)">
+          <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+            stroke-linecap="round">
+            <line x1="12" y1="5" x2="12" y2="19" />
+            <line x1="5" y1="12" x2="19" y2="12" />
+          </svg>
+        </span>
+      </div>
       <Popover trigger="click" v-model:value="textTypeSelectVisible"
         style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="!isFrame">
         <template #content>
@@ -104,6 +152,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 +259,16 @@
 </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 { useArticleReadingStore } from '@/store/articleReading'
+import {
+  ARTICLE_READING_TOOL_TYPE,
+  isArticleReadingTool,
+  isTopicDiscussionTool,
+} from '@/configs/englishSpeakingTools'
 import { getImageDataURL } from '@/utils/image'
 import useImport from '@/hooks/useImport'
 import type { ShapePoolItem } from '@/configs/shapes'
@@ -156,11 +292,49 @@ 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 articleStore = useArticleReadingStore()
 const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
 const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
-const { currentSlide } = storeToRefs(slidesStore)
+const { currentSlide, slideIndex } = storeToRefs(slidesStore)
+const { previewState } = storeToRefs(speakingStore)
+const { previewStage: articlePreviewStage } = storeToRefs(articleStore)
+
+// 当前 slide 是否包含英语口语工具(toolType=77)
+const hasEnglishSpeakingTool = computed(() => {
+  const elements = currentSlide.value?.elements || []
+  return elements.some((el: any) => el.type === 'frame' && isTopicDiscussionTool(el.toolType))
+})
+
+// 文章朗读(toolType=772)
+const hasArticleReadingTool = computed(() => {
+  const elements = currentSlide.value?.elements || []
+  return elements.some((el: any) => el.type === 'frame' && isArticleReadingTool(el.toolType))
+})
+
+// 「重置预览」两种题型同一条规则:元素在当前页 **且** 预览已经离开起始页。
+// 文章朗读原来是「元素在就一直显示」,与话题讨论不一致 —— 停在起始页时那个按钮没有任何
+// 可重置的东西,按下去只是原地重载一次。两边的 ready 都由各自的预览组件同步进 store。
+const canResetSpeakingPreview = computed(
+  () => (hasEnglishSpeakingTool.value && previewState.value !== 'ready')
+    || (hasArticleReadingTool.value && articlePreviewStage.value !== 'ready')
+)
+
+const handleResetSpeakingPreview = () => {
+  if (hasArticleReadingTool.value) articleStore.requestResetPreview()
+  if (hasEnglishSpeakingTool.value) speakingStore.requestResetPreview()
+}
 
 const getInitialViewMode = () => {
   const urlParams = new URLSearchParams(window.location.search)
@@ -177,9 +351,42 @@ const getInitialViewMode = () => {
 
 const viewMode = computed(() => getInitialViewMode())
 
+const toolTypeArray = ref([45, 15, 78, 79, 84])
+
 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' && toolTypeArray.value.includes(el.toolType))
+})
+
+// 工具iframe字号缩放:比例最小1最大24,每±1对应字号±1px,初始16px
+const MIN_FONT_SCALE = 1
+const MAX_FONT_SCALE = 24
+const BASE_FONT_SIZE = 16
+const fontScale = ref(1)
+
+const setToolIframeFontSize = () => {
+  try {
+    const iframe = document.getElementsByTagName('iframe')[0] as HTMLIFrameElement | undefined
+    const iframeDoc = iframe?.contentWindow?.document
+    if (iframeDoc?.documentElement) {
+      iframeDoc.documentElement.style.fontSize = `${BASE_FONT_SIZE + fontScale.value - 1}px`
+    }
+  }
+  catch (error) {
+    console.warn('设置iframe字号失败:', error)
+  }
+}
+
+const changeFontScale = (delta: number) => {
+  const next = fontScale.value + delta
+  if (next < MIN_FONT_SCALE || next > MAX_FONT_SCALE) return
+  fontScale.value = next
+  setToolIframeFontSize()
+}
+
+// 切换页码时重置缩放比例回1
+watch(slideIndex, () => {
+  fontScale.value = 1
 })
 
 const iframeLabel = computed(() => {
@@ -198,9 +405,16 @@ 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,
+    [ARTICLE_READING_TOOL_TYPE]: lang.ssArticleReadingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
+    84: lang.ssFile,
+    831: lang.ssKwlTitle,
   }
   return typeMap[type] || lang.ssUnknown
 }
@@ -396,12 +610,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' && toolTypeArray.value.includes(el.toolType))
     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 +778,50 @@ const editContent = (toolType: number) => {
   }
 }
 
+.font-scale-ctrl {
+  gap: 8px;
+  padding: 0 10px;
+
+  .scale-btn {
+    display: flex;
+    align-items: center;
+
+    svg {
+      width: 14px;
+      height: 14px;
+    }
+
+    &:hover {
+      color: #f89c46;
+    }
+
+    &.disable {
+      opacity: .4;
+      cursor: not-allowed;
+    }
+  }
+
+  .scale-value {
+    min-width: 20px;
+    text-align: center;
+    font-size: 13px;
+  }
+}
+
+.reset-preview-btn {
+  gap: 5px;
+  color: #6b7280;
+
+  svg {
+    width: 14px;
+    height: 14px;
+  }
+
+  &:hover {
+    color: #f97316;
+  }
+}
+
 .left-handler {
   display: flex;
   align-items: center;
@@ -624,4 +965,11 @@ const editContent = (toolType: number) => {
     }
   }
 }
+
+.row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 180px;
+}
 </style>

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

@@ -0,0 +1,186 @@
+<!--
+  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' || pageMode === 'config-article-reading'">
+            <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">
+              {{ pageMode === 'config-article-reading' ? lang.ssArticleReading : 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"
+      />
+      <ArticlePracticeConfig
+        v-else-if="pageMode === 'config-article-reading'"
+        @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 { useArticleReadingStore } from '@/store/articleReading'
+import Layer1Home from './layers/Layer1Home.vue'
+import Layer2Speaking from './layers/Layer2Speaking.vue'
+import TopicDiscussionConfig from './configs/TopicDiscussionConfig.vue'
+import ArticlePracticeConfig from './configs/ArticlePracticeConfig.vue'
+
+/** 当前页面层级:layer1(首页) → layer2(口语列表) → config(配置页) */
+const pageMode = ref<PageMode>('layer1')
+
+const speakingStore = useSpeakingStore()
+const articleStore = useArticleReadingStore()
+
+// 课本筛选条件的默认值(上海英语·五年级上·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'
+})
+
+// 772 型 frame 走各自的信号,两条配置页互不干扰
+watch(() => articleStore.openConfigSignal, (val, old) => {
+  if (val !== old) pageMode.value = 'config-article-reading'
+})
+</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>

+ 346 - 0
src/views/Editor/EnglishSpeaking/components/ArticleFormatDialog.vue

@@ -0,0 +1,346 @@
+<template>
+  <div v-if="visible" class="modal-mask">
+    <div
+      ref="dialogRef"
+      class="format-dialog scale-in"
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="article-format-dialog-title"
+      aria-describedby="article-format-dialog-desc"
+      tabindex="-1"
+      @keydown="handleKeydown"
+    >
+      <!-- 覆盖确认 / 应用守卫的进行中态:整块大转圈,按钮排消失(还原 enspeak :461-480) -->
+      <div v-if="loading && mode !== 'paste'" class="dialog-loading">
+        <svg class="spinner spinner-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
+          <path d="M12 3a9 9 0 1 0 9 9" />
+        </svg>
+        <div id="article-format-dialog-title" class="dialog-title">{{ lang.ssArticleAutoFormatting }}</div>
+        <div id="article-format-dialog-desc" class="dialog-hint">{{ lang.ssArticleAutoFormattingHint }}</div>
+      </div>
+
+      <template v-else>
+        <div class="dialog-body">
+          <div id="article-format-dialog-title" class="dialog-title">{{ title }}</div>
+          <div id="article-format-dialog-desc" class="dialog-message">{{ bodyText }}</div>
+        </div>
+
+        <div class="dialog-actions">
+          <button class="dialog-btn btn-secondary" :disabled="loading" @click="emit('cancel')">
+            {{ cancelLabel }}
+          </button>
+          <button class="dialog-btn btn-primary" :disabled="loading" @click="emit('confirm')">
+            <!-- 粘贴弹窗的进行中态:确认键内联小转圈(还原 enspeak :443-450) -->
+            <svg
+              v-if="loading"
+              class="spinner spinner-sm"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="2.5"
+              stroke-linecap="round"
+            >
+              <path d="M12 3a9 9 0 1 0 9 9" />
+            </svg>
+            {{ loading ? lang.ssArticleAutoFormatting : confirmLabel }}
+          </button>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
+import { lang } from '@/main'
+
+const props = defineProps<{
+  visible: boolean
+  mode: 'paste' | 'overwrite' | 'guard'
+  loading: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'confirm'): void
+  (e: 'cancel'): void
+}>()
+
+// 焦点陷阱 / 键盘行为:与同目录 TaskHintModal.vue 同一套实现,两个弹框统一风格
+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') {
+    if (props.loading) return // 进行中态 Escape 失效,与取消按钮 :disabled="loading" 一致
+    event.preventDefault()
+    emit('cancel')
+    return
+  }
+
+  if (event.key === 'Tab') {
+    containTabFocus(event)
+  }
+}
+
+watch(
+  () => props.visible,
+  (visible) => {
+    if (visible) {
+      void focusDialog()
+      return
+    }
+
+    restorePreviousFocus()
+  },
+  { flush: 'post', immediate: true },
+)
+
+onBeforeUnmount(() => {
+  restorePreviousFocus()
+})
+
+// 文案按 mode 分档。必须使用静态key访问(避免动态下标)
+// 扫描脚本依赖正则表达式检测源码中的key引用
+const title = computed(() => {
+  if (props.mode === 'paste') return lang.ssArticlePasteFormatTitle as string
+  if (props.mode === 'overwrite') return lang.ssArticleOverwriteFormatTitle as string
+  return lang.ssArticleGuardTitle as string
+})
+
+const bodyText = computed(() => {
+  if (props.mode === 'paste') return lang.ssArticlePasteFormatMessage as string
+  if (props.mode === 'overwrite') return lang.ssArticleOverwriteFormatMessage as string
+  return lang.ssArticleGuardMessage as string
+})
+
+const cancelLabel = computed(() =>
+  (props.mode === 'guard' ? lang.ssArticleGuardManual : lang.ssArticleFormatCancel) as string
+)
+
+const confirmLabel = computed(() =>
+  (props.mode === 'guard' ? lang.ssArticleAutoFormat : lang.ssArticleFormatConfirm) as string
+)
+</script>
+
+<style lang="scss" scoped>
+/* 值抄自同仓 TaskHintModal.vue(scoped 无法跨组件复用,故复制)= enspeak bg-black/30 + inset-0 + z-50 */
+.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);
+}
+
+/* enspeak: bg-white rounded-xl shadow-2xl w-[380px] p-6 */
+.format-dialog {
+  width: 380px;
+  max-width: 100%;
+  padding: 24px;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
+  box-sizing: border-box;
+}
+
+/* enspeak: animate-in fade-in zoom-in duration-200 */
+.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);
+  }
+}
+
+.dialog-body {
+  text-align: center;
+  margin-bottom: 24px;
+}
+
+/* enspeak: text-[15px] font-semibold text-gray-800 mb-2 */
+.dialog-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1f2937;
+  margin-bottom: 8px;
+}
+
+/* enspeak: text-[13px] text-gray-600 */
+.dialog-message {
+  font-size: 13px;
+  line-height: 1.6;
+  color: #4b5563;
+}
+
+.dialog-loading {
+  text-align: center;
+  padding: 8px 0;
+}
+
+/* enspeak: text-[13px] text-gray-500 */
+.dialog-hint {
+  font-size: 13px;
+  color: #6b7280;
+}
+
+/* enspeak: flex gap-3 */
+.dialog-actions {
+  display: flex;
+  gap: 12px;
+}
+
+/* enspeak: flex-1 h-10 rounded-lg text-[13px] font-medium */
+.dialog-btn {
+  flex: 1;
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: background 0.2s;
+
+  &:disabled {
+    cursor: not-allowed;
+  }
+}
+
+/* enspeak: border-gray-200 text-gray-700 hover:bg-gray-50 disabled:opacity-50 */
+.btn-secondary {
+  border: 1px solid #e5e7eb;
+  background: #fff;
+  color: #374151;
+
+  &:hover:not(:disabled) {
+    background: #f9fafb;
+  }
+
+  &:disabled {
+    opacity: 0.5;
+  }
+}
+
+/* enspeak: bg-orange-500 text-white hover:bg-orange-600 */
+.btn-primary {
+  border: none;
+  background: #f97316;
+  color: #fff;
+
+  &:hover:not(:disabled) {
+    background: #ea580c;
+  }
+}
+
+.spinner {
+  animation: spin 0.8s linear infinite;
+  flex-shrink: 0;
+}
+
+/* enspeak: LoaderIcon w-4 h-4 */
+.spinner-sm {
+  width: 16px;
+  height: 16px;
+}
+
+/* enspeak: LoaderIcon w-8 h-8 text-orange-500 mx-auto mb-4 */
+.spinner-lg {
+  width: 32px;
+  height: 32px;
+  color: #f97316;
+  display: block;
+  margin: 0 auto 16px;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+</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>

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

@@ -0,0 +1,136 @@
+<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>{{ typeLabel }}</span>
+        <span class="meta-dot">·</span>
+        <span>{{ countLabel }}</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 { SpeakingRecommendationTask } from '@/types/englishSpeaking'
+
+const props = defineProps<{
+  task: SpeakingRecommendationTask
+}>()
+
+defineEmits<{
+  (e: 'select'): void
+}>()
+
+// 按 taskType 分支渲染:文章卡显示「1 篇」,话题卡显示「N 轮」,不把文章塞进话题形状
+const typeLabel = computed(() =>
+  props.task.taskType === 'article-reading'
+    ? (lang.ssArticleReading as string)
+    : (lang.ssTopicDiscussion as string),
+)
+
+const countLabel = computed(() =>
+  props.task.taskType === 'article-reading'
+    ? `${props.task.count} ${props.task.unit}`
+    : `${props.task.rounds}${lang.ssRoundsUnit}`,
+)
+
+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>

+ 45 - 0
src/views/Editor/EnglishSpeaking/composables/articleAutoFormatRules.ts

@@ -0,0 +1,45 @@
+/**
+ * 配置端自动分段的纯规则(spec §3.2 / §3.2.1)。
+ *
+ * 本文件**零依赖**——不 import vue、不 import 服务,
+ * 这样 scripts/test-article-auto-format.mjs 可以直接转译执行,不需要任何 shim。
+ */
+
+/** 单段最大词数;超过则「应用配置」硬守卫拦截(spec §3.2 阈值汇总)。 */
+export const MAX_PARAGRAPH_WORDS = 300
+
+/** 粘贴触发弹窗的词数下限,严格大于才提示(spec §3.2 step 1)。 */
+export const PASTE_PROMPT_MIN_WORDS = 50
+
+export function countWords(text: string): number {
+  const trimmed = text.trim()
+  return trimmed ? trimmed.split(/\s+/).length : 0
+}
+
+/** 与后端 split_paragraphs 同规则:`\n+` 切段 + 段内空白归一 + 丢空段,再以 `\n\n` 接。 */
+export function normalizeParagraphs(text: string): string {
+  return text
+    .split(/\n+/)
+    .map(chunk => chunk.trim().split(/\s+/).join(' '))
+    .filter(chunk => chunk.length > 0)
+    .join('\n\n')
+}
+
+/** 是否存在超过上限的段落。提示文案不点名段号,故只回布尔(spec §3.2 step 4)。 */
+export function hasOverlongParagraph(text: string): boolean {
+  return normalizeParagraphs(text)
+    .split('\n\n')
+    .some(paragraph => countWords(paragraph) > MAX_PARAGRAPH_WORDS)
+}
+
+/**
+ * 空文本框粘贴长文时提示(spec §3.2 step 1)。条件就是 spec 写的两条,没有第三条:
+ * 原内容 trim 后为空,且新内容超过 50 词。
+ *
+ * 不再看新内容里有没有 `\n\n`。带空行 ≠ 分得适合朗读——一篇三段、每段 400 词的文章
+ * 完全带空行,而每段就是一个录音单位,正是本功能要拆的东西。
+ * 老师从网页/Word 复制的正文几乎必然带空行,加那条守卫等于让主要入口永不触发。
+ */
+export function shouldPromptOnPaste(prev: string, next: string): boolean {
+  return prev.trim() === '' && countWords(next) > PASTE_PROMPT_MIN_WORDS
+}

+ 117 - 0
src/views/Editor/EnglishSpeaking/composables/useArticleAutoFormat.ts

@@ -0,0 +1,117 @@
+/**
+ * 配置端自动分段的弹框状态机(spec §3.2 / §3.2.1)。
+ *
+ * 三种触发共用同一个 `POST /article/segment`(整篇,非单段):
+ * ① 空文本框粘贴长文 → mode='paste'
+ * ② 点「自动分段」按钮 → mode='overwrite'
+ * ③ 应用配置撞上 300 词硬守卫 → mode='guard'
+ */
+import { onScopeDispose, ref, type Ref } from 'vue'
+import { lang } from '@/main'
+import message from '@/utils/message'
+import { segmentArticle } from '../services/articleReading'
+import {
+  hasOverlongParagraph,
+  normalizeParagraphs,
+  shouldPromptOnPaste,
+} from './articleAutoFormatRules'
+
+export type FormatDialogMode = 'paste' | 'overwrite' | 'guard'
+
+export function useArticleAutoFormat(content: Ref<string>) {
+  const dialogVisible = ref(false)
+  const dialogMode = ref<FormatDialogMode>('paste')
+  const isFormatting = ref(false)
+
+  // ArticlePracticeConfig 是 v-else-if'd(真会卸载)。晚到的分段响应不能在作用域销毁后
+  // 还去写 content(它是外部传入、直连 pinia store 的 computed)。
+  let disposed = false
+  onScopeDispose(() => {
+    disposed = true
+  })
+
+  /**
+   * textarea 的 @input。恒写 store —— 取消弹窗即「保留原始粘贴」。
+   * 上一次的内容直接取 content.value:此刻还没写入,它就是输入前的值。
+   * 不缓存到局部变量,否则 store.replaceConfig 载入已有配置后会陈旧。
+   */
+  function onContentInput(next: string) {
+    const prev = content.value
+    content.value = next
+    if (shouldPromptOnPaste(prev, next)) {
+      dialogMode.value = 'paste'
+      dialogVisible.value = true
+    }
+  }
+
+  /** 「自动分段」按钮:先弹覆盖确认,确认才调后端。 */
+  function openOverwrite() {
+    if (!content.value.trim() || isFormatting.value) return
+    dialogMode.value = 'overwrite'
+    dialogVisible.value = true
+  }
+
+  /**
+   * 「应用配置」前置校验:归一化回写 + 300 词硬守卫。
+   * @returns true = 放行提交;false = 已开守卫弹框,调用方必须中止提交
+   */
+  function guardBeforeApply(): boolean {
+    if (isFormatting.value) return false // 请求进行中:不放行提交,也不误开/误改弹框状态
+    const normalized = normalizeParagraphs(content.value)
+    content.value = normalized
+    if (!hasOverlongParagraph(normalized)) return true
+    dialogMode.value = 'guard'
+    dialogVisible.value = true
+    return false
+  }
+
+  /** 三种弹框的确认键共用;整篇送后端,结果原样归一化后回填。 */
+  async function confirmDialog() {
+    if (isFormatting.value) return // 防重复(承 enspeak runAutoFormat 早退)
+    const source = content.value.trim()
+    if (!source) {
+      // 可达路径:老师清空文本框后点确认。不能静默无反应,必须关框(否则弹框卡住)。
+      dialogVisible.value = false
+      return
+    }
+
+    isFormatting.value = true
+    try {
+      const { content: segmented } = await segmentArticle(source)
+      if (disposed) return // 组件已卸载:作用域已销毁,响应作废,不再写 store
+      if (content.value.trim() !== source) {
+        // 30 秒等待期间老师又改了文本:新内容优先,只关框,绝不覆盖
+        dialogVisible.value = false
+        return
+      }
+      content.value = normalizeParagraphs(segmented)
+      dialogVisible.value = false
+    }
+    catch (err: unknown) {
+      if (disposed) return
+      console.error('[article-reading] auto format failed:', err)
+      message.error(lang.ssArticleSegmentFailed as string)
+      // 守卫框保持打开:老师可原地重试或改选「手动修改」。其余两种关掉即可。
+      if (dialogMode.value !== 'guard') dialogVisible.value = false
+    }
+    finally {
+      if (!disposed) isFormatting.value = false
+    }
+  }
+
+  function cancelDialog() {
+    if (isFormatting.value) return
+    dialogVisible.value = false
+  }
+
+  return {
+    dialogVisible,
+    dialogMode,
+    isFormatting,
+    onContentInput,
+    openOverwrite,
+    guardBeforeApply,
+    confirmDialog,
+    cancelDialog,
+  }
+}

+ 827 - 0
src/views/Editor/EnglishSpeaking/composables/useArticleReadingEngine.ts

@@ -0,0 +1,827 @@
+import { ref, computed, watch, onUnmounted, toValue, type MaybeRefOrGetter } from 'vue'
+import { useAudioRecorder } from './useAudioRecorder'
+import { useAudioPlayer } from './useAudioPlayer'
+import {
+  createArticleSession,
+  getArticleReport,
+  completeArticleSession,
+  getArticleDemoAudio,
+  abandonArticleSession,
+} from '../services/articleReading'
+import { friendlyArticleError, isArticleSessionGone } from '../services/articleErrors'
+import { openArticleParagraphStream, type ArticleParagraphStream } from '../services/articleReadingStream'
+import { isFinalReportReady, pollDelayMs } from '../preview/articleReadingModel'
+import type { ArticleReadingReport } from '@/types/articleReading'
+
+export type ArticlePreviewStage = 'ready' | 'reading' | 'report-loading' | 'report'
+/**
+ * 三态,**没有原型的 `done`**(enspeak `ArticleReadingView.tsx` 有第四态「完成 ✓」)。
+ *
+ * 原型的 `done` 是 mock 的产物:它录完当场出分,所以能停在「完成」上让学生看一眼。
+ * 真实管线是异步的(决策 A,spec §9)—— ack 一到 `advanceAfterAck` 就把 recordingState
+ * 打回 'idle' 并跳去下一段,分数稍后由轮询回填。`done` 因此没有任何可以停留的时刻。
+ * 别照着原型把它加回来:加回来也永远不会被赋值。
+ */
+export type ArticleRecordingState = 'idle' | 'recording' | 'processing'
+
+/** 单段录音硬上限(spec §5.1);到点只停当前段,不结束整篇。 */
+const MAX_PARAGRAPH_SECONDS = 180
+
+/**
+ * 轮询**连续失败**几次后放弃(对齐话题讨论有上限的做法,`useDialogueEngine.ts:269`)。
+ *
+ * 计的是连续失败而不是总轮询次数:段落还在 `generating` 时的健康轮询走的是同一个
+ * `pollAttempt`,拿总次数封顶会把正常等分数的学生一起掐掉。收到任何一次成功响应就归零 ——
+ * 响应本身就是后端还活着的证据。
+ *
+ * 退避封顶 8s,所以 8 次连续失败 ≈ 0+1+2+4+8+8+8 ≈ 31 秒后停手。停手不是无声的:
+ * error 横幅留在原地,report-loading 门另有手动重试按钮,学生有路可走。
+ */
+const MAX_POLL_FAILURES = 8
+
+export interface ArticleReadingEngineOptions {
+  configId: MaybeRefOrGetter<string>
+  /** null = 编辑态老师预览(spec §4.5)。engine 只负责原样转发,判定在 ArticleReadingPreview。 */
+  userId: MaybeRefOrGetter<string | null>
+  durationMinutes: MaybeRefOrGetter<number>
+  showReport: MaybeRefOrGetter<boolean>
+  onSessionStarted?: (sessionId: string) => void
+  onSessionCompleted?: (sessionId: string) => void
+  /** 「重新开始」成功作废旧 session(spec §4.2)。老师端据此把「已完成」刷回「未开始」。 */
+  onSessionAbandoned?: (sessionId: string) => void
+}
+
+export interface ArticleReadingEngineDeps {
+  createSession: typeof createArticleSession
+  getReport: typeof getArticleReport
+  completeSession: typeof completeArticleSession
+  getDemoAudio: typeof getArticleDemoAudio
+  abandonSession: typeof abandonArticleSession
+  createStream: typeof openArticleParagraphStream
+  recorder: ReturnType<typeof useAudioRecorder>
+  player: ReturnType<typeof useAudioPlayer>
+  document: Pick<Document, 'hidden' | 'addEventListener' | 'removeEventListener'>
+  setTimeout: typeof window.setTimeout
+  clearTimeout: typeof window.clearTimeout
+  setInterval: typeof window.setInterval
+  clearInterval: typeof window.clearInterval
+  /** 墙上时钟。计时器改按时间戳算之后(spec §9.2.3),测试必须能控制「现在几点」。 */
+  now: () => number
+}
+
+export function useArticleReadingEngine(
+  options: ArticleReadingEngineOptions,
+  overrides: Partial<ArticleReadingEngineDeps> = {},
+) {
+  const deps: ArticleReadingEngineDeps = {
+    createSession: createArticleSession,
+    getReport: getArticleReport,
+    completeSession: completeArticleSession,
+    getDemoAudio: getArticleDemoAudio,
+    abandonSession: abandonArticleSession,
+    createStream: openArticleParagraphStream,
+    recorder: overrides.recorder ?? useAudioRecorder(),
+    player: overrides.player ?? useAudioPlayer(),
+    document: globalThis.document,
+    setTimeout: ((...args: Parameters<typeof setTimeout>) => setTimeout(...args)) as typeof window.setTimeout,
+    clearTimeout: ((id?: number) => clearTimeout(id)) as typeof window.clearTimeout,
+    setInterval: ((...args: Parameters<typeof setInterval>) => setInterval(...args)) as typeof window.setInterval,
+    clearInterval: ((id?: number) => clearInterval(id)) as typeof window.clearInterval,
+    now: () => Date.now(),
+    ...overrides,
+  }
+  const recorder = deps.recorder
+  const player = deps.player
+
+  const stage = ref<ArticlePreviewStage>('ready')
+  /** `POST /session` 在途。视图据此置灰「开始朗读」;闩本身在 startSession 里,按钮只是它的显示。 */
+  const starting = ref(false)
+  const report = ref<ArticleReadingReport | null>(null)
+  const currentParagraphIdx = ref(0)
+  const recordingState = ref<ArticleRecordingState>('idle')
+  /** 已用练习秒数:仅计时模式下计数,非计时模式恒为 null(没有计时器)。到上限后冻结,不再累加。 */
+  const elapsedPracticeSeconds = ref<number | null>(null)
+  const localAudioByParagraph = ref(new Map<number, Blob>())
+  const error = ref<string | null>(null)
+  /**
+   * 后端说这一轮已经作废(409,spec §4.2)。重试必然再失败,视图应改提示「请重新开始」。
+   * 与 error 分开:error 是给人看的文案,这面旗是给视图分支用的。
+   */
+  const sessionInvalid = ref(false)
+
+  const recordedParagraphs = new Set<number>()
+  const demoUrlCache = new Map<string, string>()
+
+  let activeStream: ArticleParagraphStream | null = null
+  let startingRecording = false
+  let finishingRecording = false
+  let pollTimer: number | null = null
+  let pollAttempt = 0
+  /** 连续失败次数(成功即归零)。到 MAX_POLL_FAILURES 就停手,不再无限敲后端。 */
+  let pollFailures = 0
+  /**
+   * 当前那面横幅是不是轮询自己竖起来的。
+   *
+   * 只有它该被下一次成功轮询收走 —— 麦克风、录音被拒、示范音频那些错误不能碰。
+   * 尤其是录音被拒(`Session has ended`):那条 catch 随后就 `schedulePoll(0)`,
+   * 若在 acceptReport 里无差别清 error,学生会在唯读锁合上的同一瞬间失去
+   * 「为什么我录不了」的唯一解释。
+   */
+  let pollErrorShown = false
+  let pollInFlight = false
+  let practiceTimer: number | null = null
+  let reportAbort: AbortController | null = null
+  let disposed = false
+  let waitingForVisible = false
+
+  let completionPromise: Promise<void> | null = null
+  let completionNotified = false
+  let completionSucceeded = false
+  let hasFetchedAfterComplete = false
+
+  /**
+   * 已录完的段落 = 后端已知有结果(generating/completed/failed),或本地刚 ack 过。
+   * 本地集合让最后一段 ack 后按钮立刻可用,不必等下一轮 GET。绝不本地伪造分数。
+   */
+  const canViewReport = computed(() => {
+    const current = report.value
+    if (!current || current.paragraphs.length === 0) return false
+    return current.paragraphs.every(
+      paragraph => paragraph.status !== 'pending' || recordedParagraphs.has(paragraph.idx),
+    )
+  })
+
+  /** 这一轮是否已结束。showReport 关闭时,视图据此把练习页转为只读(spec §9.2.1)。 */
+  const sessionEnded = computed(() => report.value?.status === 'completed')
+
+  /**
+   * 整篇总评已经开始生成(`overallStatus` 非 null = generating / completed / failed)。
+   *
+   * 后端 2026-07-27 起在这之后**拒绝重录**(409 `Report already generated`):放行不是多烧一次
+   * LLM,而是作废一份学生/老师可能已经看到的报告。视图据此提前锁掉录音,否则那颗按钮按下去
+   * 必然报错。判据是 `overallStatus` 而不是 `sessionEnded` —— 静默预热把 status 翻成
+   * completed 的那一刻总评还没开始,那时重录仍然允许。
+   */
+  const overallStarted = computed(() => (report.value?.overallStatus ?? null) !== null)
+
+  // ---------------- session ----------------
+
+  async function startSession() {
+    /**
+     * 双击「开始朗读」= 两个 `POST /session`,而后端的 get-or-create 既没有锁也没有唯一约束
+     * (`article_service.py` `get_or_create_session`):两个并发请求会双双查不到既有 session,
+     * 然后各插一行。编辑态老师预览(`userId=null`)更是**必然**两行 —— `_latest_session`
+     * 对空 user 早退返回 None 是设计如此(spec §4.5)。
+     *
+     * 多出来的那一行不是无害的垃圾:它一路算进 §4.3 B 的「N 名学生正在练习中」直到
+     * `expires_at` 把它扫掉,老师明明只有一个学生在读却被问「2 名学生正在练习中」;
+     * `onSessionStarted` 也会跑两次,宿主的作业表因此多一笔(§9.2.4)。
+     *
+     * 闩放在这里而不是只置灰按钮:按钮只挡得住鼠标,挡不住程序化调用(承 §3.2.1
+     * `guardBeforeApply` 早退同一条理由)。对齐话题讨论的 `sessionCreating`
+     * (`TopicDiscussionPreview.vue:33`)。
+     */
+    if (starting.value) return
+    starting.value = true
+    error.value = null
+    sessionInvalid.value = false
+    // 每一轮都从干净的完成态起步。reset() 也清这四个,但 startSession 不该依赖
+    // 「上一次一定 reset 过、且没有在途的 POST 在 reset 之后把它们设回来」
+    completionPromise = null
+    completionNotified = false
+    completionSucceeded = false
+    hasFetchedAfterComplete = false
+    try {
+      const next = await deps.createSession(toValue(options.configId), toValue(options.userId))
+      report.value = next
+      stage.value = 'reading'
+      currentParagraphIdx.value = firstPendingIdx(next)
+      options.onSessionStarted?.(next.sessionId)
+      startPracticeTimer()
+      if (next.paragraphs.some(paragraph => paragraph.status === 'generating')) schedulePoll(0)
+    } catch (cause) {
+      error.value = friendlyArticleError(cause)
+    } finally {
+      // 只在这里放闩。reset() **故意不清它** —— 请求还在飞的时候把闩打开,
+      // 第二次点击照样能发出第二个 POST,等于没修。
+      starting.value = false
+    }
+  }
+
+  function firstPendingIdx(current: ArticleReadingReport): number {
+    const pending = current.paragraphs.find(paragraph => paragraph.status === 'pending')
+    return pending ? pending.idx : 0
+  }
+
+  /**
+   * 刷新页面 resume:用后端最新 session 的 report 直接水合引擎,跳过 ready 起始页。
+   * reading → 回到朗读视图;completed → 回到结果页(overall 未就绪则走一次性 loading 门轮询)。
+   * 不触发 onSessionStarted(那是真正开始时记作业用);presence 广播由调用方负责。
+   */
+  function resumeFromReport(next: ArticleReadingReport) {
+    error.value = null
+    sessionInvalid.value = false
+    report.value = next
+
+    if (next.status === 'completed') {
+      // 已完成:complete 不必再 POST;标记成已完成态,防止 preheat 重发或重复通知
+      completionPromise = Promise.resolve()
+      completionSucceeded = true
+      completionNotified = true
+      hasFetchedAfterComplete = false
+      if (!toValue(options.showReport)) {
+        // 关闭「展示学习报告」:完成也不进结果页,留在练习页(只读锁由 sessionEnded 触发,spec §9.2.1)
+        stage.value = 'reading'
+        currentParagraphIdx.value = firstPendingIdx(next)
+        return
+      }
+      if (isFinalReportReady(next)) {
+        stage.value = 'report'
+      } else {
+        // 段落已落定但整篇总评仍 generating → 走一次性 loading 门 + 退避轮询
+        stage.value = 'report-loading'
+        schedulePoll(0)
+      }
+      return
+    }
+
+    // reading:回到朗读视图。计时器改按时间戳算之后,resume 重启是安全的 ——
+    // 它从 expiresAt 反推起点,算出的是真实已用时长,不会「刷新即重置」。
+    stage.value = 'reading'
+    currentParagraphIdx.value = firstPendingIdx(next)
+    startPracticeTimer()
+    if (next.paragraphs.some(paragraph => paragraph.status === 'generating')) schedulePoll(0)
+  }
+
+  // ---------------- recording ----------------
+
+  async function startRecording() {
+    /**
+     * `recordingState` 要等 getUserMedia + addModule 落地才翻成 'recording',那期间
+     * 录音按钮是亮的、idle 那一栏也还在 —— 少了 startingRecording 这道闩,连点两下
+     * 就会**建起两套采集图**:两个 worklet 的 onmessage 往同一个 pcmChunks 里塞,
+     * 采样数翻倍且两路交错,本地回放听起来就是「慢一半 + 刺啦声」;`onChunk` 同样被
+     * 两路调用,上行 WS 与后端存的 WAV 一并被污染。第二次调用还会覆盖 activeStream,
+     * 把第一条 WS 连同它的 attempt 一起漏在半空。
+     */
+    if (!report.value || recordingState.value === 'recording' || startingRecording) return
+    startingRecording = true
+    try {
+      await beginRecording()
+    }
+    finally {
+      startingRecording = false
+    }
+  }
+
+  async function beginRecording() {
+    if (!report.value) return
+    error.value = null
+    // 录音器先于 socket 就绪吐 chunk 是常态,先缓在本地,建流后补推,开头不能丢
+    const beforeStream: ArrayBuffer[] = []
+    recorder.onChunk.value = chunk => {
+      if (activeStream) activeStream.pushChunk(chunk)
+      else beforeStream.push(chunk)
+    }
+
+    try {
+      await recorder.startRecording()
+    } catch (cause) {
+      recorder.onChunk.value = null
+      error.value = friendlyArticleError(cause)
+      return
+    }
+
+    activeStream = deps.createStream({
+      sessionId: report.value.sessionId,
+      paragraphIdx: currentParagraphIdx.value,
+      sampleRate: recorder.sampleRate.value,
+    })
+    for (const chunk of beforeStream) activeStream.pushChunk(chunk)
+    beforeStream.length = 0
+    recordingState.value = 'recording'
+  }
+
+  async function finishRecording() {
+    if (!activeStream || finishingRecording) return
+    finishingRecording = true
+    recordingState.value = 'processing'
+    const paragraphIdx = currentParagraphIdx.value
+    const stream = activeStream
+    const sessionId = report.value?.sessionId
+    /**
+     * 两个 await 期间「重新开始」都可能跑过 reset(),把引擎带到另一轮。
+     * 之后任何回写都是污染:blob 会落进刚清空的 Map(新一轮那段带着「已录制」徽章
+     * 和一个播放上一次录音的回放按钮),activeStream/recordingState 则会踩掉新一轮。
+     */
+    const stillCurrent = () => report.value?.sessionId === sessionId
+
+    try {
+      const wav = await recorder.stopRecording()
+      if (!stillCurrent()) {
+        stream.abort('Article session reset while finishing')
+        return
+      }
+      activeStream = null
+      await stream.stop()
+      if (!stillCurrent()) return
+      localAudioByParagraph.value.set(paragraphIdx, wav)
+      recordedParagraphs.add(paragraphIdx)
+      advanceAfterAck(paragraphIdx)
+      schedulePoll(0)
+    }
+    catch (cause) {
+      stream.abort('Article recording attempt failed')
+      if (!stillCurrent()) return
+      activeStream = null
+      if (isArticleSessionGone(cause)) sessionInvalid.value = true
+      error.value = friendlyArticleError(cause)
+      recordingState.value = 'idle'
+      // 失败可能是「后端说这一轮已到期」(409 Session has ended,§4.4)。补一次查询,
+      // 让 status: completed 经 acceptReport 落地 —— 否则学生要等到下一次主动动作
+      // 才会发现自己已经结束了。
+      schedulePoll(0)
+    }
+    finally {
+      if (stillCurrent()) recorder.onChunk.value = null
+      finishingRecording = false
+    }
+  }
+
+  /**
+   * 中途放弃本次录音:abort 掉流(后端 attempt 已 +1,旧分数保留、无任务写入),
+   * 回到 idle。不发 stop,因此不会产生评分任务。
+   */
+  async function cancelRecording() {
+    if (!activeStream || finishingRecording) return
+    const stream = activeStream
+    activeStream = null
+    stream.abort('Article recording cancelled by user')
+    try {
+      await recorder.stopRecording()
+    } catch {
+      // 录音器已经停了或从未真正启动,忽略
+    }
+    recorder.onChunk.value = null
+    recordingState.value = 'idle'
+    error.value = null
+  }
+
+  /** ack 到手即前进,不等分数(异步 UX 决策 A)。最后一段则留在原地。 */
+  function advanceAfterAck(paragraphIdx: number) {
+    recordingState.value = 'idle'
+    const current = report.value
+    if (!current) return
+    const next = current.paragraphs.find(
+      paragraph => paragraph.idx > paragraphIdx && paragraph.status === 'pending'
+        && !recordedParagraphs.has(paragraph.idx),
+    )
+    if (next) currentParagraphIdx.value = next.idx
+  }
+
+  function rerecordParagraph(idx: number) {
+    // 后端重录会重开 session 并作废整篇总评,本地完成态必须一并清掉
+    recordedParagraphs.delete(idx)
+    localAudioByParagraph.value.delete(idx)
+    completionPromise = null
+    completionNotified = false
+    completionSucceeded = false
+    hasFetchedAfterComplete = false
+    currentParagraphIdx.value = idx
+    stage.value = 'reading'
+    error.value = null
+  }
+
+  function selectParagraph(idx: number) {
+    if (recordingState.value === 'recording') return
+    currentParagraphIdx.value = idx
+  }
+
+  // ---------------- timers ----------------
+
+  // 单段 180s 安全阀:只停当前段,不结束整篇
+  watch(() => recorder.recordingDuration.value, duration => {
+    if (duration >= MAX_PARAGRAPH_SECONDS && recordingState.value === 'recording') {
+      void finishRecording()
+    }
+  })
+
+  /** report 上的截止时刻,毫秒;无截止 / 解析不出来 → null。 */
+  function deadlineMs(current: ArticleReadingReport | null): number | null {
+    if (!current?.expiresAt) return null
+    const parsed = Date.parse(current.expiresAt)
+    return Number.isNaN(parsed) ? null : parsed
+  }
+
+  /**
+   * 练习计时器:**纯展示**(spec §9.2.3)。
+   *
+   * 值从 `expiresAt` 与配置时长反推起点、按墙上时钟算,不再逐 tick 累加 —— 后台标签页
+   * 的 setInterval 会被浏览器节流(可能一分钟才跑一次),累加法切回来时显示的数字是错的。
+   * 同样因为这个算法,刷新页面 resume 时可以直接重启计时器:它会自己算出真实已用时长。
+   *
+   * 到点**什么都不做**,只停自己 + 发一次 GET /report。到期由后端判定(§4.4);
+   * 本地时钟只是触发器,不是判据:算早了后端说还没到,算晚了后端早已标好,两种都不会错。
+   */
+  function startPracticeTimer() {
+    stopPracticeTimer()
+    const expiresAtMs = deadlineMs(report.value)
+    if (expiresAtMs === null) {
+      elapsedPracticeSeconds.value = null
+      return
+    }
+    const capSeconds = Math.max(0, Math.round(toValue(options.durationMinutes) * 60))
+    if (capSeconds <= 0) {
+      elapsedPracticeSeconds.value = null
+      return
+    }
+    const startedAtMs = expiresAtMs - capSeconds * 1000
+    const tick = () => {
+      const elapsed = Math.floor((deps.now() - startedAtMs) / 1000)
+      elapsedPracticeSeconds.value = Math.min(capSeconds, Math.max(0, elapsed))
+      if (elapsed < capSeconds) return
+      stopPracticeTimer()
+      // 到点问一次。schedulePoll 复用既有的后台标签页挂起与去重逻辑;
+      // 没有 generating 段时 acceptReport 会 stopPolling(),所以确实只有这一次。
+      schedulePoll(0)
+    }
+    tick()
+    practiceTimer = deps.setInterval(tick, 1000)
+  }
+
+  /** 清掉练习页上的错误横幅。重试无从谈起的那些错(麦克风、示范音频、轮询)只能这样收走。 */
+  function dismissError() {
+    error.value = null
+    pollErrorShown = false
+  }
+
+  function stopPracticeTimer() {
+    if (practiceTimer !== null) {
+      deps.clearInterval(practiceTimer)
+      practiceTimer = null
+    }
+  }
+
+  // ---------------- completion ----------------
+
+  function completeOnce(): Promise<void> {
+    if (!report.value) return Promise.reject(new Error('Article session is not ready'))
+    if (!completionPromise) {
+      const sessionId = report.value.sessionId
+      completionPromise = deps.completeSession(sessionId).then(() => {
+        // POST 在途时学生可能已经「重新开始」。reset() 清过这些标记,这里绝不能再设回去 ——
+        // 否则下一轮开局就带着 completionNotified=true,那一轮真正完成时 onSessionCompleted
+        // 不再触发,老师端整轮都收不到刷新(复合放大「重新开始不广播」那条)。
+        if (report.value?.sessionId !== sessionId) return
+        completionSucceeded = true
+        hasFetchedAfterComplete = false
+        if (!completionNotified) {
+          completionNotified = true
+          options.onSessionCompleted?.(sessionId)
+        }
+      })
+    }
+    return completionPromise
+  }
+
+  function allParagraphsRecorded(current: ArticleReadingReport): boolean {
+    return current.paragraphs.every(paragraph =>
+      paragraph.status === 'generating'
+        || paragraph.status === 'completed'
+        || paragraph.status === 'failed',
+    )
+  }
+
+  /** 全段评完就静默 complete,让老师端列表提前刷新;不改 stage。 */
+  async function preheatIfEligible(current: ArticleReadingReport) {
+    if (!allParagraphsRecorded(current)) return
+    if (current.paragraphs.some(paragraph => paragraph.status === 'generating')) return
+    try {
+      await completeOnce()
+    } catch {
+      completionPromise = null
+    }
+  }
+
+  async function completeAndShowReport() {
+    if (recordingState.value === 'recording') await finishRecording()
+    stopPracticeTimer()
+    stage.value = 'report-loading'
+    error.value = null
+    try {
+      await completeOnce()
+      schedulePoll(0)
+    }
+    catch (cause) {
+      completionPromise = null
+      if (isArticleSessionGone(cause)) sessionInvalid.value = true
+      error.value = friendlyArticleError(cause)
+    }
+  }
+
+  /**
+   * 「重新开始」(spec §4.2):把当前 session 标作废,回起始页。
+   * 不在这里建新 session —— 学生点「开始朗读」时由 startSession 的 get-or-create 建,
+   * 那样 created_at 才等于真正的开始时刻。
+   * @returns 成功与否;失败时不重置,调用方负责提示。
+   */
+  async function restartSession(): Promise<boolean> {
+    const sessionId = report.value?.sessionId
+    if (!sessionId) return false
+    try {
+      await deps.abandonSession(sessionId)
+    }
+    catch (cause) {
+      console.error('[article-reading] abandon failed:', cause)
+      return false
+    }
+    reset()
+    // 库里已经是「未开始」了(§4.2 各处查询排除 abandoned),但老师端只在收到学生广播时
+    // 才重取;不发这一条,他会一直看着一个不再作数的「已完成 + 分数」,点进去还是作废那轮的报告。
+    options.onSessionAbandoned?.(sessionId)
+    return true
+  }
+
+  async function acceptReport(next: ArticleReadingReport) {
+    const wasReading = report.value?.status === 'reading'
+    report.value = next
+
+    /**
+     * 后端单方面把这一轮结束了(到期,spec §4.4)。这是学生结束的**唯一信号**,
+     * 而它可能来自任何一次响应 —— 到点那次查询、恢复探针、或学生自己的动作触发的轮询,
+     * 所以反应必须写在这个统一入口,不能绑在某一次调用上。
+     *
+     * 下面那句 `if (!completionSucceeded) return` 原本假定「只有我能结束 session」;
+     * 判定权搬到后端后这个前提不再成立,必须先在这里补齐本地的完成态,否则这次响应
+     * 会被那句早退直接吞掉。
+     *
+     * 放在 preheatIfEligible **之前**:preheat 走的是 completeOnce(),而这里已经把
+     * completionPromise 填好,于是它自然成为 no-op。反过来先 preheat 的话,它会先
+     * 把 completionSucceeded 置真、让下面的条件失效,还白发一次没有意义的 POST,
+     * 并因为「POST 之后还没 GET 过」再多轮询一轮。
+     */
+    if (wasReading && next.status === 'completed' && !completionSucceeded) {
+      stopPracticeTimer()
+      completionPromise = Promise.resolve()
+      completionSucceeded = true
+      // 这个响应本身就是「结束之后拿到的」—— 后端在这次查询里完成的收口
+      hasFetchedAfterComplete = true
+      if (!completionNotified) {
+        completionNotified = true
+        options.onSessionCompleted?.(next.sessionId)
+      }
+      // showReport 关闭时不进 loading 门:留在练习页,由 sessionEnded 合上只读锁(§9.2.1)
+      if (toValue(options.showReport)) stage.value = 'report-loading'
+    }
+
+    await preheatIfEligible(next)
+
+    if (!completionSucceeded) {
+      if (!next.paragraphs.some(paragraph => paragraph.status === 'generating')) stopPolling()
+      return
+    }
+
+    // POST 之前抓到的 report 不算数:必须至少有一次 POST 之后发起的 GET
+    if (!hasFetchedAfterComplete) {
+      schedulePoll(0)
+      return
+    }
+
+    if (isFinalReportReady(next)) {
+      if (stage.value === 'report-loading') stage.value = 'report'
+      stopPolling()
+    } else {
+      // 段落已全部落定,但整篇 overallStatus 仍是 'generating'(complete 触发的后台总评
+      // LLM 未完成)。此时没有 generating 段,pollOnce 的兜底重排不会触发,必须在这里继续
+      // 退避轮询,否则 report-loading 门会永久卡住等一个再也不来的 GET。
+      schedulePoll(pollAttempt + 1)
+    }
+  }
+
+  // ---------------- polling ----------------
+
+  function schedulePoll(attempt: number) {
+    if (disposed) return
+    // attempt === 0 = 重新起一轮(录完一段、点重试、到点查一次、resume)。
+    // 这是用户动作或新事件带来的,给它一份新的失败预算 —— 否则上一次敲到上限之后,
+    // 之后每一条新链都会在第一次失败时立刻再次停手。
+    if (attempt === 0) pollFailures = 0
+    pollAttempt = attempt
+    if (pollTimer !== null) deps.clearTimeout(pollTimer)
+    pollTimer = deps.setTimeout(() => {
+      pollTimer = null
+      void pollOnce()
+    }, attempt === 0 ? 0 : pollDelayMs(attempt - 1))
+  }
+
+  function stopPolling() {
+    if (pollTimer !== null) {
+      deps.clearTimeout(pollTimer)
+      pollTimer = null
+    }
+    pollAttempt = 0
+    // 不清 pollFailures:停手那一刻它正好等于上限,清了就没意义了。
+    // 由重新起一轮轮询的入口负责归零 —— 见 schedulePoll(0)。
+  }
+
+  function onVisible() {
+    deps.document.removeEventListener('visibilitychange', onVisible)
+    waitingForVisible = false
+    // 后台期间不计入重试次数,回到前台立即补一次
+    schedulePoll(0)
+  }
+
+  async function pollOnce() {
+    if (disposed || pollInFlight || !report.value) return
+
+    // 后台标签页不发 GET,挂起等 visibilitychange,且不推进退避
+    if (deps.document.hidden) {
+      if (!waitingForVisible) {
+        waitingForVisible = true
+        deps.document.addEventListener('visibilitychange', onVisible)
+      }
+      return
+    }
+
+    pollInFlight = true
+    const requestStartedAfterComplete = completionSucceeded
+    reportAbort = new AbortController()
+    try {
+      const next = await deps.getReport(report.value.sessionId, reportAbort.signal)
+      if (disposed) return
+      // 拿到响应 = 后端还活着,连续失败计数归零
+      pollFailures = 0
+      // 上一次轮询失败留下的横幅已经不成立了,收走它(只收自己竖的那面)
+      if (pollErrorShown) {
+        error.value = null
+        pollErrorShown = false
+      }
+      if (requestStartedAfterComplete) hasFetchedAfterComplete = true
+      const wasPolling = pollTimer
+      await acceptReport(next)
+      // acceptReport 未自行安排下一轮,且仍有段落在生成 → 退避重试
+      if (pollTimer === wasPolling && next.paragraphs.some(p => p.status === 'generating')) {
+        schedulePoll(pollAttempt + 1)
+      }
+    } catch (cause) {
+      if (disposed || (cause instanceof Error && cause.name === 'AbortError')) return
+      // 瞬时失败不退出 loading 门,退避后再试
+      error.value = friendlyArticleError(cause)
+      pollErrorShown = true
+      pollFailures += 1
+      if (pollFailures >= MAX_POLL_FAILURES) {
+        // 后端连续不应答,别再每 8 秒敲一次敲到学生关页面。横幅留在原地,
+        // report-loading 门那个手动重试按钮是学生的出口。
+        stopPolling()
+        return
+      }
+      schedulePoll(pollAttempt + 1)
+    } finally {
+      reportAbort = null
+      pollInFlight = false
+    }
+  }
+
+  // ---------------- audio ----------------
+
+  /**
+   * 全篇(示范 + 我的录音)共用一个 player,所以「点别的按钮」天然会 stop 掉上一段。
+   * 这里只负责另一半:**同一颗按钮再点一次 = 停**。loading 也要算进来,否则取流/
+   * 合成的那 1-3 秒里按钮按不停,只会排队再放一遍(对齐话题讨论 DialogueChatView:898)。
+   */
+  function isBusyWith(id: string): boolean {
+    return player.playingId.value === id || player.loadingId.value === id
+  }
+
+  async function playOwnAudio(idx: number) {
+    const id = `own-${idx}`
+    if (isBusyWith(id)) {
+      player.stop()
+      return
+    }
+    const localBlob = localAudioByParagraph.value.get(idx)
+    if (localBlob) {
+      await player.play(id, { kind: 'blob', blob: localBlob })
+      return
+    }
+    const url = report.value?.paragraphs.find(paragraph => paragraph.idx === idx)?.audioUrl
+    if (!url) return
+    await player.play(id, { kind: 'url', url })
+  }
+
+  async function playDemoAudio(idx: number) {
+    const id = `demo-${idx}`
+    if (isBusyWith(id)) {
+      player.stop()
+      return
+    }
+    const sessionId = report.value?.sessionId
+    if (!sessionId) return
+    const cacheKey = `${sessionId}:${idx}`
+    // 取 URL 交给 player(lazy-url),不在这儿先 await:缓存 miss 时后端同步合成要
+    // 1-3s,在外面等的话这段时间里上一段录音还在响、这颗按钮也不转圈 —— 学生看到的
+    // 就是「点了没反应,旧的还在放」。进了 play() 才有 stop() 和 loadingId。
+    await player.play(id, {
+      kind: 'lazy-url',
+      resolve: async () => {
+        const cached = demoUrlCache.get(cacheKey)
+        if (cached) return cached
+        try {
+          const res = await deps.getDemoAudio(sessionId, idx)
+          demoUrlCache.set(cacheKey, res.url)
+          return res.url
+        } catch (cause) {
+          // 横幅仍由引擎竖:player 只有 errorId,卡片上没有它的出口
+          error.value = friendlyArticleError(cause)
+          throw cause
+        }
+      },
+    })
+  }
+
+  function stopAudio() {
+    player.stop()
+  }
+
+  // ---------------- lifecycle ----------------
+
+  function reset() {
+    stopPolling()
+    pollFailures = 0
+    pollErrorShown = false
+    stopPracticeTimer()
+    reportAbort?.abort()
+    reportAbort = null
+    activeStream?.abort('Article session reset')
+    activeStream = null
+    player.stop()
+    recorder.onChunk.value = null
+    // 录音器必须真的拆掉,不能只摘 onChunk:mediaStream 的 track、AudioContext、
+    // AudioWorkletNode 和时长 interval 都还活着。留着的后果不是「泄漏一点内存」——
+    // 下一次 startRecording 会挂上第二个时长 interval(180s 单段安全阀在 ~90 真实秒就开火),
+    // 而孤儿 worklet 的 onmessage 闭包着同一个 chunk 缓冲与 onChunk ref,会继续把音频
+    // 灌进**下一段**录音,上传的 WAV 与 WS 流里都是两套采集图交织的声音。
+    // 放在 reset() 而不是 restartSession():configId 切换与老师端「应用配置」同样只走
+    // reset(),三条路一次修完;completeAndShowReport / cancelRecording 本就先过录音器。
+    recorder.cleanup()
+    recordedParagraphs.clear()
+    localAudioByParagraph.value.clear()
+    demoUrlCache.clear()
+    completionPromise = null
+    completionNotified = false
+    completionSucceeded = false
+    hasFetchedAfterComplete = false
+    finishingRecording = false
+    stage.value = 'ready'
+    report.value = null
+    currentParagraphIdx.value = 0
+    recordingState.value = 'idle'
+    elapsedPracticeSeconds.value = null
+    error.value = null
+    sessionInvalid.value = false
+    if (waitingForVisible) {
+      deps.document.removeEventListener('visibilitychange', onVisible)
+      waitingForVisible = false
+    }
+  }
+
+  function dispose() {
+    disposed = true
+    reset()
+  }
+
+  onUnmounted(() => dispose())
+
+  return {
+    stage,
+    starting,
+    report,
+    currentParagraphIdx,
+    recordingState,
+    recordingDuration: recorder.recordingDuration,
+    elapsedPracticeSeconds,
+    sessionEnded,
+    overallStarted,
+    canViewReport,
+    localAudioByParagraph,
+    error,
+    sessionInvalid,
+    startSession,
+    resumeFromReport,
+    startRecording,
+    finishRecording,
+    cancelRecording,
+    rerecordParagraph,
+    selectParagraph,
+    completeAndShowReport,
+    dismissError,
+    restartSession,
+    playOwnAudio,
+    playDemoAudio,
+    stopAudio,
+    playingId: player.playingId,
+    loadingId: player.loadingId,
+    reset,
+    dispose,
+  }
+}

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

@@ -0,0 +1,215 @@
+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 }
+  /**
+   * URL 要先问一次后端才知道(文章示范音频:缓存 miss 时后端同步合成 1-3s)。
+   * 解析必须发生在 play() 内部,不能由调用方先 await 再进来 —— 只有进了 play()
+   * 才算「这一次播放开始了」:前一段音频才会被 stop(),按钮才转得起圈,
+   * 中途改点别的按钮时这次解析才会被 abort。
+   */
+  | { kind: 'lazy-url'; resolve: (signal: AbortSignal) => Promise<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
+
+  /**
+   * 已经建好的 object URL,按「这段音频是什么」缓存。重复播放同一段因此既不重新
+   * 下载、也不再堆一个新的 object URL(原本每按一次播放都两样都做一遍,而 object
+   * URL 要到卸载才 revoke —— 学生把一段示范听五遍就是五次下载加五条泄漏)。
+   *
+   * key 用**解析出来的那条远端 URL**,不用 id:`own-{idx}` 这种 id 在学生重录之后
+   * 仍然是同一个,指向的却是新的 audioUrl,按 id 缓存会把上一次的录音放给他听。
+   * URL 变了就是另一个 key,这个失效模式从根上不存在。tts 没有 URL,按 `tts:${id}`。
+   */
+  const objectUrls = new Map<string, string>()
+  /** 本地录音(kind: 'blob')没有 URL 可当 key,按 Blob 身份缓存。 */
+  const blobObjectUrls = new WeakMap<Blob, string>()
+  /** 建过的全部 object URL,卸载时统一 revoke。 */
+  const createdUrls: string[] = []
+
+  function trackUrl(url: string): string {
+    createdUrls.push(url)
+    return url
+  }
+
+  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 objectUrl: string
+
+      if (source.kind === 'blob') {
+        let cached = blobObjectUrls.get(source.blob)
+        if (!cached) {
+          cached = trackUrl(URL.createObjectURL(source.blob))
+          blobObjectUrls.set(source.blob, cached)
+        }
+        objectUrl = cached
+      }
+      else if (source.kind === 'url' || source.kind === 'lazy-url') {
+        synthAbort = new AbortController()
+        const signal = synthAbort.signal
+        const remoteUrl = source.kind === 'url' ? source.url : await source.resolve(signal)
+        // 解析期间被打断(stop() 或换了个按钮)—— 那条 URL 不再属于任何一次播放
+        if (loadingId.value !== id) return
+
+        const cached = objectUrls.get(remoteUrl)
+        if (cached) {
+          // 第二次起完全不碰网络。lazy-url 的 resolve 也已经命中引擎那层的 URL 缓存,
+          // 所以整条路上只剩一次 Map 查询。
+          synthAbort = null
+          objectUrl = cached
+        }
+        else {
+          const res = await fetch(remoteUrl, { signal })
+          synthAbort = null
+          if (loadingId.value !== id) return
+          if (!res.ok) throw new Error(`fetch audio failed: ${res.status}`)
+          objectUrl = trackUrl(URL.createObjectURL(await res.blob()))
+          objectUrls.set(remoteUrl, objectUrl)
+        }
+      }
+      else {
+        const key = `tts:${id}`
+        const cached = objectUrls.get(key)
+        if (cached) {
+          objectUrl = cached
+        }
+        else {
+          synthAbort = new AbortController()
+          const blob = await synthesize(source.text, synthAbort.signal)
+          synthAbort = null
+          // We may have been interrupted while awaiting (loadingId changed).
+          if (loadingId.value !== id) return
+          objectUrl = trackUrl(URL.createObjectURL(blob))
+          objectUrls.set(key, objectUrl)
+        }
+      }
+
+      const audio = new Audio(objectUrl)
+      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 createdUrls) {
+      try { URL.revokeObjectURL(url) } catch { /* ignore */ }
+    }
+    createdUrls.length = 0
+    objectUrls.clear()
+  })
+
+  return {
+    playingId,
+    loadingId,
+    errorId,
+    play,
+    stop,
+  }
+}

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

@@ -0,0 +1,368 @@
+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 同理。
+ */
+
+/**
+ * 想要的采样率。Azure 发音评估原生就是 16kHz —— 送 48kHz 不会让评分更准,只是把
+ * 重采样挪到别人那边做,而每一段录音的三个成本都是 3 倍:
+ *
+ *            记忆体(180s 单声道)   上行频宽      S3 存储
+ *   48kHz         16.5 MB         96 KB/s        ×3
+ *   16kHz          5.5 MB         32 KB/s        ×1
+ *
+ * 一个班 30 人同时录,上行是 2.8 MB/s 对 0.9 MB/s —— 在学校 wifi 上这一项比记忆体
+ * 更容易先出事。
+ */
+const TARGET_SAMPLE_RATE = 16000
+
+/**
+ * 优先要 16kHz,拿不到就用硬件默认。
+ *
+ * 原来这里是写死的 `new AudioContext()`,注解写着「iOS Safari 不支持自定义采样率」。
+ * 那在 Safari 14.1(2021)之前是对的。但与其去赌教室里那台 iPad 的版本号,不如让
+ * 「赌输」变成一件无所谓的事 —— **整条链路带的都是真实采样率**:WS handshake 送
+ * `recorder.sampleRate.value`(useArticleReadingEngine.ts),WAV header 写
+ * `audioContext.sampleRate`(encodeWAV),后端的评估器格式与字节上限又是从那个
+ * handshake 推出来的。所以退回 48kHz 时一切照常,只是没省到。
+ *
+ * 两种失败都要接住,而且形状不同:
+ *   ① 构造抛(NotSupportedError)→ 这里 catch,退回默认;
+ *   ② 构造成功但浏览器无视这个提示,sampleRate 仍是硬件值 → 不需要任何处理,
+ *      因为下面读的是 `audioContext.sampleRate` 而不是 TARGET_SAMPLE_RATE。
+ * 千万别把 sampleRate.value 写成常数 —— 那会让 ② 变成一个静默的谎:后端按 16kHz
+ * 解 48kHz 的数据,回放速度慢三倍,而没有任何一层会报错。
+ */
+function createAudioContext(): AudioContext {
+  try {
+    return new AudioContext({ sampleRate: TARGET_SAMPLE_RATE })
+  }
+  catch (err) {
+    console.debug('[recorder] 16kHz AudioContext rejected, using hardware default', err)
+    return new AudioContext()
+  }
+}
+
+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
+  // 墙钟起点。用它而不是 recordingDuration —— 采集图若被建了两套,那个 interval 也会
+  // 跟着翻倍,两边一起错就比不出问题。samplesSeconds / wallSeconds 明显大于 1 = 采样重复。
+  let debugStartedAt = 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')
+    }
+
+    // 优先 16kHz(见 createAudioContext)。sampleRate.value 一定是**实际拿到的**那个,
+    // 不是我们要的那个 —— 浏览器可以无视这个提示,而下游全部按这个值走。
+    audioContext = createAudioContext()
+    sampleRate.value = audioContext.sampleRate
+    // createMediaStreamSource 会由浏览器在 graph 层把麦克风的硬件采样率重采样到
+    // context 的采样率,带正规的低通。别改成自己抽样:无低通的降采样会混叠,而混叠
+    // 的表现是发音分数**普遍偏低** —— 没有错误、没有日志,只有「这批学生分数怎么低了」。
+    const source = audioContext.createMediaStreamSource(mediaStream)
+
+    // 诊断录音管线是否健康:mic track 状态 + AudioContext 状态
+    console.debug('[recorder] startRecording', {
+      contextState: audioContext.state,
+      requestedSampleRate: TARGET_SAMPLE_RATE,
+      contextSampleRate: audioContext.sampleRate,
+      sampleRateHonoured: audioContext.sampleRate === TARGET_SAMPLE_RATE,
+      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
+    debugStartedAt = Date.now()
+
+    // 收集 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 停止。onmessage 必须当场摘掉:'stop' 是投递到音频线程的异步消息,
+    // 在它生效之前音频线程还会再吐几块,那几块会落进**下一段**录音的 pcmChunks
+    // (pcmChunks 是跨 start/stop 复用的同一个闭包变量)。
+    workletNode.port.onmessage = null
+    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
+    const wallSeconds = (Date.now() - debugStartedAt) / 1000
+    const samplesSeconds = totalSamples / sampleRate
+    console.debug('[recorder] stopRecording final', {
+      chunks: debugChunkCount,
+      totalSamples,
+      sampleRate,
+      duration: samplesSeconds.toFixed(2) + 's',
+      wallSeconds: wallSeconds.toFixed(2) + 's',
+      // 1.0 = 正常。≈2 就是同时跑着两套采集图,采样重复 → 回放慢一半且刺啦
+      sampleRatio: (samplesSeconds / Math.max(wallSeconds, 0.001)).toFixed(2),
+      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()
+
+    // stopRecording() 之外的路径(reset / 卸载 / startRecording 失败)也会走到这里,
+    // 那时 worklet 还挂着 onmessage。不摘掉它,孤儿 worklet 会继续往 pcmChunks 灌音频。
+    if (workletNode) {
+      workletNode.port.onmessage = null
+      try { workletNode.port.postMessage('stop') } catch { /* context 已关闭 */ }
+      try { workletNode.disconnect() } catch { /* 已断开 */ }
+      workletNode = null
+    }
+
+    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))
+  }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません