Compare commits

..

5 Commits

Author SHA1 Message Date
ed97b30d51 feat(chat): 聊天滚动条美化 + Markdown 增强 — OverlayScrollbars/CodeHighlighter/代码复制/表格样式 2026-06-02 21:19:50 +08:00
26ecaadb26 feat(chat): 引入 @ant-design/x 组件 — Sender/Conversations/XMarkdown 替代手动拼装
- ConfigProvider → XProvider(ConsoleShell + test-utils)
- ChatSidebar → Conversations(menu dropdown + MoreOutlined trigger)
- ChatInputArea → Sender(footer 左右排版 + 模型 Select + 自动清空)
- Streamdown → XMarkdown(streaming hasNextChunk 映射 AI SDK 状态)
- CSS 清理 ~94 行废弃样式,新增统一布局规则
- 删除 streamdown 依赖
2026-06-02 18:31:31 +08:00
9c9afbd108 feat(chat): 优化聊天面板交互体验 — 推理折叠/智能滚动/工具中文名/代码块按钮 2026-06-02 08:43:26 +08:00
628b592577 fix: 修复 ChatPanel 无限重渲染 — useLogger 新增 bindings 参数保证引用稳定 2026-06-02 00:50:59 +08:00
29bf61f7a3 docs: 同步开发文档至代码最新状态 2026-06-01 23:33:16 +08:00
28 changed files with 1150 additions and 564 deletions

229
bun.lock
View File

@@ -10,6 +10,8 @@
"@ai-sdk/openai-compatible": "^2.0.48",
"@ai-sdk/react": "^3.0.195",
"@ant-design/icons": "^6.2.3",
"@ant-design/x": "^2.7.0",
"@ant-design/x-markdown": "^2.7.0",
"@sinclair/typebox": "^0.34.49",
"@tanstack/react-query": "^5.100.14",
"ai": "^6.0.193",
@@ -17,6 +19,8 @@
"antd": "^6.4.3",
"drizzle-orm": "^0.45.2",
"es-toolkit": "^1.47.0",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars-react": "^0.5.6",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"pino-roll": "^4.0.0",
@@ -24,7 +28,6 @@
"react-dom": "^19.2.6",
"react-router": "^7.15.1",
"recharts": "^3.8.1",
"streamdown": "^2.5.0",
"zod": "^4.4.3",
},
"devDependencies": {
@@ -86,6 +89,10 @@
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
"@ant-design/x": ["@ant-design/x@2.7.0", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/cssinjs": "^2.0.1", "@ant-design/cssinjs-utils": "^2.0.2", "@ant-design/fast-color": "^3.0.0", "@ant-design/icons": "^6.0.0", "@babel/runtime": "^7.25.6", "@rc-component/motion": "^1.1.6", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "lodash.throttle": "^4.1.1", "mermaid": "^11.12.1", "react-syntax-highlighter": "^16.1.0" }, "peerDependencies": { "antd": "^6.1.1", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-p5OtxQ9elbmeFRllGt1yj5wi6VHe41PIAmwrBU/OlaYydru5qIYsJzCS3DPRhkWkVdErU5oZwU74Z2oce2F5Uw=="],
"@ant-design/x-markdown": ["@ant-design/x-markdown@2.7.0", "", { "dependencies": { "clsx": "^2.1.1", "dompurify": "^3.2.6", "html-react-parser": "^5.2.13", "katex": "^0.16.22", "marked": "^15.0.12" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-tmuwbeulTD5nfO15VCb3mN13iCTT106626dVFxGjhj1tWnmLL+fIngyv3U8SOTq94+Baor6QdSWtPZluVKCIbw=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
@@ -516,14 +523,10 @@
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
@@ -534,12 +537,10 @@
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.6.2", "https://registry.npmmirror.com/@types/node/-/node-25.6.2.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
"@types/react": ["@types/react@19.2.15", "https://registry.npmmirror.com/@types/react/-/react-19.2.15.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
@@ -572,8 +573,6 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
@@ -658,8 +657,6 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="],
@@ -684,12 +681,8 @@
"caniuse-lite": ["caniuse-lite@1.0.30001792", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
@@ -842,14 +835,20 @@
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"dompurify": ["dompurify@3.4.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dot-prop": ["dot-prop@5.3.0", "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
"drizzle-kit": ["drizzle-kit@0.31.10", "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.10.tgz", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
@@ -928,16 +927,12 @@
"estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-copy": ["fast-copy@4.0.3", "https://registry.npmmirror.com/fast-copy/-/fast-copy-4.0.3.tgz", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -952,6 +947,8 @@
"fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -964,6 +961,8 @@
"for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -1012,20 +1011,8 @@
"hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"help-me": ["help-me@5.0.0", "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
@@ -1034,11 +1021,17 @@
"hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
"html-dom-parser": ["html-dom-parser@5.1.8", "", { "dependencies": { "domhandler": "5.0.3", "htmlparser2": "10.1.0" } }, "sha512-MCIUng//mF2qTtGHXJWr6OLfHWmg3Pm8ezpfiltF83tizPWY17JxT4dRLE8lykJ5bChJELoY3onQKPbufJHxYA=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"html-react-parser": ["html-react-parser@5.2.17", "", { "dependencies": { "domhandler": "5.0.3", "html-dom-parser": "5.1.8", "react-property": "2.0.2", "style-to-js": "1.1.21" }, "peerDependencies": { "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", "react": "0.14 || 15 || 16 || 17 || 18 || 19" }, "optionalPeers": ["@types/react"] }, "sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"husky": ["husky@9.1.7", "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
@@ -1206,112 +1199,26 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
"log-update": ["log-update@6.1.0", "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
"lru-cache": ["lru-cache@11.3.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
"lz-string": ["lz-string@1.5.0", "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdn-data": ["mdn-data@2.27.1", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"meow": ["meow@13.2.0", "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -1354,6 +1261,10 @@
"optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"overlayscrollbars": ["overlayscrollbars@2.16.0", "", {}, "sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg=="],
"overlayscrollbars-react": ["overlayscrollbars-react@0.5.6", "", { "peerDependencies": { "overlayscrollbars": "^2.0.0", "react": ">=16.8.0" } }, "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw=="],
"own-keys": ["own-keys@1.0.1", "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -1408,6 +1319,8 @@
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process-warning": ["process-warning@5.0.0", "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -1424,10 +1337,14 @@
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-property": ["react-property@2.0.2", "", {}, "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug=="],
"react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"react-router": ["react-router@7.15.1", "https://registry.npmmirror.com/react-router/-/react-router-7.15.1.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A=="],
"react-syntax-highlighter": ["react-syntax-highlighter@16.1.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA=="],
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"recharts": ["recharts@3.8.1", "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
@@ -1438,24 +1355,10 @@
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
@@ -1538,8 +1441,6 @@
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="],
"string-argv": ["string-argv@0.3.2", "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-convert": ["string-convert@0.2.1", "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
@@ -1552,8 +1453,6 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
@@ -1574,8 +1473,6 @@
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"thread-stream": ["thread-stream@4.2.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-4.2.0.tgz", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
"throttle-debounce": ["throttle-debounce@5.0.2", "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
@@ -1596,10 +1493,6 @@
"tr46": ["tr46@6.0.0", "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
@@ -1630,18 +1523,6 @@
"undici-types": ["undici-types@7.25.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.25.0.tgz", {}, "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
@@ -1652,20 +1533,12 @@
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@8.0.14", "https://registry.npmmirror.com/vite/-/vite-8.0.14.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
@@ -1708,8 +1581,6 @@
"zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -1772,6 +1643,8 @@
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"eslint/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "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" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1782,7 +1655,7 @@
"eslint-plugin-import/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"import-fresh/resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -1792,8 +1665,6 @@
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
@@ -1886,8 +1757,6 @@
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],

View File

@@ -42,6 +42,7 @@ AI 工具必须严格遵守以下全部约束。
| `src/server/` | 后端,禁止 import src/web/ |
| `src/server/db/` | 数据库层schema、connection、migration、DAO |
| `src/server/ai/` | AI Provider Registry + Agent + 工具 |
| `src/server/config/` | 配置子系统types、variables、issues、schema |
| `src/server/helpers/` | 跨路由工具响应格式化、URL 拼接 |
| `src/server/middleware/` | 参数校验 + 错误处理中间件 |
| `src/web/` | 前端,禁止 import src/server/ 运行时实现 |
@@ -54,7 +55,8 @@ AI 工具必须严格遵守以下全部约束。
### 类型与配置
- 共享类型唯一源头:`src/shared/api.ts`;应用常量唯一源头:`src/shared/app.ts`;版本号唯一源头:`package.json`
- 配置加载流程unknown → AuthoringConfig → NormalizedConfig → ValidatedConfig → ServerConfig。
- 配置加载流程unknown → AuthoringConfig → NormalizedConfig → ValidatedConfig → ResolvedConfig。
- 配置系统入口:`src/server/config.ts`(统一配置加载、运行时校验、默认值解析)。
- Ajv 严格拒绝模式:不类型转换、不注入默认值、不删除未知字段。
- 新增/修改配置字段必须同步更新 TypeBox schema、`config.schema.json`、测试和用户文档。

View File

@@ -6,8 +6,8 @@
dev.ts / main.ts
-> parseRuntimeArgs(cli args) — 必须指定 config.yaml
-> bootstrap({ configPath, mode })
-> loadServerConfig(configPath)
-> createRuntimeLogger(config.logging)
-> loadServerConfig(configPath) — 加载并校验配置config.ts
-> createRuntimeLogger(config.logging) — 创建 Logger 实例
-> mkdirSync(dataDir)
-> 加载 migrations生产嵌入 bytes开发磁盘 drizzle/
-> createDatabase(dataDir)
@@ -33,18 +33,20 @@ Request -> Bun.serve routes 声明式匹配 -> routes/*.ts handler -> helpers/
## 主要模块
| 模块 | 职责 |
| ------------------------- | ------------------------------------------------ |
| `src/server/bootstrap.ts` | 统一启动引导、DB 初始化、shutdown 编排 |
| `src/server/server.ts` | Bun HTTP server + routes 注册 |
| `src/server/routes/` | API handler按资源端点拆分 |
| `src/server/db/` | SQLite 连接、schema、migration、data access |
| `src/server/ai/` | AI Provider Registry + Agent 流式调用 |
| `src/server/config/` | 配置解析types、variables、normalizer、schema |
| `src/server/helpers/` | 响应格式化、URL 工具 |
| `src/server/middleware/` | 参数校验 + 错误处理 |
| `src/shared/api.ts` | 前后端共享 API 类型 |
| `src/shared/app.ts` | 应用全局常量 |
| 模块 | 职责 |
| ------------------------- | -------------------------------------------------------- |
| `src/server/bootstrap.ts` | 统一启动引导、DB 初始化、shutdown 编排 |
| `src/server/config.ts` | 配置加载入口YAML 解析、规范化、契约校验、运行时校验 |
| `src/server/config/` | 配置子系统types、variables、issues、normalizer、schema |
| `src/server/logger.ts` | Logger 接口 + Pino 实现 + 测试替身 |
| `src/server/server.ts` | Bun HTTP server + routes 注册 |
| `src/server/routes/` | API handler按资源端点拆分 |
| `src/server/db/` | SQLite 连接、schema、migration、data access |
| `src/server/ai/` | AI Provider Registry + Agent + 工具 |
| `src/server/helpers/` | 响应格式化、URL 工具 |
| `src/server/middleware/` | 参数校验 + 错误处理 |
| `src/shared/api.ts` | 前后端共享 API 类型 |
| `src/shared/app.ts` | 应用全局常量 |
## 路由分组

View File

@@ -28,7 +28,7 @@ SQLite + bun:sqlite + Drizzle ORM。
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `projects.ts` | createProject、getProject、listProjects、updateProject、deleteProject、archiveProject、restoreProject |
| `providers.ts` | createProvider、getProvider、listProviders、listProviderOptions、updateProvider、deleteProvider |
| `models.ts` | createModel、getModel、listModels、getModelsByProviderId、updateModel、deleteModel |
| `models.ts` | createModel、getModel、listModels、getModelWithProvider、getModelsByProviderId、updateModel、deleteModel |
| `conversations.ts` | createConversation、getConversation、listConversations、updateConversation、updateConversationTimestamp、deleteConversation、createMessage、createMessages、listMessages |
输入输出类型来自 `src/shared/api.ts`
@@ -38,9 +38,8 @@ SQLite + bun:sqlite + Drizzle ORM。
- `src/server/ai/types.ts``AIProviderConfig`name、type、baseUrl、apiKey`AIModelConfig`providerId、modelId、capabilities
- `src/server/ai/registry.ts`
- `buildProviderRegistry(db)` — 从 DB 查询供应商构建 AI SDK Provider Registry每次调用重建不缓存。通过 `registry.languageModel('providerId:modelId')` 获取模型实例。
- `testProviderConnection(config)` — 测试 Base URL 可达性 + `/models` 接口
- `testModelConnection(config)` — 测试模型连通性
- `countModels(db)` — 统计已配置模型数
- `testProviderConnection(config, logger)` — 测试 Base URL 可达性 + `/models` 接口
- `testModelConnection(config, logger)` — 测试模型连通性(需传入含 modelId 的合并配置)
- `src/server/ai/agents/alfred-agent.ts``createAlfredAgent(model)` — ToolLoopAgent + `stepCountIs(20)` + `getCurrentTime` 工具。
- `src/server/ai/tools/`AI 工具定义。

View File

@@ -9,7 +9,7 @@
- **Admin**`src/web/consoles/admin/`):路由 `/`(总览)、`/projects``/models`
- **Workbench**`src/web/consoles/workbench/`):路由 `/workbench/:projectId``/workbench/:projectId/chat``WorkbenchProjectGate` 从 URL 读 projectId通过 `ProjectContext` 提供项目上下文,仅 active 项目渲染。
ConsoleShell 包含:`ConfigProvider(zhCN)` + `AntApp` + `Layout`(Header/Sider/Content) + 主题切换(明亮/黑暗/系统)+ 侧边栏折叠。Header 显示品牌名、版本号和控制台标题。
ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Sider/Content) + 主题切换(明亮/黑暗/系统)+ 侧边栏折叠。Header 显示品牌名、版本号和控制台标题。
`Sidebar``src/web/components/Sidebar/`)纯展示组件,通过 `menuItems` props 接收配置。
@@ -25,11 +25,11 @@ ConsoleShell 包含:`ConfigProvider(zhCN)` + `AntApp` + `Layout`(Header/Sider/
### 聊天页面
`ChatPage` = `ChatSidebar` + `ChatPanel`
`ChatPage` = `Conversations`@ant-design/x+ `ChatPanel`
- **ChatSidebar**TanStack Query 管理会话列表,创建/删除操作
- **ChatPanel**`useChat`@ai-sdk/react+ `DefaultChatTransport`ai 包)与后端 SSE 通信。按 `part.type` 分派渲染TextPartstreamdown Markdown、ReasoningPart、ToolPart四态。支持编辑重发、重新生成、复制。
- **ChatInputArea**`Input.TextArea` + `Button` + `Select`(模型切换)。
- **Conversations**:会话侧边栏,TanStack Query 管理会话列表,支持创建/选中/删除menu dropdown
- **ChatPanel**`useChat`@ai-sdk/react+ `DefaultChatTransport`ai 包)与后端 SSE 通信。按 `part.type` 分派渲染TextPartXMarkdown、ReasoningPart、ToolPart四态。支持编辑重发、重新生成、复制。
- **Sender**@ant-design/x输入框 + 发送/停止按钮 + 模型 Selectfooter slot)。
## Hooks
@@ -73,12 +73,12 @@ export interface Logger {
### 实现
| 实现 | 工厂函数 | 用途 |
| ----------------------- | --------------------------------------- | ------------------------------------------------------- |
| `DefaultLogger` + Sinks | `useLogger()` / `createDefaultLogger()` | 组件内使用ConsoleSink + AntdMessageSink 双流 |
| `ConsoleLogger` | `createConsoleLogger()` | 非组件纯函数ErrorBoundary、工具函数仅 ConsoleSink |
| `NoopLogger` | `createNoopLogger()` | 测试中不需要日志的场景 |
| `MemoryLogger` | `createMemoryLogger()` | 测试断言日志条目 |
| 实现 | 工厂函数 | 用途 |
| ----------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `DefaultLogger` + Sinks | `useLogger(bindings?)` / `createDefaultLogger()` | 组件内使用ConsoleSink + AntdMessageSink 双流;传入 bindings 自动创建带作用域的子 Logger |
| `ConsoleLogger` | `createConsoleLogger()` | 非组件纯函数ErrorBoundary、工具函数仅 ConsoleSink |
| `NoopLogger` | `createNoopLogger()` | 测试中不需要日志的场景 |
| `MemoryLogger` | `createMemoryLogger()` | 测试断言日志条目 |
### 使用方式
@@ -88,7 +88,7 @@ export interface Logger {
import { useLogger } from "../hooks/use-logger";
function MyComponent() {
const logger = useLogger();
const logger = useLogger({ component: "MyComponent" });
logger.info("数据加载完成", { count: 42 });
logger.warn("即将超时");
logger.error("操作失败", { error: new Error("...") });
@@ -106,6 +106,15 @@ logger.debug("调试信息");
**作用域绑定:**
组件内直接通过 `useLogger``bindings` 参数传入hook 内部保证引用稳定(值不变时多次渲染返回同一 Logger
```typescript
const logger = useLogger({ component: "ChatPanel", page: "workbench" });
logger.info("页面加载"); // [Alfred:INFO] 页面加载 [component=ChatPanel][page=workbench]
```
非组件场景仍可使用 `logger.child()`
```typescript
const pageLogger = logger.child({ page: "projects" });
pageLogger.info("页面加载"); // [Alfred:INFO] 页面加载 [page=projects]

View File

@@ -58,6 +58,8 @@
"@ai-sdk/openai-compatible": "^2.0.48",
"@ai-sdk/react": "^3.0.195",
"@ant-design/icons": "^6.2.3",
"@ant-design/x": "^2.7.0",
"@ant-design/x-markdown": "^2.7.0",
"@sinclair/typebox": "^0.34.49",
"@tanstack/react-query": "^5.100.14",
"ai": "^6.0.193",
@@ -65,6 +67,8 @@
"antd": "^6.4.3",
"drizzle-orm": "^0.45.2",
"es-toolkit": "^1.47.0",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars-react": "^0.5.6",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"pino-roll": "^4.0.0",
@@ -72,7 +76,6 @@
"react-dom": "^19.2.6",
"react-router": "^7.15.1",
"recharts": "^3.8.1",
"streamdown": "^2.5.0",
"zod": "^4.4.3"
}
}

View File

@@ -18,6 +18,20 @@
"sourceType": "github",
"skillPath": "skills/antd/SKILL.md",
"computedHash": "5e26c8042060bb811118927b5daf637af7929a00fa973dd8f5f804f3ba6e2bf2"
},
"x-components": {
"source": "ant-design/x",
"ref": "main",
"sourceType": "github",
"skillPath": "packages/x-skill/skills/x-components/SKILL.md",
"computedHash": "ebc195a3a5020b6d4f4533adf2e0af33253919f0c704947e727f877aba23a4c2"
},
"x-markdown": {
"source": "ant-design/x",
"ref": "main",
"sourceType": "github",
"skillPath": "packages/x-skill/skills/x-markdown/SKILL.md",
"computedHash": "2d26b8eda1692929e99a8b6163ef8b206f1f096a4a84507b50dbe836a7ec041e"
}
}
}

View File

@@ -10,6 +10,7 @@ export function createGetCurrentTime(logger?: Logger) {
inputSchema: z.object({
timezone: z.string().optional().describe("IANA 时区名称,如 'Asia/Shanghai'、'America/New_York'"),
}),
metadata: { displayName: "获取当前时间" },
});
}

View File

@@ -1,5 +1,7 @@
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { App as AntApp, ConfigProvider, Layout, Segmented, theme } from "antd";
import { XProvider } from "@ant-design/x";
import zhCN_X from "@ant-design/x/locale/zh_CN";
import { App as AntApp, Layout, Segmented, theme } from "antd";
import zhCN from "antd/locale/zh_CN";
import type { ConsoleShellProps } from "./types";
@@ -28,7 +30,7 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp
const themeAlgorithm = effectiveTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm;
return (
<ConfigProvider locale={zhCN} theme={{ algorithm: themeAlgorithm }}>
<XProvider locale={{ ...zhCN, ...zhCN_X }} theme={{ algorithm: themeAlgorithm }}>
<AntApp>
<Layout className="app-layout">
<Header className="app-header">
@@ -68,6 +70,6 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp
</Layout>
</Layout>
</AntApp>
</ConfigProvider>
</XProvider>
);
}

View File

@@ -1,68 +0,0 @@
import { Button, Flex, Input, Select } from "antd";
interface ChatInputAreaProps {
displayModelId: null | string;
input: string;
isLoading: boolean;
modelOptions: Array<{ label: string; value: string }>;
onInputChange: (value: string) => void;
onModelChange: (value: string) => void;
onSend: () => void;
onStop: () => void;
}
export function ChatInputArea({
displayModelId,
input,
isLoading,
modelOptions,
onInputChange,
onModelChange,
onSend,
onStop,
}: ChatInputAreaProps) {
return (
<div className="chat-input-area">
<Input.TextArea
autoSize={{ maxRows: 6, minRows: 1 }}
className="chat-textarea"
disabled={isLoading}
onChange={(e) => onInputChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void onSend();
}
}}
placeholder="输入消息..."
value={input}
/>
<Flex align="center" gap={8} justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={onModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center" gap={8}>
{isLoading ? (
<Button
danger
onClick={() => {
onStop?.();
}}
>
</Button>
) : (
<Button onClick={() => void onSend()} type="primary">
</Button>
)}
</Flex>
</Flex>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { useChat } from "@ai-sdk/react";
import { CopyOutlined, EditOutlined, RedoOutlined, RobotOutlined } from "@ant-design/icons";
import { Sender } from "@ant-design/x";
import { useQueryClient } from "@tanstack/react-query";
import { DefaultChatTransport, type UIMessage } from "ai";
import { App, Button, Card, Flex, Input, Spin, Typography } from "antd";
import { App, Button, Card, Divider, Flex, Input, Select, Spin, Typography } from "antd";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
@@ -13,7 +14,7 @@ import {
} from "../../../../hooks/use-conversations";
import { useLogger } from "../../../../hooks/use-logger";
import { useModelList } from "../../../../hooks/use-models";
import { ChatInputArea } from "./ChatInputArea";
import { ChatScrollArea } from "./ChatScrollArea";
import { ReasoningPart } from "./parts/ReasoningPart";
import { TextPart } from "./parts/TextPart";
import { ToolPart } from "./parts/ToolPart";
@@ -26,7 +27,7 @@ interface ChatPanelProps {
export function ChatPanel({ conversationId, onConversationCreated, projectId }: ChatPanelProps) {
const { message } = App.useApp();
const logger = useLogger().child({ component: "ChatPanel", page: "workbench" });
const logger = useLogger({ component: "ChatPanel", page: "workbench" });
const queryClient = useQueryClient();
const [input, setInput] = useState("");
const [editingMessageId, setEditingMessageId] = useState<null | string>(null);
@@ -34,7 +35,6 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const [loadingHistory, setLoadingHistory] = useState(false);
const [selectedModelId, setSelectedModelId] = useState<null | string>(null);
const fetchRef = useRef(fetchMessages);
const scrollRef = useRef<HTMLDivElement>(null);
const skipHistoryLoadRef = useRef<null | string>(null);
const { data: modelsData } = useModelList({ pageSize: 200 });
@@ -125,10 +125,6 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
});
}, [conversationId, textModels, projectId, logger]);
useEffect(() => {
scrollRef.current?.scrollTo({ behavior: "smooth", top: scrollRef.current.scrollHeight });
}, [messages]);
useEffect(() => {
if (status === "ready" && conversationId) {
void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] });
@@ -150,42 +146,37 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
[projectId, conversationId, logger],
);
const handleSend = useCallback(async () => {
if (!input.trim()) return;
const text = input;
setInput("");
const handleSenderSubmit = useCallback(
(msg: string) => {
if (!msg.trim()) return;
if (!conversationId) {
try {
const conv = await createConversation(projectId, displayModelId ?? undefined);
skipHistoryLoadRef.current = conv.id;
void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] });
void sendMessage({ text }, { body: { conversationId: conv.id } });
onConversationCreated(conv.id);
} catch (err: unknown) {
setInput(text);
const msg = err instanceof Error ? err.message : String(err);
logger.error("创建会话失败", { error: msg, projectId });
void message.error(`创建会话失败:${msg}`);
if (!conversationId) {
void (async () => {
try {
const conv = await createConversation(projectId, displayModelId ?? undefined);
skipHistoryLoadRef.current = conv.id;
void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] });
void sendMessage({ text: msg }, { body: { conversationId: conv.id } });
setInput("");
onConversationCreated(conv.id);
} catch (err: unknown) {
setInput(msg);
const errMsg = err instanceof Error ? err.message : String(err);
logger.error("创建会话失败", { error: errMsg, projectId });
void message.error(`创建会话失败:${errMsg}`);
}
})();
return;
}
return;
}
void sendMessage({ text }, { body: { conversationId } }).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.error("发送消息失败", { conversationId, error: msg, projectId });
});
}, [
input,
sendMessage,
conversationId,
projectId,
onConversationCreated,
message,
queryClient,
displayModelId,
logger,
]);
setInput("");
void sendMessage({ text: msg }, { body: { conversationId } }).catch((err: unknown) => {
const errMsg = err instanceof Error ? err.message : String(err);
logger.error("发送消息失败", { conversationId, error: errMsg, projectId });
});
},
[sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId, logger],
);
const extractText = useCallback((msg: UIMessage) => {
return msg.parts
@@ -309,23 +300,40 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<Typography.Text type="secondary"></Typography.Text>
</Flex>
</div>
<ChatInputArea
displayModelId={displayModelId}
input={input}
isLoading={isLoading}
modelOptions={modelOptions}
onInputChange={setInput}
onModelChange={handleModelChange}
onSend={() => {
void handleSend();
}}
onStop={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
/>
<div className="chat-sender-area">
<Sender
autoSize={{ maxRows: 3, minRows: 1 }}
classNames={{ root: "chat-sender-box" }}
footer={(actionNode) => (
<Flex align="center" justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={handleModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center">
<Divider orientation="vertical" />
{actionNode}
</Flex>
</Flex>
)}
loading={isLoading}
onCancel={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
onChange={setInput}
onSubmit={handleSenderSubmit}
placeholder="输入消息..."
suffix={false}
value={input}
/>
</div>
</div>
);
}
@@ -337,7 +345,7 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<Spin />
</div>
) : (
<div className="chat-scroll-area" ref={scrollRef}>
<ChatScrollArea messages={messages} status={status}>
<Flex gap={8} vertical>
{messages.map((msg, idx) => (
<Card
@@ -366,7 +374,12 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
</Flex>
) : (
msg.parts.map((part: Record<string, unknown>, i: number) => (
<PartRenderer key={i} part={part} role={msg.role} />
<PartRenderer
isStreaming={isLoading && idx === messages.length - 1 && msg.role === "assistant"}
key={i}
part={part}
role={msg.role}
/>
))
)}
</div>
@@ -378,34 +391,59 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
</Flex>
)}
</Flex>
</div>
</ChatScrollArea>
)}
<ChatInputArea
displayModelId={displayModelId}
input={input}
isLoading={isLoading}
modelOptions={modelOptions}
onInputChange={setInput}
onModelChange={handleModelChange}
onSend={() => {
void handleSend();
}}
onStop={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
/>
<div className="chat-sender-area">
<Sender
autoSize={{ maxRows: 3, minRows: 1 }}
classNames={{ root: "chat-sender-box" }}
footer={(actionNode) => (
<Flex align="center" justify="space-between">
<Select
className="chat-model-select"
disabled={isLoading}
onChange={handleModelChange}
options={modelOptions}
placeholder="选择模型"
value={displayModelId}
/>
<Flex align="center">
<Divider orientation="vertical" />
{actionNode}
</Flex>
</Flex>
)}
loading={isLoading}
onCancel={() => {
void stop().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
logger.warn("停止聊天失败", { error: msg });
});
}}
onChange={setInput}
onSubmit={handleSenderSubmit}
placeholder="输入消息..."
suffix={false}
value={input}
/>
</div>
</div>
);
}
function PartRenderer({ part, role }: { part: Record<string, unknown>; role: string }) {
function PartRenderer({
isStreaming,
part,
role,
}: {
isStreaming: boolean;
part: Record<string, unknown>;
role: string;
}) {
const partType = typeof part["type"] === "string" ? part["type"] : "";
if (partType === "text") {
return <TextPart part={part} role={role} />;
return <TextPart isStreaming={isStreaming} part={part} role={role} />;
}
if (partType.startsWith("tool-")) {
return <ToolPart part={part} />;

View File

@@ -0,0 +1,58 @@
import type { UIMessage } from "ai";
import { ArrowDownOutlined } from "@ant-design/icons";
import { Button } from "antd";
import "overlayscrollbars/styles/overlayscrollbars.css";
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { useCallback, useRef, useState } from "react";
import { useChatScroll } from "./use-chat-scroll";
interface ChatScrollAreaProps {
children: React.ReactNode;
messages: UIMessage[];
status: string;
}
export function ChatScrollArea({ children, messages, status }: ChatScrollAreaProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
const handleOsInitialized = useCallback(() => {
const os = osRef.current;
if (!os) return;
const instance = os.osInstance();
if (!instance) return;
const viewport = instance.elements().viewport as HTMLDivElement;
scrollRef.current = viewport;
setViewportElement(viewport);
}, []);
const { isAtBottom, scrollToBottom } = useChatScroll({ messages, scrollRef, status, viewportElement });
return (
<>
<OverlayScrollbarsComponent
className="chat-scroll-area"
events={{ initialized: handleOsInitialized }}
options={{
overflow: { x: "hidden", y: "scroll" },
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
}}
ref={osRef}
>
{children}
</OverlayScrollbarsComponent>
{!isAtBottom && (
<Button
className="chat-scroll-bottom-btn"
icon={<ArrowDownOutlined />}
onClick={scrollToBottom}
shape="circle"
size="small"
/>
)}
</>
);
}

View File

@@ -1,102 +0,0 @@
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { App, Button, Flex, Popconfirm, Spin, Typography } from "antd";
import type { Conversation } from "../../../../../shared/api";
import { createConversation, deleteConversation, fetchConversations } from "../../../../hooks/use-conversations";
import { useModelList } from "../../../../hooks/use-models";
interface ChatSidebarProps {
activeId: null | string;
onSelect: (id: null | string) => void;
projectId: string;
}
export function ChatSidebar({ activeId, onSelect, projectId }: ChatSidebarProps) {
const queryClient = useQueryClient();
const { message } = App.useApp();
const CONVERSATIONS_KEY = ["conversations", projectId] as const;
const { data, isLoading } = useQuery({
queryFn: () => fetchConversations(projectId),
queryKey: CONVERSATIONS_KEY,
});
const { data: modelsData } = useModelList({ pageSize: 200 });
const textModels = (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text"));
const defaultModelId = textModels[0]?.id;
const createMutation = useMutation({
mutationFn: () => createConversation(projectId, defaultModelId),
onError: (err: Error) => {
void message.error(`创建会话失败:${err.message}`);
},
onSuccess: (conversation: Conversation) => {
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
onSelect(conversation.id);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteConversation(projectId, id),
onError: (err: Error) => {
void message.error(`删除会话失败:${err.message}`);
},
onSuccess: (_data: void, id: string) => {
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
if (activeId === id) onSelect(null);
},
});
const conversations = data?.items ?? [];
return (
<div className="app-chat-sidebar">
<div className="app-chat-sidebar-header">
<Button
block
icon={<PlusOutlined />}
loading={createMutation.isPending}
onClick={() => createMutation.mutate()}
type="primary"
>
</Button>
</div>
<div className="app-chat-sidebar-list">
{isLoading ? (
<div className="app-chat-sidebar-loading">
<Spin />
</div>
) : (
conversations.map((item: Conversation) => (
<Flex
align="center"
className={`app-chat-sidebar-item ${activeId === item.id ? "app-chat-sidebar-item-active" : ""}`}
gap="small"
justify="space-between"
key={item.id}
onClick={() => onSelect(item.id)}
>
<Typography.Text className="app-chat-sidebar-item-title" ellipsis>
{item.title}
</Typography.Text>
<Popconfirm onConfirm={() => deleteMutation.mutate(item.id)} title="确定删除此会话?">
<Button
className="app-chat-sidebar-item-action"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
size="small"
type="text"
/>
</Popconfirm>
</Flex>
))
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import { CopyOutlined } from "@ant-design/icons";
import { CodeHighlighter } from "@ant-design/x";
import { App, Button, Flex, Typography } from "antd";
import React from "react";
interface CodeBlockWithCopyProps {
block?: boolean;
children?: React.ReactNode;
className?: string;
lang?: string;
streamStatus?: "done" | "loading";
}
export function CodeBlockWithCopy({ block, children, className, lang }: CodeBlockWithCopyProps) {
const { message } = App.useApp();
if (!block) {
return <code className={className}>{children}</code>;
}
const codeText = extractText(children);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const displayLang = lang || "plaintext";
const handleCopy = () => {
void navigator.clipboard.writeText(codeText).then(() => {
void message.success("已复制");
});
};
const header = (
<Flex align="center" justify="space-between" style={{ padding: "0 4px" }}>
<Typography.Text style={{ color: "var(--ant-color-text-quaternary)", fontSize: 12 }}>
{displayLang}
</Typography.Text>
<Button
icon={<CopyOutlined />}
onClick={handleCopy}
size="small"
style={{ color: "var(--ant-color-text-quaternary)" }}
type="text"
/>
</Flex>
);
return (
<CodeHighlighter header={header} lang={displayLang}>
{codeText}
</CodeHighlighter>
);
}
function extractText(children: React.ReactNode): string {
return React.Children.toArray(children)
.map((child) => (typeof child === "string" ? child : ""))
.join("");
}

View File

@@ -1,20 +1,40 @@
import { CheckCircleFilled, LoadingOutlined } from "@ant-design/icons";
import { Collapse, Flex, Typography } from "antd";
import { useCallback, useMemo, useState } from "react";
import type { PartProps } from "./types";
const REASONING_KEY = "reasoning";
export function ReasoningPart({ part }: PartProps) {
const text = typeof part["text"] === "string" ? part["text"] : "";
const state = typeof part["state"] === "string" ? part["state"] : "";
const isStreaming = state === "streaming";
const [userToggled, setUserToggled] = useState(false);
const [userActiveKey, setUserActiveKey] = useState<string[]>([]);
const autoActiveKey = useMemo(() => {
if (isStreaming) return [REASONING_KEY];
if (state === "complete" || text.length > 0) return [];
return [];
}, [isStreaming, state, text]);
const activeKey = userToggled ? userActiveKey : autoActiveKey;
const handleChange = useCallback((keys: string | string[]) => {
setUserToggled(true);
setUserActiveKey(keys as string[]);
}, []);
return (
<Collapse
activeKey={activeKey}
ghost
items={[
{
children: <Typography.Text type="secondary">{text}</Typography.Text>,
key: "reasoning",
key: REASONING_KEY,
label: isStreaming ? (
<Flex align="center" component="span" gap={4}>
<LoadingOutlined className="icon-primary" />
@@ -28,6 +48,7 @@ export function ReasoningPart({ part }: PartProps) {
),
},
]}
onChange={handleChange}
size="small"
/>
);

View File

@@ -1,13 +1,22 @@
import { XMarkdown } from "@ant-design/x-markdown";
import "@ant-design/x-markdown/themes/light.css";
import { Typography } from "antd";
import { Streamdown } from "streamdown";
import type { PartProps } from "./types";
import { CodeBlockWithCopy } from "./CodeBlockWithCopy";
interface TextPartProps extends PartProps {
isStreaming: boolean;
role: string;
}
export function TextPart({ part, role }: TextPartProps) {
const xmarkdownComponents = {
code: CodeBlockWithCopy,
pre: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
};
export function TextPart({ isStreaming, part, role }: TextPartProps) {
const text = typeof part["text"] === "string" ? part["text"] : "";
return (
@@ -15,7 +24,12 @@ export function TextPart({ part, role }: TextPartProps) {
{role === "user" ? (
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
) : (
<Streamdown parseIncompleteMarkdown>{text}</Streamdown>
<XMarkdown
className="x-markdown-light"
components={xmarkdownComponents}
content={text}
streaming={{ hasNextChunk: isStreaming }}
/>
)}
</div>
);

View File

@@ -8,6 +8,7 @@ interface ToolPartData {
input?: unknown;
output?: unknown;
toolCallId?: string;
toolMetadata?: Record<string, unknown>;
toolName?: string;
type?: string;
}
@@ -24,7 +25,9 @@ const FORMAT_JSON = (v: unknown) => JSON.stringify(v, null, 2);
export function ToolPart({ part }: PartProps) {
const toolPart = part as unknown as ToolPartData;
const state = getToolState(toolPart);
const toolName = toolPart.toolName ?? (toolPart.type ?? "unknown").replace(/^tool-/, "");
const rawToolName = toolPart.toolName ?? (toolPart.type ?? "unknown").replace(/^tool-/, "");
const toolName =
typeof toolPart.toolMetadata?.["displayName"] === "string" ? toolPart.toolMetadata["displayName"] : rawToolName;
const isStreaming = state === "input-streaming" || state === "input-available";

View File

@@ -0,0 +1,70 @@
import type { UIMessage } from "ai";
import { useCallback, useEffect, useRef, useState } from "react";
const NEAR_BOTTOM_THRESHOLD = 80;
interface UseChatScrollOptions {
loadingHistory?: boolean;
messages: UIMessage[];
scrollRef: React.RefObject<HTMLDivElement | null>;
status: string;
viewportElement: HTMLDivElement | null;
}
export function useChatScroll({ loadingHistory, messages, scrollRef, status, viewportElement }: UseChatScrollOptions) {
const [isAtBottom, setIsAtBottom] = useState(true);
const isStreaming = status === "streaming";
const prevLoadingRef = useRef(loadingHistory ?? false);
const checkNearBottom = useCallback(() => {
const el = scrollRef.current;
if (!el) return true;
return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
}, [scrollRef]);
useEffect(() => {
const el = viewportElement;
if (!el) return;
const handleScroll = () => {
setIsAtBottom(checkNearBottom());
};
el.addEventListener("scroll", handleScroll, { passive: true });
return () => el.removeEventListener("scroll", handleScroll);
}, [viewportElement, checkNearBottom]);
useEffect(() => {
const wasLoading = prevLoadingRef.current;
prevLoadingRef.current = loadingHistory ?? false;
if (wasLoading && !loadingHistory) {
const el = scrollRef.current;
if (!el) return;
requestAnimationFrame(() => {
const target = scrollRef.current;
if (!target) return;
target.scrollTo({ behavior: "instant", top: target.scrollHeight });
setIsAtBottom(true);
});
return;
}
}, [loadingHistory, scrollRef]);
useEffect(() => {
const el = scrollRef.current;
if (!el || !isAtBottom) return;
el.scrollTo({ behavior: "instant", top: el.scrollHeight });
}, [messages, isStreaming, isAtBottom, scrollRef]);
const scrollToBottom = useCallback(() => {
const el = scrollRef.current;
if (!el) return;
el.scrollTo({ behavior: "instant", top: el.scrollHeight });
setIsAtBottom(true);
}, [scrollRef]);
return { isAtBottom, scrollToBottom };
}

View File

@@ -1,22 +1,93 @@
import { Flex } from "antd";
import { DeleteOutlined, MoreOutlined } from "@ant-design/icons";
import { Conversations } from "@ant-design/x";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { App, Spin } from "antd";
import { useState } from "react";
import type { Conversation } from "../../../../shared/api";
import { createConversation, deleteConversation, fetchConversations } from "../../../hooks/use-conversations";
import { useModelList } from "../../../hooks/use-models";
import { ChatPanel } from "../components/chat/ChatPanel";
import { ChatSidebar } from "../components/chat/ChatSidebar";
import { useCurrentProject } from "../useCurrentProject";
export function ChatPage() {
const project = useCurrentProject();
const [activeConversationId, setActiveConversationId] = useState<null | string>(null);
const queryClient = useQueryClient();
const { message } = App.useApp();
const CONVERSATIONS_KEY = ["conversations", project.id] as const;
const { data, isLoading } = useQuery({
queryFn: () => fetchConversations(project.id),
queryKey: CONVERSATIONS_KEY,
});
const { data: modelsData } = useModelList({ pageSize: 200 });
const textModels = (modelsData?.items ?? []).filter((m) => m.capabilities.includes("text"));
const defaultModelId = textModels[0]?.id;
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteConversation(project.id, id),
onError: (err: Error) => {
void message.error(`删除会话失败:${err.message}`);
},
onSuccess: (_data: void, id: string) => {
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
if (activeConversationId === id) setActiveConversationId(null);
},
});
const conversations = (data?.items ?? []).map((c: Conversation) => ({
key: c.id,
label: c.title,
}));
return (
<Flex className="app-chat-page" gap={0} vertical={false}>
<ChatSidebar activeId={activeConversationId} onSelect={setActiveConversationId} projectId={project.id} />
<div className="app-chat-page">
<div className="app-chat-conversations">
{isLoading ? (
<Spin />
) : (
<Conversations
activeKey={activeConversationId ?? ""}
creation={{
onClick: () => {
void createConversation(project.id, defaultModelId)
.then((conv) => {
void queryClient.invalidateQueries({ queryKey: CONVERSATIONS_KEY });
setActiveConversationId(conv.id);
})
.catch((err: Error) => {
void message.error(`创建会话失败:${err.message}`);
});
},
}}
items={conversations}
menu={(conv) => ({
items: [
{
danger: true,
icon: <DeleteOutlined />,
key: "delete",
label: "删除",
onClick: () => {
deleteMutation.mutate(conv.key);
},
},
],
trigger: <MoreOutlined />,
})}
onActiveChange={(key) => setActiveConversationId(key)}
/>
)}
</div>
<ChatPanel
conversationId={activeConversationId}
onConversationCreated={setActiveConversationId}
projectId={project.id}
/>
</Flex>
</div>
);
}

View File

@@ -1,14 +1,25 @@
import { App } from "antd";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import type { Logger } from "../utils/logger";
import { AntdMessageSink, ConsoleSink, createDefaultLogger } from "../utils/logger";
export function useLogger(): Logger {
export function useLogger(bindings?: Record<string, unknown>): Logger {
const { message } = App.useApp();
const [stableJson, setStableJson] = useState(() => JSON.stringify(bindings ?? {}));
const [stableBindings, setStableBindings] = useState(() => bindings);
const currentJson = JSON.stringify(bindings ?? {});
if (currentJson !== stableJson) {
setStableJson(currentJson);
setStableBindings(bindings);
}
return useMemo(() => {
const isProduction = !!import.meta.env["PROD"];
return createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
}, [message]);
const base = createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
if (!stableBindings || Object.keys(stableBindings).length === 0) return base;
return base.child(stableBindings);
}, [message, stableBindings]);
}

View File

@@ -56,6 +56,7 @@ body {
}
.app-chat-page {
display: flex;
height: 100%;
overflow: hidden;
}
@@ -80,67 +81,22 @@ body {
min-height: 60vh;
}
.app-chat-sidebar {
.app-chat-conversations {
display: flex;
width: 260px;
flex-direction: column;
border-right: 1px solid var(--ant-color-border-secondary);
border-radius: var(--ant-border-radius-lg);
background: var(--ant-color-bg-container);
}
.app-chat-sidebar-header {
padding: var(--ant-padding-sm);
border-bottom: 1px solid var(--ant-color-border-secondary);
}
.app-chat-sidebar-list {
flex: 1;
min-height: 0;
overflow: auto;
}
.app-chat-sidebar-loading {
display: flex;
align-items: center;
justify-content: center;
padding: var(--ant-padding-xl);
}
.app-chat-sidebar-item {
cursor: pointer;
padding: var(--ant-padding-xs) var(--ant-padding-sm);
border-bottom: 1px solid var(--ant-color-border-secondary);
}
.app-chat-sidebar-item:hover {
background: var(--ant-color-bg-text-hover);
}
.app-chat-sidebar-item-active {
background: var(--ant-color-bg-text-hover);
}
.app-chat-sidebar-item-title {
flex: 1;
min-width: 0;
}
.app-chat-sidebar-item-action {
opacity: 0;
transition: opacity 0.2s;
}
.app-chat-sidebar-item:hover .app-chat-sidebar-item-action,
.app-chat-sidebar-item-active .app-chat-sidebar-item-action {
opacity: 1;
}
.app-chat-panel {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
min-width: 0;
position: relative;
}
.chat-welcome-area {
@@ -158,45 +114,29 @@ body {
flex: 1;
}
.chat-input-area {
.chat-sender-area {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
padding-left: var(--ant-padding-sm);
padding-top: var(--ant-padding-sm);
border-top: 1px solid var(--ant-color-border-secondary);
margin-left: var(--ant-padding-sm);
margin-top: var(--ant-padding-sm);
}
.message-body {
margin-top: 8px;
}
.message-body pre {
background: var(--ant-color-bg-layout);
padding: 8px 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 13px;
}
.chat-scroll-area {
flex: 1;
min-height: 0;
overflow: auto;
overflow-anchor: auto;
padding-left: var(--ant-padding-sm);
scroll-behavior: smooth;
margin-left: var(--ant-padding-sm);
border-radius: var(--ant-border-radius-lg);
height: 100%;
}
.chat-loading-indicator {
padding: 8px;
}
.chat-textarea {
flex: 1;
}
.message-body-text {
margin-bottom: 0;
}
@@ -243,6 +183,10 @@ body {
width: 180px;
}
.chat-sender-box {
background: var(--ant-color-bg-container);
}
.card-extra-actions .btn-dimmed {
color: var(--ant-color-text-quaternary);
}
@@ -251,6 +195,53 @@ body {
color: var(--ant-color-text-secondary);
}
.chat-scroll-bottom-btn {
position: absolute;
bottom: 115px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.app-page-flex {
flex: 1;
}
.os-theme-custom {
--os-size: 8px;
--os-padding-perpendicular: 2px;
--os-padding-axis: 2px;
--os-track-border-radius: 10px;
--os-handle-border-radius: 10px;
--os-handle-bg: rgba(0, 0, 0, 0.15);
--os-handle-bg-hover: rgba(0, 0, 0, 0.25);
--os-handle-bg-active: rgba(0, 0, 0, 0.35);
--os-handle-min-size: 33px;
--os-handle-max-size: none;
--os-handle-interactive-area-offset: 4px;
}
.x-markdown-light table {
border-collapse: collapse;
width: 100%;
}
.x-markdown-light th,
.x-markdown-light td {
border: 1px solid var(--ant-color-border);
padding: 6px 12px;
text-align: left;
}
.x-markdown-light th {
background: var(--ant-color-fill-quaternary);
font-weight: 600;
}
.x-markdown-light .x-md-table-wrap {
overflow-x: auto;
}

View File

@@ -51,8 +51,8 @@ void mock.module("ai", () => ({
},
}));
function getSendButton() {
return screen.getByRole("button", { name: /发.*送/ });
function getSenderTextarea() {
return screen.getByPlaceholderText("输入消息...");
}
function setupFetchMock() {
@@ -90,8 +90,7 @@ describe("ChatPanel", () => {
expect(screen.getByText("你好,我是阿福")).toBeTruthy();
expect(screen.getByText("有什么我可以帮助你的吗?")).toBeTruthy();
expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy();
expect(getSendButton()).toBeTruthy();
expect(getSenderTextarea()).toBeTruthy();
});
});
@@ -108,9 +107,9 @@ describe("ChatPanel", () => {
}),
);
const input = screen.getByPlaceholderText("输入消息...");
fireEvent.change(input, { target: { value: "你好" } });
fireEvent.click(getSendButton());
const textarea = getSenderTextarea();
fireEvent.change(textarea, { target: { value: "你好" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(onCreated).toHaveBeenCalledWith("conv-new");
@@ -139,12 +138,12 @@ describe("ChatPanel", () => {
}),
);
const input = screen.getByPlaceholderText("输入消息...");
fireEvent.change(input, { target: { value: "测试输入" } });
fireEvent.click(getSendButton());
const textarea = getSenderTextarea();
fireEvent.change(textarea, { target: { value: "测试输入" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect((input as HTMLTextAreaElement).value).toBe("测试输入");
expect((textarea as HTMLTextAreaElement).value).toBe("测试输入");
});
});
});
@@ -161,7 +160,7 @@ describe("ChatPanel", () => {
);
expect(screen.queryByText("你好,我是阿福")).toBeNull();
expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy();
expect(getSenderTextarea()).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,99 @@
import { screen } from "@testing-library/react";
import { describe, expect, mock, test } from "bun:test";
import { createElement } from "react";
import { CodeBlockWithCopy } from "../../../../src/web/consoles/workbench/components/chat/parts/CodeBlockWithCopy";
import { renderWithProviders } from "../../test-utils";
const mockWriteText = mock(() => Promise.resolve());
Object.defineProperty(navigator, "clipboard", {
configurable: true,
get: () => ({ writeText: mockWriteText }),
});
describe("CodeBlockWithCopy", () => {
test("block 模式渲染 CodeHighlighter 和语言标签", () => {
renderWithProviders(
createElement(CodeBlockWithCopy, {
block: true,
children: "const x = 1;",
lang: "typescript",
streamStatus: "done",
}),
);
expect(screen.getByText("typescript")).toBeTruthy();
});
test("block 模式渲染复制按钮", () => {
renderWithProviders(
createElement(CodeBlockWithCopy, {
block: true,
children: "hello world",
lang: "python",
streamStatus: "done",
}),
);
const copyBtn = screen.getByRole("button");
expect(copyBtn).toBeTruthy();
});
test("block 模式语言为空时显示 plaintext", () => {
renderWithProviders(
createElement(CodeBlockWithCopy, {
block: true,
children: "some code",
lang: "",
streamStatus: "done",
}),
);
expect(screen.getByText("plaintext")).toBeTruthy();
});
test("block 模式语言为 undefined 时显示 plaintext", () => {
renderWithProviders(
createElement(CodeBlockWithCopy, {
block: true,
children: "some code",
streamStatus: "done",
}),
);
expect(screen.getByText("plaintext")).toBeTruthy();
});
test("inline 模式返回 code 元素", () => {
const { container } = renderWithProviders(
createElement(CodeBlockWithCopy, {
block: false,
children: "inline code",
className: "language-ts",
}),
);
const code = container.querySelector("code.language-ts");
expect(code).toBeTruthy();
expect(code?.textContent).toBe("inline code");
});
test("点击复制按钮调用 clipboard.writeText", () => {
mockWriteText.mockClear();
renderWithProviders(
createElement(CodeBlockWithCopy, {
block: true,
children: "copy me",
lang: "javascript",
streamStatus: "done",
}),
);
const copyBtn = screen.getByRole("button");
copyBtn.click();
expect(mockWriteText).toHaveBeenCalledWith("copy me");
});
});

View File

@@ -0,0 +1,49 @@
import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import { createElement } from "react";
import { ReasoningPart } from "../../../../src/web/consoles/workbench/components/chat/parts/ReasoningPart";
import { renderWithProviders } from "../../test-utils";
describe("ReasoningPart", () => {
test("流式状态时自动展开显示文本", () => {
const part = { state: "streaming", text: "正在思考...", type: "reasoning" };
renderWithProviders(createElement(ReasoningPart, { part }));
expect(screen.getByText("正在思考...")).toBeTruthy();
expect(screen.getByText("思考中")).toBeTruthy();
});
test("完成状态时显示思考完成标签", () => {
const part = { state: "complete", text: "思考内容", type: "reasoning" };
renderWithProviders(createElement(ReasoningPart, { part }));
expect(screen.getByText("思考完成")).toBeTruthy();
});
test("无状态时默认收起", () => {
const part = { text: "默认内容", type: "reasoning" };
const { container } = renderWithProviders(createElement(ReasoningPart, { part }));
const expanded = container.querySelector('[aria-expanded="true"]');
expect(expanded).toBeNull();
});
test("用户点击折叠后尊重用户意图", () => {
const part = { state: "streaming", text: "思考中内容", type: "reasoning" };
const { container } = renderWithProviders(createElement(ReasoningPart, { part }));
const collapseHeader = container.querySelector(".ant-collapse-header");
expect(collapseHeader).toBeTruthy();
if (collapseHeader) {
fireEvent.click(collapseHeader);
}
expect(screen.getByText("思考中内容")).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import { screen } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import { createElement } from "react";
import { ToolPart } from "../../../../src/web/consoles/workbench/components/chat/parts/ToolPart";
import { renderWithProviders } from "../../test-utils";
describe("ToolPart 工具显示名", () => {
test("无 toolMetadata 时使用 toolName", () => {
const part = {
input: { timezone: "Asia/Shanghai" },
output: { iso: "2024-01-01T00:00:00.000Z", local: "2024年1月1日", timestamp: 1704067200000 },
toolCallId: "call-1",
toolName: "getCurrentTime",
type: "tool-getCurrentTime",
};
renderWithProviders(createElement(ToolPart, { part }));
expect(screen.getByText(/getCurrentTime/)).toBeTruthy();
});
test("有 toolMetadata.displayName 时优先使用显示名", () => {
const part = {
input: { timezone: "Asia/Shanghai" },
output: { iso: "2024-01-01T00:00:00.000Z", local: "2024年1月1日", timestamp: 1704067200000 },
toolCallId: "call-1",
toolMetadata: { displayName: "获取当前时间" },
toolName: "getCurrentTime",
type: "tool-getCurrentTime",
};
renderWithProviders(createElement(ToolPart, { part }));
expect(screen.getByText("获取当前时间")).toBeTruthy();
expect(screen.queryByText(/getCurrentTime/)).toBeNull();
});
test("toolMetadata.displayName 非字符串时回退到 toolName", () => {
const part = {
input: {},
output: {},
toolCallId: "call-2",
toolMetadata: { displayName: 123 },
type: "tool-someTool",
};
renderWithProviders(createElement(ToolPart, { part }));
expect(screen.getByText("someTool")).toBeTruthy();
});
test("错误状态时使用显示名", () => {
const part = {
errorText: "超时",
toolCallId: "call-3",
toolMetadata: { displayName: "获取当前时间" },
type: "tool-getCurrentTime",
};
renderWithProviders(createElement(ToolPart, { part }));
expect(screen.getByText(/获取当前时间.*失败/)).toBeTruthy();
});
});

View File

@@ -0,0 +1,190 @@
import type { UIMessage } from "ai";
import { act, renderHook } from "@testing-library/react";
import { describe, expect, mock, test } from "bun:test";
import { useChatScroll } from "../../../src/web/consoles/workbench/components/chat/use-chat-scroll";
interface HookProps {
loadingHistory: boolean;
messages: UIMessage[];
scrollRef: React.RefObject<HTMLDivElement | null>;
status: string;
viewportElement: HTMLDivElement | null;
}
interface MockedElement {
addEventListener: ReturnType<typeof mock>;
clientHeight: number;
removeEventListener: ReturnType<typeof mock>;
scrollHeight: number;
scrollTo: ReturnType<typeof mock>;
scrollTop: number;
}
function asRef(el: MockedElement): React.RefObject<HTMLDivElement | null> {
return { current: el as unknown as HTMLDivElement };
}
function asViewport(el: MockedElement): HTMLDivElement {
return el as unknown as HTMLDivElement;
}
function createScrollElement(overrides: Partial<MockedElement> = {}): MockedElement {
return {
addEventListener: mock(() => {}),
clientHeight: 400,
removeEventListener: mock(() => {}),
scrollHeight: 1000,
scrollTo: mock(() => {}),
scrollTop: 600,
...overrides,
};
}
describe("useChatScroll", () => {
test("在底部附近时自动滚动", () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 920 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [{ id: "1", parts: [], role: "user" }] as UIMessage[];
const { rerender } = renderHook(
({ loadingHistory, messages, scrollRef, status, viewportElement }: HookProps) =>
useChatScroll({ loadingHistory, messages, scrollRef, status, viewportElement }),
{
initialProps: { loadingHistory: false, messages, scrollRef, status: "streaming", viewportElement },
},
);
const newMessages: UIMessage[] = [
{ id: "1", parts: [], role: "user" },
{ id: "2", parts: [], role: "assistant" },
] as UIMessage[];
rerender({ loadingHistory: false, messages: newMessages, scrollRef, status: "streaming", viewportElement });
expect(el.scrollTo).toHaveBeenCalled();
});
test("不在底部时不自动滚动", () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 200 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [{ id: "1", parts: [], role: "user" }] as UIMessage[];
const { rerender } = renderHook(
({ loadingHistory, messages, scrollRef, status, viewportElement }: HookProps) =>
useChatScroll({ loadingHistory, messages, scrollRef, status, viewportElement }),
{
initialProps: { loadingHistory: false, messages, scrollRef, status: "streaming", viewportElement },
},
);
const handlerCalls = el.addEventListener.mock.calls as unknown[][];
const scrollHandler = handlerCalls.find((c) => c[0] === "scroll")?.[1];
if (scrollHandler) {
el.scrollTop = 200;
(scrollHandler as () => void)();
}
el.scrollTo.mockClear();
const newMessages: UIMessage[] = [
{ id: "1", parts: [], role: "user" },
{ id: "2", parts: [], role: "assistant" },
] as UIMessage[];
rerender({ loadingHistory: false, messages: newMessages, scrollRef, status: "streaming", viewportElement });
const scrollToCalls = el.scrollTo.mock.calls as unknown[][];
expect(scrollToCalls.length).toBe(0);
});
test("scrollToBottom 使用 instant 滚动", () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 200 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [];
const { result } = renderHook(() => useChatScroll({ messages, scrollRef, status: "ready", viewportElement }));
act(() => {
result.current.scrollToBottom();
});
expect(el.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 1000 });
});
test("流式时使用 instant 滚动", () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 920 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [{ id: "1", parts: [], role: "user" }] as UIMessage[];
renderHook(() =>
useChatScroll({ loadingHistory: false, messages, scrollRef, status: "streaming", viewportElement }),
);
const calls = el.scrollTo.mock.calls as unknown[][];
const instantCall = calls.find((call) => {
const opts = call[0] as Record<string, unknown> | undefined;
return opts?.["behavior"] === "instant";
});
expect(instantCall).toBeTruthy();
});
test("loadingHistory 从 true 变为 false 时强制 scrollToBottom", async () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 0 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [{ id: "1", parts: [], role: "user" }] as UIMessage[];
const { rerender } = renderHook(
({ loadingHistory, messages, scrollRef, status, viewportElement }: HookProps) =>
useChatScroll({ loadingHistory, messages, scrollRef, status, viewportElement }),
{
initialProps: { loadingHistory: true, messages, scrollRef, status: "ready", viewportElement },
},
);
el.scrollTo.mockClear();
act(() => {
rerender({ loadingHistory: false, messages, scrollRef, status: "ready", viewportElement });
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
expect(el.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: el.scrollHeight });
});
test("loadingHistory 不变时不触发强制滚动", () => {
const el = createScrollElement({ scrollHeight: 1000, scrollTop: 920 });
const scrollRef = asRef(el);
const viewportElement = asViewport(el);
const messages: UIMessage[] = [];
const { rerender } = renderHook(
({ loadingHistory, messages, scrollRef, status, viewportElement }: HookProps) =>
useChatScroll({ loadingHistory, messages, scrollRef, status, viewportElement }),
{
initialProps: { loadingHistory: false, messages, scrollRef, status: "ready", viewportElement },
},
);
el.scrollTo.mockClear();
const newMessages: UIMessage[] = [{ id: "1", parts: [], role: "user" }] as UIMessage[];
rerender({ loadingHistory: false, messages: newMessages, scrollRef, status: "ready", viewportElement });
const instantCalls = el.scrollTo.mock.calls.filter((call: unknown[]) => {
const opts = call[0] as Record<string, unknown> | undefined;
return opts?.["behavior"] === "instant" && opts?.["top"] === el.scrollHeight;
});
expect(instantCalls.length).toBe(1);
});
});

View File

@@ -1,11 +1,23 @@
import { describe, expect, mock, test } from "bun:test";
import { createElement } from "react";
import { act, createElement, useState } from "react";
import type { Logger } from "../../../src/web/utils/logger";
import { useLogger } from "../../../src/web/hooks/use-logger";
import { renderWithProviders } from "../test-utils";
function BindingsHookTester({
bindings,
onMount,
}: {
bindings?: Record<string, unknown>;
onMount: (logger: Logger) => void;
}) {
const logger = useLogger(bindings);
onMount(logger);
return null;
}
function HookTester({ onMount }: { onMount: (logger: Logger) => void }) {
const logger = useLogger();
onMount(logger);
@@ -70,3 +82,109 @@ describe("useLogger", () => {
expect(errorSpy).toHaveBeenCalled();
});
});
describe("useLogger bindings 参数", () => {
test("传入 bindings 返回带上下文前缀的 Logger", () => {
const spy = mock((..._args: unknown[]) => {});
const origWarn = console.warn;
console.warn = spy;
let logger: Logger | undefined;
renderWithProviders(
createElement(BindingsHookTester, {
bindings: { component: "TestComp" },
onMount: (l: Logger) => {
logger = l;
},
}),
);
logger!.warn("绑定测试");
console.warn = origWarn;
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0]![0]).toMatch(/\[component=TestComp\]/);
});
test("无参调用与空 bindings 行为一致", () => {
let noBindingsLogger: Logger | undefined;
let emptyBindingsLogger: Logger | undefined;
const spy = mock((..._args: unknown[]) => {});
const origWarn = console.warn;
console.warn = spy;
renderWithProviders(
createElement(HookTester, {
onMount: (l: Logger) => {
noBindingsLogger = l;
},
}),
);
renderWithProviders(
createElement(BindingsHookTester, {
bindings: {},
onMount: (l: Logger) => {
emptyBindingsLogger = l;
},
}),
);
noBindingsLogger!.warn("a");
emptyBindingsLogger!.warn("b");
console.warn = origWarn;
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0]![0]).not.toMatch(/\[component=/);
expect(spy.mock.calls[1]![0]).not.toMatch(/\[component=/);
});
test("相同 bindings 值多次渲染返回同一 Logger 引用", () => {
const refs: Logger[] = [];
function StableTester() {
const logger = useLogger({ component: "Stable" });
const [, setTick] = useState(0);
refs.push(logger);
return createElement("button", {
onClick: () => setTick((t) => t + 1),
});
}
const result = renderWithProviders(createElement(StableTester));
const initialCount = refs.length;
act(() => {
result.getByRole("button").click();
});
act(() => {
result.getByRole("button").click();
});
expect(refs.length).toBeGreaterThan(initialCount);
for (let i = 1; i < refs.length; i++) {
expect(refs[i]).toBe(refs[0]);
}
});
test("bindings 值变化时返回新 Logger 引用", () => {
const refs: Logger[] = [];
let currentBindings: Record<string, unknown> = { component: "A" };
function DynamicTester() {
const logger = useLogger(currentBindings);
refs.push(logger);
return null;
}
const { rerender } = renderWithProviders(createElement(DynamicTester));
const countBefore = refs.length;
currentBindings = { component: "B" };
rerender(createElement(DynamicTester));
expect(refs.length).toBe(countBefore + 1);
expect(refs[refs.length - 1]).not.toBe(refs[0]);
});
});

View File

@@ -1,6 +1,7 @@
import { XProvider } from "@ant-design/x";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "@testing-library/react";
import { App, ConfigProvider } from "antd";
import { App } from "antd";
import { mock } from "bun:test";
import { createElement, StrictMode } from "react";
import { MemoryRouter } from "react-router";
@@ -81,7 +82,7 @@ export function renderWithProviders(ui: React.ReactElement, options?: RenderWith
createElement(
MemoryRouter,
{ initialEntries: [initialRoute] },
createElement(ConfigProvider, null, createElement(App, null, ui)),
createElement(XProvider, null, createElement(App, null, ui)),
),
),
),