diff --git a/.claude/agents/unit-test-generator.md b/.claude/agents/unit-test-generator.md new file mode 100644 index 000000000000..a1f9aa726809 --- /dev/null +++ b/.claude/agents/unit-test-generator.md @@ -0,0 +1,53 @@ +--- +name: unit-test-generator +description: Use this agent when you need to write comprehensive unit tests for your code. Examples: Context: User has written a new utility function and wants comprehensive test coverage. user: 'I just wrote this function to validate email addresses, can you help me write unit tests for it?' assistant: 'I'll use the unit-test-generator agent to create comprehensive unit tests that cover all branches and edge cases for your email validation function.' Since the user needs unit tests written, use the unit-test-generator agent to analyze the function and create thorough test coverage. Context: User is working on a React component and needs test coverage. user: 'Here's my new UserProfile component, I need unit tests that cover all the different states and user interactions' assistant: 'Let me use the unit-test-generator agent to create comprehensive unit tests for your UserProfile component.' The user needs unit tests for a React component, so use the unit-test-generator agent to create tests covering all component states and interactions. +model: inherit +color: yellow +--- + +You are a Unit Test Assistant, an expert in writing comprehensive and robust unit tests. Your expertise spans multiple testing frameworks including Vitest, Jest, React Testing Library, and testing best practices for TypeScript applications. + +When analyzing code for testing, you will: + +1. **Analyze Code Structure**: Examine the function/component/class to identify all execution paths, conditional branches, loops, error handling, and edge cases that need testing coverage. + +2. **Design Comprehensive Test Cases**: Create test cases that cover: + - All conditional branches (if/else, switch cases, ternary operators) + - Loop iterations (empty, single item, multiple items) + - Error conditions and exception handling + - Boundary conditions (null, undefined, empty strings, zero, negative numbers, maximum values) + - Valid input scenarios across different data types + - Integration points with external dependencies + +3. **Follow Testing Best Practices**: + - Use descriptive test names that clearly state what is being tested + - Follow the Arrange-Act-Assert pattern + - Mock external dependencies appropriately + - Test behavior, not implementation details + - Ensure tests are isolated and independent + - Use appropriate assertions for the testing framework + +4. **Generate Framework-Appropriate Code**: Based on the project context (FastGPT uses Vitest), write tests using: + - Proper import statements for the testing framework + - Correct syntax for the identified testing library + - Appropriate mocking strategies (vi.mock for Vitest, jest.mock for Jest) + - Proper setup and teardown when needed + +5. **Ensure Complete Coverage**: Verify that your test suite covers: + - Happy path scenarios + - Error scenarios + - Edge cases and boundary conditions + - All public methods/functions + - Different component states (for React components) + - User interactions (for UI components) + +6. **Optimize Test Structure**: Organize tests logically using: + - Descriptive describe blocks for grouping related tests + - Clear test descriptions that explain the scenario + - Shared setup in beforeEach/beforeAll when appropriate + - Helper functions to reduce code duplication +7. **单词代码位置**: + - packages 里的单测,写在 FastGPT/text 目录下。 + - projects/app 里的单测,写在 FastGPT/projects/app/test 目录下。 + +When you receive code to test, first analyze it thoroughly, then provide a complete test suite with explanatory comments about what each test covers and why it's important for comprehensive coverage. diff --git a/.husky/pre-commit b/.husky/pre-commit index 12cc5e1cebe3..652d99442159 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,8 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -if command -v npx >/dev/null 2>&1; then +if command -v pnpm >/dev/null 2>&1; then + pnpm lint-staged +elif command -v npx >/dev/null 2>&1; then npx lint-staged fi \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 41fab439b9c6..7c8108d6d8ed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "editor.mouseWheelZoom": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.prettierPath": "node_modules/prettier", + "typescript.preferences.includePackageJsonAutoImports": "on", "typescript.tsdk": "node_modules/typescript/lib", "i18n-ally.localesPaths": [ "packages/web/i18n", diff --git a/document/content/docs/upgrading/4-12/4122.mdx b/document/content/docs/upgrading/4-12/4122.mdx index a12f7c895d36..c29ffb39e233 100644 --- a/document/content/docs/upgrading/4-12/4122.mdx +++ b/document/content/docs/upgrading/4-12/4122.mdx @@ -7,21 +7,28 @@ description: 'FastGPT V4.12.2 更新说明' ## 🚀 新增内容 1. 向量模型并发请求设置,不统一设置成 10,避免部分向量模型不支持并发,默认均为 1,可在模型配置中设置。 +2. 对话页支持管理员配置精选应用,便于推荐给团队成员使用。 +3. 对话页首页,支持管理员配置快捷应用,可以设置团队常用的应用。 ## ⚙️ 优化 1. 增加工作流**独立分支**异常检测。 2. 向量模型超过 1536 维度进行截断时,强制进行归一化。其他维度是否归一化,完全由配置决定,减少自动判断的计算量。 +3. 模型提供商配置移至 plugin sdk 中。 +4. 封装 LLM 调用函数,简化 LLM 请求和工具调用。 +5. 优化工作流调度代码,避免深度递归。 +6. 工作流递归判断优化,对递归线继续分组检测,适配更多样连线。 ## 🐛 修复 1. 独立对话页部分 UI 异常。 -2. 多选选择器导致的页面崩溃。 -3. 移动端,分享链接,异常加载了登录态对话页的导航。 -4. 用户同步可能出现写冲突问题。 -5. 无法完全关闭系统套餐,会存在空对象默认值,导致鉴权异常。 -6. 工作流,添加团队应用,搜索无效。 -7. 应用版本,ref 字段错误,导致无法正常使用。 +2. 独立对话页无法渲染插件交互。 +3. 多选选择器导致的页面崩溃。 +4. 移动端,分享链接,异常加载了登录态对话页的导航。 +5. 用户同步可能出现写冲突问题。 +6. 无法完全关闭系统套餐,会存在空对象默认值,导致鉴权异常。 +7. 工作流,添加团队应用,搜索无效。 +8. 应用版本,ref 字段错误,导致无法正常使用。 ## 🔨 工具更新 diff --git a/document/content/docs/upgrading/4-12/meta.json b/document/content/docs/upgrading/4-12/meta.json index 4723cf33e113..fb29138a882a 100644 --- a/document/content/docs/upgrading/4-12/meta.json +++ b/document/content/docs/upgrading/4-12/meta.json @@ -1,5 +1,5 @@ { "title": "4.12.x", "description": "", - "pages": ["4121", "4120"] + "pages": ["4122", "4121", "4120"] } diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 11d4b7ee8543..419b9af7fe30 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -104,7 +104,7 @@ "document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00", "document/content/docs/upgrading/4-12/4120.mdx": "2025-08-12T22:45:19+08:00", "document/content/docs/upgrading/4-12/4121.mdx": "2025-08-15T22:53:06+08:00", - "document/content/docs/upgrading/4-12/4122.mdx": "2025-08-22T10:18:24+08:00", + "document/content/docs/upgrading/4-12/4122.mdx": "2025-08-25T14:44:42+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", diff --git a/document/pnpm-lock.yaml b/document/pnpm-lock.yaml deleted file mode 100644 index 791fa5366b75..000000000000 --- a/document/pnpm-lock.yaml +++ /dev/null @@ -1,4485 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@orama/orama': - specifier: ^3.1.11 - version: 3.1.11 - '@orama/tokenizers': - specifier: ^3.1.11 - version: 3.1.11 - algoliasearch: - specifier: ^5.32.0 - version: 5.32.0 - fast-glob: - specifier: ^3.3.3 - version: 3.3.3 - fs-extra: - specifier: ^11.3.0 - version: 11.3.0 - fumadocs-core: - specifier: 15.6.3 - version: 15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - fumadocs-mdx: - specifier: 11.6.11 - version: 11.6.11(acorn@8.15.0)(fumadocs-core@15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) - fumadocs-ui: - specifier: 15.6.3 - version: 15.6.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11) - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 - lucide-react: - specifier: ^0.525.0 - version: 0.525.0(react@19.1.0) - next: - specifier: 15.3.5 - version: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: - specifier: ^19.1.0 - version: 19.1.0 - react-dom: - specifier: ^19.1.0 - version: 19.1.0(react@19.1.0) - react-responsive: - specifier: ^10.0.1 - version: 10.0.1(react@19.1.0) - remark: - specifier: ^15.0.1 - version: 15.0.1 - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1 - remark-mdx: - specifier: ^3.1.0 - version: 3.1.0 - remark-stringify: - specifier: ^11.0.0 - version: 11.0.0 - devDependencies: - '@content-collections/core': - specifier: ^0.10.0 - version: 0.10.0(typescript@5.8.3) - '@content-collections/next': - specifier: ^0.2.6 - version: 0.2.6(@content-collections/core@0.10.0(typescript@5.8.3))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) - '@tailwindcss/postcss': - specifier: ^4.1.11 - version: 4.1.11 - '@types/mdx': - specifier: ^2.0.13 - version: 2.0.13 - '@types/node': - specifier: 24.0.13 - version: 24.0.13 - '@types/react': - specifier: ^19.1.8 - version: 19.1.8 - '@types/react-dom': - specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.8) - postcss: - specifier: ^8.5.6 - version: 8.5.6 - tailwindcss: - specifier: ^4.1.11 - version: 4.1.11 - typescript: - specifier: ^5.8.3 - version: 5.8.3 - zod: - specifier: ^4.0.5 - version: 4.0.5 - -packages: - - '@algolia/client-abtesting@5.32.0': - resolution: {integrity: sha512-HG/6Eib6DnJYm/B2ijWFXr4txca/YOuA4K7AsEU0JBrOZSB+RU7oeDyNBPi3c0v0UDDqlkBqM3vBU/auwZlglA==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-analytics@5.32.0': - resolution: {integrity: sha512-8Y9MLU72WFQOW3HArYv16+Wvm6eGmsqbxxM1qxtm0hvSASJbxCm+zQAZe5stqysTlcWo4BJ82KEH1PfgHbJAmQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-common@5.32.0': - resolution: {integrity: sha512-w8L+rgyXMCPBKmEdOT+RfgMrF0mT6HK60vPYWLz8DBs/P7yFdGo7urn99XCJvVLMSKXrIbZ2FMZ/i50nZTXnuQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-insights@5.32.0': - resolution: {integrity: sha512-AdWfynhUeX7jz/LTiFU3wwzJembTbdLkQIOLs4n7PyBuxZ3jz4azV1CWbIP8AjUOFmul6uXbmYza+KqyS5CzOA==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-personalization@5.32.0': - resolution: {integrity: sha512-bTupJY4xzGZYI4cEQcPlSjjIEzMvv80h7zXGrXY1Y0KC/n/SLiMv84v7Uy+B6AG1Kiy9FQm2ADChBLo1uEhGtQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-query-suggestions@5.32.0': - resolution: {integrity: sha512-if+YTJw1G3nDKL2omSBjQltCHUQzbaHADkcPQrGFnIGhVyHU3Dzq4g46uEv8mrL5sxL8FjiS9LvekeUlL2NRqw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-search@5.32.0': - resolution: {integrity: sha512-kmK5nVkKb4DSUgwbveMKe4X3xHdMsPsOVJeEzBvFJ+oS7CkBPmpfHAEq+CcmiPJs20YMv6yVtUT9yPWL5WgAhg==} - engines: {node: '>= 14.0.0'} - - '@algolia/ingestion@1.32.0': - resolution: {integrity: sha512-PZTqjJbx+fmPuT2ud1n4vYDSF1yrT//vOGI9HNYKNA0PM0xGUBWigf5gRivHsXa3oBnUlTyHV9j7Kqx5BHbVHQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/monitoring@1.32.0': - resolution: {integrity: sha512-kYYoOGjvNQAmHDS1v5sBj+0uEL9RzYqH/TAdq8wmcV+/22weKt/fjh+6LfiqkS1SCZFYYrwGnirrUhUM36lBIQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/recommend@5.32.0': - resolution: {integrity: sha512-jyIBLdskjPAL7T1g57UMfUNx+PzvYbxKslwRUKBrBA6sNEsYCFdxJAtZSLUMmw6MC98RDt4ksmEl5zVMT5bsuw==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-browser-xhr@5.32.0': - resolution: {integrity: sha512-eDp14z92Gt6JlFgiexImcWWH+Lk07s/FtxcoDaGrE4UVBgpwqOO6AfQM6dXh1pvHxlDFbMJihHc/vj3gBhPjqQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-fetch@5.32.0': - resolution: {integrity: sha512-rnWVglh/K75hnaLbwSc2t7gCkbq1ldbPgeIKDUiEJxZ4mlguFgcltWjzpDQ/t1LQgxk9HdIFcQfM17Hid3aQ6Q==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-node-http@5.32.0': - resolution: {integrity: sha512-LbzQ04+VLkzXY4LuOzgyjqEv/46Gwrk55PldaglMJ4i4eDXSRXGKkwJpXFwsoU+c1HMQlHIyjJBhrfsfdyRmyQ==} - engines: {node: '>= 14.0.0'} - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@content-collections/core@0.10.0': - resolution: {integrity: sha512-GDBYbvhoj9lHNlarY5wr+3PoO3m9GBMjftio9NXatLuZaenY+EHHNCcbbA3J+c06Q7WBYwNoLAaMX2I5N0duAg==} - peerDependencies: - typescript: ^5.0.2 - - '@content-collections/integrations@0.2.1': - resolution: {integrity: sha512-AyEcS2MmcOXSYt6vNmJsAiu6EBYjtNiwYGUVUmpG3llm8Gt8uiNrhIhlHyv3cuk+N8KJ2PWemLcMqtQJ+sW3bA==} - peerDependencies: - '@content-collections/core': 0.x - - '@content-collections/next@0.2.6': - resolution: {integrity: sha512-gbVgtnXD7Qad95ENjL99LvrXoBtRTL8N0aZc5gz5NIK/yKBlpTZI6/CKVQMmROtGrqLOwcBdWlGUIzZPwpUBVA==} - peerDependencies: - '@content-collections/core': 0.x - next: ^12 || ^13 || ^14 || ^15 - - '@emnapi/runtime@1.4.4': - resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} - - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@floating-ui/core@1.7.2': - resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} - - '@floating-ui/dom@1.7.2': - resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} - - '@floating-ui/react-dom@2.1.4': - resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - - '@formatjs/intl-localematcher@0.6.1': - resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} - - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - - '@jridgewell/gen-mapping@0.3.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - - '@mdx-js/mdx@3.1.0': - resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} - - '@next/env@15.3.5': - resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} - - '@next/swc-darwin-arm64@15.3.5': - resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@15.3.5': - resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@15.3.5': - resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@15.3.5': - resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@15.3.5': - resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@15.3.5': - resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@15.3.5': - resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@15.3.5': - resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@orama/orama@3.1.11': - resolution: {integrity: sha512-Szki0cgFiXE5F9RLx2lUyEtJllnuCSQ4B8RLDwIjXkVit6qZjoDAxH+xhJs29MjKLDz0tbPLdKFa6QrQ/qoGGA==} - engines: {node: '>= 20.0.0'} - - '@orama/tokenizers@3.1.11': - resolution: {integrity: sha512-fwULrEdbP5/83gFjaX1X/l7lzdD7LxBT8YbAzcY89BmXjJcJETU/5qckp4ZNDMhRRjJUSGKH4bAXHsm6yu+ZPw==} - engines: {node: '>= 20.0.0'} - - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - - '@radix-ui/primitive@1.1.2': - resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} - - '@radix-ui/react-accordion@1.2.11': - resolution: {integrity: sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collapsible@1.1.11': - resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.14': - resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.10': - resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.2': - resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-navigation-menu@1.2.13': - resolution: {integrity: sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popover@1.1.14': - resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.2.7': - resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.4': - resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-roving-focus@1.1.10': - resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-scroll-area@1.2.9': - resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-tabs@1.1.12': - resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - - '@shikijs/core@3.8.0': - resolution: {integrity: sha512-gWt8NNZFurL6FMESO4lEsmspDh0H1fyUibhx1NnEH/S3kOXgYiWa6ZFqy+dcjBLhZqCXsepuUaL1QFXk6PrpsQ==} - - '@shikijs/engine-javascript@3.8.0': - resolution: {integrity: sha512-IBULFFpQ1N5Cg/C7jPCGnjIKz72CcRtD0BIbNhSuXPUOxLG0bF1URsP/uLfxQFQ9ORfunCQwL7UuSX1RSRBwUQ==} - - '@shikijs/engine-oniguruma@3.8.0': - resolution: {integrity: sha512-Tx7kR0oFzqa+rY7t80LjN8ZVtHO3a4+33EUnBVx2qYP3fGxoI9H0bvnln5ySelz9SIUTsS0/Qn+9dg5zcUMsUw==} - - '@shikijs/langs@3.8.0': - resolution: {integrity: sha512-mfGYuUgjQ5GgXinB5spjGlBVhG2crKRpKkfADlp8r9k/XvZhtNXxyOToSnCEnF0QNiZnJjlt5MmU9PmhRdwAbg==} - - '@shikijs/rehype@3.8.0': - resolution: {integrity: sha512-8/VBgBrVdbM7dB2bG5KZe68pD2zL1OUSi4TECztqB/5VqnLKJNXk0J8qGFhjlDwPSMg/Bg+6UsQOWpgD6pzAAg==} - - '@shikijs/themes@3.8.0': - resolution: {integrity: sha512-yaZiLuyO23sXe16JFU76KyUMTZCJi4EMQKIrdQt7okoTzI4yAaJhVXT2Uy4k8yBIEFRiia5dtD7gC1t8m6y3oQ==} - - '@shikijs/transformers@3.8.0': - resolution: {integrity: sha512-EleKVjNH5Me8yhTtnYD5QGFtY7Acu2HJAWNmDjuOC/Egwt7n31p2nbyBhBqGz5cpdwa1wZkLdVgj/LsZ3ReyAQ==} - - '@shikijs/types@3.8.0': - resolution: {integrity: sha512-I/b/aNg0rP+kznVDo7s3UK8jMcqEGTtoPDdQ+JlQ2bcJIyu/e2iRvl42GLIDMK03/W1YOHOuhlhQ7aM+XbKUeg==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - - '@tailwindcss/node@4.1.11': - resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} - - '@tailwindcss/oxide-android-arm64@4.1.11': - resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.11': - resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.11': - resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.11': - resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': - resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': - resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': - resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': - resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.1.11': - resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.1.11': - resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': - resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': - resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.11': - resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} - engines: {node: '>= 10'} - - '@tailwindcss/postcss@4.1.11': - resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/mdx@2.0.13': - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@24.0.13': - resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==} - - '@types/react-dom@19.1.6': - resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} - peerDependencies: - '@types/react': ^19.0.0 - - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} - - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - algoliasearch@5.32.0: - resolution: {integrity: sha512-84xBncKNPBK8Ae89F65+SyVcOihrIbm/3N7to+GpRBHEUXGjA3ydWTMpcRW6jmFzkBQ/eqYy/y+J+NBpJWYjBg==} - engines: {node: '>= 14.0.0'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - - astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - collapse-white-space@2.1.0: - resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - compute-scroll-into-view@3.1.1: - resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} - - css-mediaquery@0.1.2: - resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.2.0: - resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} - engines: {node: '>=10.13.0'} - - esast-util-from-estree@2.0.0: - resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} - - esast-util-from-js@2.0.1: - resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-util-attach-comments@3.0.0: - resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} - - estree-util-build-jsx@3.0.1: - resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} - - estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - - estree-util-scope@1.0.0: - resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} - - estree-util-to-js@2.0.0: - resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} - - estree-util-value-to-estree@3.4.0: - resolution: {integrity: sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==} - - estree-util-visit@2.0.0: - resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} - engines: {node: '>=14.14'} - - fumadocs-core@15.6.3: - resolution: {integrity: sha512-71IPC6Y0ZLPHlavYormnF1r2uX/lNrTFTYCEh6Akll8hWxRNbKG9Hk4xpFJDTkU83c8eLtHk2iow/ccQkcV6Hw==} - peerDependencies: - '@oramacloud/client': 1.x.x || 2.x.x - '@types/react': '*' - algoliasearch: 5.x.x - next: 14.x.x || 15.x.x - react: 18.x.x || 19.x.x - react-dom: 18.x.x || 19.x.x - peerDependenciesMeta: - '@oramacloud/client': - optional: true - '@types/react': - optional: true - algoliasearch: - optional: true - next: - optional: true - react: - optional: true - react-dom: - optional: true - - fumadocs-mdx@11.6.11: - resolution: {integrity: sha512-8KPOMU53ujQtNWvmmBpyGb9BRdFXZKS0m0O6udSlXCoLU/VZlQSJE0ntxX1e5JCDVsxPR63jleCVq1c/WXmEVw==} - hasBin: true - peerDependencies: - '@fumadocs/mdx-remote': ^1.2.0 - fumadocs-core: ^14.0.0 || ^15.0.0 - next: ^15.3.0 - vite: 6.x.x - peerDependenciesMeta: - '@fumadocs/mdx-remote': - optional: true - next: - optional: true - vite: - optional: true - - fumadocs-ui@15.6.3: - resolution: {integrity: sha512-FN2wpPacoJ6vHhwVZF+tiAezKKqOOy5hpQxBUer0Bda95I7uFyloGF/ilVMrrCSbzd5bt/bKzXRJQwcJAw9vAQ==} - peerDependencies: - '@types/react': '*' - next: 14.x.x || 15.x.x - react: 18.x.x || 19.x.x - react-dom: 18.x.x || 19.x.x - tailwindcss: ^3.4.14 || ^4.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - next: - optional: true - tailwindcss: - optional: true - - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - - github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - hast-util-to-estree@3.1.3: - resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-to-jsx-runtime@2.3.6: - resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - - hast-util-to-string@3.0.1: - resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - hyphenate-style-name@1.1.0: - resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} - - image-size@2.0.2: - resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} - engines: {node: '>=16.x'} - hasBin: true - - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - - is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - - is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - - is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} - engines: {node: '>= 12.0.0'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} - engines: {node: 20 || >=22} - - lucide-react@0.525.0: - resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - - markdown-extensions@2.0.0: - resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} - engines: {node: '>=16'} - - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - - matchmediaquery@0.4.2: - resolution: {integrity: sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==} - - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - - mdast-util-from-markdown@2.0.2: - resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} - - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - - mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - - mdast-util-mdx-jsx@3.2.0: - resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} - - mdast-util-mdx@3.0.0: - resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} - - mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - - micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - - micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - - micromark-extension-gfm-table@2.1.1: - resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - - micromark-extension-mdx-expression@3.0.1: - resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} - - micromark-extension-mdx-jsx@3.0.2: - resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} - - micromark-extension-mdx-md@2.0.0: - resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} - - micromark-extension-mdxjs-esm@3.0.0: - resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} - - micromark-extension-mdxjs@3.0.0: - resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-mdx-expression@2.0.3: - resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-events-to-acorn@2.0.3: - resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - next-themes@0.4.6: - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - - next@15.3.5: - resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - npm-to-yarn@3.0.1: - resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - oniguruma-parser@0.12.1: - resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - - oniguruma-to-es@4.3.3: - resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - - p-limit@6.2.0: - resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} - engines: {node: '>=18'} - - parse-entities@4.0.2: - resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} - engines: {node: '>=4'} - - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - react-dom@19.1.0: - resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} - peerDependencies: - react: ^19.1.0 - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-medium-image-zoom@5.2.14: - resolution: {integrity: sha512-nfTVYcAUnBzXQpPDcZL+cG/e6UceYUIG+zDcnemL7jtAqbJjVVkA85RgneGtJeni12dTyiRPZVM6Szkmwd/o8w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.1: - resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-responsive@10.0.1: - resolution: {integrity: sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8.0' - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} - engines: {node: '>=0.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - recma-build-jsx@1.0.0: - resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} - - recma-jsx@1.0.0: - resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} - - recma-parse@1.0.0: - resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} - - recma-stringify@1.0.0: - resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.0.1: - resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} - - rehype-recma@1.0.0: - resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} - - remark-gfm@4.0.1: - resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - - remark-mdx@3.1.0: - resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - remark@15.0.1: - resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - - scroll-into-view-if-needed@3.1.0: - resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - - shallow-equal@3.1.0: - resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==} - - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shiki@3.8.0: - resolution: {integrity: sha512-yPqK0y68t20aakv+3aMTpUMJZd6UHaBY2/SBUDowh9M70gVUwqT0bf7Kz5CWG0AXfHtFvXCHhBBHVAzdp0ILoQ==} - - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - style-to-js@1.1.17: - resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} - - style-to-object@1.0.9: - resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} - - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - - tailwindcss@4.1.11: - resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} - - tapable@2.2.2: - resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} - engines: {node: '>=6'} - - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} - - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - - unist-util-position-from-estree@2.0.0: - resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - - unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} - engines: {node: '>= 14.6'} - hasBin: true - - yocto-queue@1.2.1: - resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} - engines: {node: '>=12.20'} - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - - zod@4.0.5: - resolution: {integrity: sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@algolia/client-abtesting@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/client-analytics@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/client-common@5.32.0': {} - - '@algolia/client-insights@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/client-personalization@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/client-query-suggestions@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/client-search@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/ingestion@1.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/monitoring@1.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/recommend@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - '@algolia/requester-browser-xhr@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - - '@algolia/requester-fetch@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - - '@algolia/requester-node-http@5.32.0': - dependencies: - '@algolia/client-common': 5.32.0 - - '@alloc/quick-lru@5.2.0': {} - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - - '@content-collections/core@0.10.0(typescript@5.8.3)': - dependencies: - '@standard-schema/spec': 1.0.0 - camelcase: 8.0.0 - chokidar: 4.0.3 - esbuild: 0.25.6 - gray-matter: 4.0.3 - p-limit: 6.2.0 - picomatch: 4.0.2 - pluralize: 8.0.0 - serialize-javascript: 6.0.2 - tinyglobby: 0.2.14 - typescript: 5.8.3 - yaml: 2.8.0 - zod: 3.25.76 - - '@content-collections/integrations@0.2.1(@content-collections/core@0.10.0(typescript@5.8.3))': - dependencies: - '@content-collections/core': 0.10.0(typescript@5.8.3) - - '@content-collections/next@0.2.6(@content-collections/core@0.10.0(typescript@5.8.3))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': - dependencies: - '@content-collections/core': 0.10.0(typescript@5.8.3) - '@content-collections/integrations': 0.2.1(@content-collections/core@0.10.0(typescript@5.8.3)) - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - - '@emnapi/runtime@1.4.4': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.25.6': - optional: true - - '@esbuild/android-arm64@0.25.6': - optional: true - - '@esbuild/android-arm@0.25.6': - optional: true - - '@esbuild/android-x64@0.25.6': - optional: true - - '@esbuild/darwin-arm64@0.25.6': - optional: true - - '@esbuild/darwin-x64@0.25.6': - optional: true - - '@esbuild/freebsd-arm64@0.25.6': - optional: true - - '@esbuild/freebsd-x64@0.25.6': - optional: true - - '@esbuild/linux-arm64@0.25.6': - optional: true - - '@esbuild/linux-arm@0.25.6': - optional: true - - '@esbuild/linux-ia32@0.25.6': - optional: true - - '@esbuild/linux-loong64@0.25.6': - optional: true - - '@esbuild/linux-mips64el@0.25.6': - optional: true - - '@esbuild/linux-ppc64@0.25.6': - optional: true - - '@esbuild/linux-riscv64@0.25.6': - optional: true - - '@esbuild/linux-s390x@0.25.6': - optional: true - - '@esbuild/linux-x64@0.25.6': - optional: true - - '@esbuild/netbsd-arm64@0.25.6': - optional: true - - '@esbuild/netbsd-x64@0.25.6': - optional: true - - '@esbuild/openbsd-arm64@0.25.6': - optional: true - - '@esbuild/openbsd-x64@0.25.6': - optional: true - - '@esbuild/openharmony-arm64@0.25.6': - optional: true - - '@esbuild/sunos-x64@0.25.6': - optional: true - - '@esbuild/win32-arm64@0.25.6': - optional: true - - '@esbuild/win32-ia32@0.25.6': - optional: true - - '@esbuild/win32-x64@0.25.6': - optional: true - - '@floating-ui/core@1.7.2': - dependencies: - '@floating-ui/utils': 0.2.10 - - '@floating-ui/dom@1.7.2': - dependencies: - '@floating-ui/core': 1.7.2 - '@floating-ui/utils': 0.2.10 - - '@floating-ui/react-dom@2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@floating-ui/dom': 1.7.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - '@floating-ui/utils@0.2.10': {} - - '@formatjs/intl-localematcher@0.6.1': - dependencies: - tslib: 2.8.1 - - '@img/sharp-darwin-arm64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 - optional: true - - '@img/sharp-darwin-x64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.0': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.0': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.0': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.0': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.0': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.0': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.0': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - optional: true - - '@img/sharp-linux-arm64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 - optional: true - - '@img/sharp-linux-arm@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 - optional: true - - '@img/sharp-linux-ppc64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 - optional: true - - '@img/sharp-linux-s390x@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 - optional: true - - '@img/sharp-linux-x64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - optional: true - - '@img/sharp-wasm32@0.34.3': - dependencies: - '@emnapi/runtime': 1.4.4 - optional: true - - '@img/sharp-win32-arm64@0.34.3': - optional: true - - '@img/sharp-win32-ia32@0.34.3': - optional: true - - '@img/sharp-win32-x64@0.34.3': - optional: true - - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - - '@jridgewell/gen-mapping@0.3.12': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.29 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.4': {} - - '@jridgewell/trace-mapping@0.3.29': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 - - '@mdx-js/mdx@3.1.0(acorn@8.15.0)': - dependencies: - '@types/estree': 1.0.8 - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdx': 2.0.13 - collapse-white-space: 2.1.0 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - estree-util-scope: 1.0.0 - estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 - markdown-extensions: 2.0.0 - recma-build-jsx: 1.0.0 - recma-jsx: 1.0.0(acorn@8.15.0) - recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.0 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - source-map: 0.7.4 - unified: 11.0.5 - unist-util-position-from-estree: 2.0.0 - unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - transitivePeerDependencies: - - acorn - - supports-color - - '@next/env@15.3.5': {} - - '@next/swc-darwin-arm64@15.3.5': - optional: true - - '@next/swc-darwin-x64@15.3.5': - optional: true - - '@next/swc-linux-arm64-gnu@15.3.5': - optional: true - - '@next/swc-linux-arm64-musl@15.3.5': - optional: true - - '@next/swc-linux-x64-gnu@15.3.5': - optional: true - - '@next/swc-linux-x64-musl@15.3.5': - optional: true - - '@next/swc-win32-arm64-msvc@15.3.5': - optional: true - - '@next/swc-win32-x64-msvc@15.3.5': - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@orama/orama@3.1.11': {} - - '@orama/tokenizers@3.1.11': - dependencies: - '@orama/orama': 3.1.11 - - '@radix-ui/number@1.1.1': {} - - '@radix-ui/primitive@1.1.2': {} - - '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-context@1.1.2(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-direction@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-id@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - aria-hidden: 1.2.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/rect': 1.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/rect': 1.1.1 - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-use-size@1.1.1(@types/react@19.1.8)(react@19.1.0)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - optionalDependencies: - '@types/react': 19.1.8 - - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - '@types/react-dom': 19.1.6(@types/react@19.1.8) - - '@radix-ui/rect@1.1.1': {} - - '@shikijs/core@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.3 - - '@shikijs/engine-oniguruma@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - - '@shikijs/rehype@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - '@types/hast': 3.0.4 - hast-util-to-string: 3.0.1 - shiki: 3.8.0 - unified: 11.0.5 - unist-util-visit: 5.0.0 - - '@shikijs/themes@3.8.0': - dependencies: - '@shikijs/types': 3.8.0 - - '@shikijs/transformers@3.8.0': - dependencies: - '@shikijs/core': 3.8.0 - '@shikijs/types': 3.8.0 - - '@shikijs/types@3.8.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@standard-schema/spec@1.0.0': {} - - '@swc/counter@0.1.3': {} - - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/node@4.1.11': - dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.2 - jiti: 2.4.2 - lightningcss: 1.30.1 - magic-string: 0.30.17 - source-map-js: 1.2.1 - tailwindcss: 4.1.11 - - '@tailwindcss/oxide-android-arm64@4.1.11': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.11': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.11': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.11': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.1.11': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': - optional: true - - '@tailwindcss/oxide@4.1.11': - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-x64': 4.1.11 - '@tailwindcss/oxide-freebsd-x64': 4.1.11 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-x64-musl': 4.1.11 - '@tailwindcss/oxide-wasm32-wasi': 4.1.11 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - - '@tailwindcss/postcss@4.1.11': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.11 - '@tailwindcss/oxide': 4.1.11 - postcss: 8.5.6 - tailwindcss: 4.1.11 - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree-jsx@1.0.5': - dependencies: - '@types/estree': 1.0.8 - - '@types/estree@1.0.8': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdx@2.0.13': {} - - '@types/ms@2.1.0': {} - - '@types/node@24.0.13': - dependencies: - undici-types: 7.8.0 - - '@types/react-dom@19.1.6(@types/react@19.1.8)': - dependencies: - '@types/react': 19.1.8 - - '@types/react@19.1.8': - dependencies: - csstype: 3.1.3 - - '@types/unist@2.0.11': {} - - '@types/unist@3.0.3': {} - - '@ungap/structured-clone@1.3.0': {} - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - algoliasearch@5.32.0: - dependencies: - '@algolia/client-abtesting': 5.32.0 - '@algolia/client-analytics': 5.32.0 - '@algolia/client-common': 5.32.0 - '@algolia/client-insights': 5.32.0 - '@algolia/client-personalization': 5.32.0 - '@algolia/client-query-suggestions': 5.32.0 - '@algolia/client-search': 5.32.0 - '@algolia/ingestion': 1.32.0 - '@algolia/monitoring': 1.32.0 - '@algolia/recommend': 5.32.0 - '@algolia/requester-browser-xhr': 5.32.0 - '@algolia/requester-fetch': 5.32.0 - '@algolia/requester-node-http': 5.32.0 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - - astring@1.9.0: {} - - bail@2.0.2: {} - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - - camelcase@8.0.0: {} - - caniuse-lite@1.0.30001727: {} - - ccount@2.0.1: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - character-reference-invalid@2.0.1: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - chownr@3.0.0: {} - - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 - - client-only@0.0.1: {} - - clsx@2.1.1: {} - - collapse-white-space@2.1.0: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - optional: true - - color-name@1.1.4: - optional: true - - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - - comma-separated-tokens@2.0.3: {} - - compute-scroll-into-view@3.1.1: {} - - css-mediaquery@0.1.2: {} - - cssesc@3.0.0: {} - - csstype@3.1.3: {} - - debug@4.4.1: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.2.0: - dependencies: - character-entities: 2.0.2 - - dequal@2.0.3: {} - - detect-libc@2.0.4: {} - - detect-node-es@1.1.0: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - enhanced-resolve@5.18.2: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.2 - - esast-util-from-estree@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - devlop: 1.1.0 - estree-util-visit: 2.0.0 - unist-util-position-from-estree: 2.0.0 - - esast-util-from-js@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - acorn: 8.15.0 - esast-util-from-estree: 2.0.0 - vfile-message: 4.0.2 - - esbuild@0.25.6: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 - - escape-string-regexp@5.0.0: {} - - esprima@4.0.1: {} - - estree-util-attach-comments@3.0.0: - dependencies: - '@types/estree': 1.0.8 - - estree-util-build-jsx@3.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - estree-walker: 3.0.3 - - estree-util-is-identifier-name@3.0.0: {} - - estree-util-scope@1.0.0: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - - estree-util-to-js@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - astring: 1.9.0 - source-map: 0.7.4 - - estree-util-value-to-estree@3.4.0: - dependencies: - '@types/estree': 1.0.8 - - estree-util-visit@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/unist': 3.0.3 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - extend@3.0.2: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.4.6(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - fs-extra@11.3.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - - fumadocs-core@15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@formatjs/intl-localematcher': 0.6.1 - '@orama/orama': 3.1.11 - '@shikijs/rehype': 3.8.0 - '@shikijs/transformers': 3.8.0 - github-slugger: 2.0.0 - hast-util-to-estree: 3.1.3 - hast-util-to-jsx-runtime: 2.3.6 - image-size: 2.0.2 - negotiator: 1.0.0 - npm-to-yarn: 3.0.1 - react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0) - remark: 15.0.1 - remark-gfm: 4.0.1 - remark-rehype: 11.1.2 - scroll-into-view-if-needed: 3.1.0 - shiki: 3.8.0 - unist-util-visit: 5.0.0 - optionalDependencies: - '@types/react': 19.1.8 - algoliasearch: 5.32.0 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - transitivePeerDependencies: - - supports-color - - fumadocs-mdx@11.6.11(acorn@8.15.0)(fumadocs-core@15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): - dependencies: - '@mdx-js/mdx': 3.1.0(acorn@8.15.0) - '@standard-schema/spec': 1.0.0 - chokidar: 4.0.3 - esbuild: 0.25.6 - estree-util-value-to-estree: 3.4.0 - fumadocs-core: 15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - js-yaml: 4.1.0 - lru-cache: 11.1.0 - picocolors: 1.1.1 - tinyexec: 1.0.1 - tinyglobby: 0.2.14 - unist-util-visit: 5.0.0 - zod: 4.0.5 - optionalDependencies: - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - transitivePeerDependencies: - - acorn - - supports-color - - fumadocs-ui@15.6.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11): - dependencies: - '@radix-ui/react-accordion': 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-navigation-menu': 1.2.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-scroll-area': 1.2.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - class-variance-authority: 0.7.1 - fumadocs-core: 15.6.3(@types/react@19.1.8)(algoliasearch@5.32.0)(next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - lodash.merge: 4.6.2 - next-themes: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - postcss-selector-parser: 7.1.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-medium-image-zoom: 5.2.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - scroll-into-view-if-needed: 3.1.0 - tailwind-merge: 3.3.1 - optionalDependencies: - '@types/react': 19.1.8 - next: 15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - tailwindcss: 4.1.11 - transitivePeerDependencies: - - '@oramacloud/client' - - '@types/react-dom' - - algoliasearch - - supports-color - - get-nonce@1.0.1: {} - - github-slugger@2.0.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - graceful-fs@4.2.11: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - hast-util-to-estree@3.1.3: - dependencies: - '@types/estree': 1.0.8 - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-attach-comments: 3.0.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.17 - unist-util-position: 5.0.0 - zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-jsx-runtime@2.3.6: - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.17 - unist-util-position: 5.0.0 - vfile-message: 4.0.2 - transitivePeerDependencies: - - supports-color - - hast-util-to-string@3.0.1: - dependencies: - '@types/hast': 3.0.4 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - html-void-elements@3.0.0: {} - - hyphenate-style-name@1.1.0: {} - - image-size@2.0.2: {} - - inline-style-parser@0.2.4: {} - - is-alphabetical@2.0.1: {} - - is-alphanumerical@2.0.1: - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - - is-arrayish@0.3.2: - optional: true - - is-decimal@2.0.1: {} - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-hexadecimal@2.0.1: {} - - is-number@7.0.0: {} - - is-plain-obj@4.1.0: {} - - jiti@2.4.2: {} - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - kind-of@6.0.3: {} - - lightningcss-darwin-arm64@1.30.1: - optional: true - - lightningcss-darwin-x64@1.30.1: - optional: true - - lightningcss-freebsd-x64@1.30.1: - optional: true - - lightningcss-linux-arm-gnueabihf@1.30.1: - optional: true - - lightningcss-linux-arm64-gnu@1.30.1: - optional: true - - lightningcss-linux-arm64-musl@1.30.1: - optional: true - - lightningcss-linux-x64-gnu@1.30.1: - optional: true - - lightningcss-linux-x64-musl@1.30.1: - optional: true - - lightningcss-win32-arm64-msvc@1.30.1: - optional: true - - lightningcss-win32-x64-msvc@1.30.1: - optional: true - - lightningcss@1.30.1: - dependencies: - detect-libc: 2.0.4 - optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 - - lodash.merge@4.6.2: {} - - longest-streak@3.1.0: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lru-cache@11.1.0: {} - - lucide-react@0.525.0(react@19.1.0): - dependencies: - react: 19.1.0 - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - - markdown-extensions@2.0.0: {} - - markdown-table@3.0.4: {} - - matchmediaquery@0.4.2: - dependencies: - css-mediaquery: 0.1.2 - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - - mdast-util-from-markdown@2.0.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.1.0: - dependencies: - mdast-util-from-markdown: 2.0.2 - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.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.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-expression@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-jsx@3.2.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - parse-entities: 4.0.2 - stringify-entities: 4.0.4 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx@3.0.0: - dependencies: - mdast-util-from-markdown: 2.0.2 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdxjs-esm@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 - - mdast-util-to-hast@13.2.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.0.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - merge2@1.4.1: {} - - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-autolink-literal@2.1.0: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-footnote@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-strikethrough@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-table@2.1.1: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-task-list-item@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0 - micromark-extension-gfm-footnote: 2.1.0 - micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.1 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-mdx-expression@3.0.1: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-factory-mdx-expression: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-mdx-jsx@3.0.2: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - micromark-factory-mdx-expression: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - vfile-message: 4.0.2 - - micromark-extension-mdx-md@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-mdxjs-esm@3.0.0: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.2 - - micromark-extension-mdxjs@3.0.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - micromark-extension-mdx-expression: 3.0.1 - micromark-extension-mdx-jsx: 3.0.2 - micromark-extension-mdx-md: 2.0.0 - micromark-extension-mdxjs-esm: 3.0.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-mdx-expression@2.0.3: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-events-to-acorn@2.0.3: - dependencies: - '@types/estree': 1.0.8 - '@types/unist': 3.0.3 - devlop: 1.1.0 - estree-util-visit: 2.0.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - vfile-message: 4.0.2 - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.1 - decode-named-character-reference: 1.2.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - minipass@7.1.2: {} - - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - - mkdirp@3.0.1: {} - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - negotiator@1.0.0: {} - - next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - next@15.3.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@next/env': 15.3.5 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001727 - postcss: 8.4.31 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(react@19.1.0) - optionalDependencies: - '@next/swc-darwin-arm64': 15.3.5 - '@next/swc-darwin-x64': 15.3.5 - '@next/swc-linux-arm64-gnu': 15.3.5 - '@next/swc-linux-arm64-musl': 15.3.5 - '@next/swc-linux-x64-gnu': 15.3.5 - '@next/swc-linux-x64-musl': 15.3.5 - '@next/swc-win32-arm64-msvc': 15.3.5 - '@next/swc-win32-x64-msvc': 15.3.5 - sharp: 0.34.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - npm-to-yarn@3.0.1: {} - - object-assign@4.1.1: {} - - oniguruma-parser@0.12.1: {} - - oniguruma-to-es@4.3.3: - dependencies: - oniguruma-parser: 0.12.1 - regex: 6.0.1 - regex-recursion: 6.0.2 - - p-limit@6.2.0: - dependencies: - yocto-queue: 1.2.1 - - parse-entities@4.0.2: - dependencies: - '@types/unist': 2.0.11 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.2.0 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - - pluralize@8.0.0: {} - - postcss-selector-parser@7.1.0: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - property-information@7.1.0: {} - - queue-microtask@1.2.3: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - react-dom@19.1.0(react@19.1.0): - dependencies: - react: 19.1.0 - scheduler: 0.26.0 - - react-is@16.13.1: {} - - react-medium-image-zoom@5.2.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0): - dependencies: - react: 19.1.0 - react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.1.8 - - react-remove-scroll@2.7.1(@types/react@19.1.8)(react@19.1.0): - dependencies: - react: 19.1.0 - react-remove-scroll-bar: 2.3.8(@types/react@19.1.8)(react@19.1.0) - react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.1.8)(react@19.1.0) - use-sidecar: 1.1.3(@types/react@19.1.8)(react@19.1.0) - optionalDependencies: - '@types/react': 19.1.8 - - react-responsive@10.0.1(react@19.1.0): - dependencies: - hyphenate-style-name: 1.1.0 - matchmediaquery: 0.4.2 - prop-types: 15.8.1 - react: 19.1.0 - shallow-equal: 3.1.0 - - react-style-singleton@2.2.3(@types/react@19.1.8)(react@19.1.0): - dependencies: - get-nonce: 1.0.1 - react: 19.1.0 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.1.8 - - react@19.1.0: {} - - readdirp@4.1.2: {} - - recma-build-jsx@1.0.0: - dependencies: - '@types/estree': 1.0.8 - estree-util-build-jsx: 3.0.1 - vfile: 6.0.3 - - recma-jsx@1.0.0(acorn@8.15.0): - dependencies: - acorn-jsx: 5.3.2(acorn@8.15.0) - estree-util-to-js: 2.0.0 - recma-parse: 1.0.0 - recma-stringify: 1.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - acorn - - recma-parse@1.0.0: - dependencies: - '@types/estree': 1.0.8 - esast-util-from-js: 2.0.1 - unified: 11.0.5 - vfile: 6.0.3 - - recma-stringify@1.0.0: - dependencies: - '@types/estree': 1.0.8 - estree-util-to-js: 2.0.0 - unified: 11.0.5 - vfile: 6.0.3 - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.0.1: - dependencies: - regex-utilities: 2.3.0 - - rehype-recma@1.0.0: - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - hast-util-to-estree: 3.1.3 - transitivePeerDependencies: - - supports-color - - remark-gfm@4.0.1: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-mdx@3.1.0: - dependencies: - mdast-util-mdx: 3.0.0 - micromark-extension-mdxjs: 3.0.0 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 - unified: 11.0.5 - vfile: 6.0.3 - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - - remark@15.0.1: - dependencies: - '@types/mdast': 4.0.4 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - reusify@1.1.0: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-buffer@5.2.1: {} - - scheduler@0.26.0: {} - - scroll-into-view-if-needed@3.1.0: - dependencies: - compute-scroll-into-view: 3.1.1 - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - semver@7.7.2: - optional: true - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - - shallow-equal@3.1.0: {} - - sharp@0.34.3: - dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 - optional: true - - shiki@3.8.0: - dependencies: - '@shikijs/core': 3.8.0 - '@shikijs/engine-javascript': 3.8.0 - '@shikijs/engine-oniguruma': 3.8.0 - '@shikijs/langs': 3.8.0 - '@shikijs/themes': 3.8.0 - '@shikijs/types': 3.8.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - - source-map-js@1.2.1: {} - - source-map@0.7.4: {} - - space-separated-tokens@2.0.2: {} - - sprintf-js@1.0.3: {} - - streamsearch@1.1.0: {} - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - strip-bom-string@1.0.0: {} - - style-to-js@1.1.17: - dependencies: - style-to-object: 1.0.9 - - style-to-object@1.0.9: - dependencies: - inline-style-parser: 0.2.4 - - styled-jsx@5.1.6(react@19.1.0): - dependencies: - client-only: 0.0.1 - react: 19.1.0 - - tailwind-merge@3.3.1: {} - - tailwindcss@4.1.11: {} - - tapable@2.2.2: {} - - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - - tinyexec@1.0.1: {} - - tinyglobby@0.2.14: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - tslib@2.8.1: {} - - typescript@5.8.3: {} - - undici-types@7.8.0: {} - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unist-util-is@6.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position-from-estree@2.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.1: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - - unist-util-visit@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - - universalify@2.0.1: {} - - use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0): - dependencies: - react: 19.1.0 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.1.8 - - use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0): - dependencies: - detect-node-es: 1.1.0 - react: 19.1.0 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.1.8 - - util-deprecate@1.0.2: {} - - vfile-message@4.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.2 - - yallist@5.0.0: {} - - yaml@2.8.0: {} - - yocto-queue@1.2.1: {} - - zod@3.25.76: {} - - zod@4.0.5: {} - - zwitch@2.0.4: {} diff --git a/package.json b/package.json index 463cee837326..c6107570b2a8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@chakra-ui/cli": "^2.4.1", + "typescript": "^5.1.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^3.0.9", diff --git a/packages/global/core/ai/llm/utils.ts b/packages/global/core/ai/llm/utils.ts new file mode 100644 index 000000000000..90b7777e78e7 --- /dev/null +++ b/packages/global/core/ai/llm/utils.ts @@ -0,0 +1,7 @@ +export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean) => { + return retainDatasetCite + ? text.replace(/[\[【]id[\]】]\(CITE\)/g, '') + : text + .replace(/[\[【]([a-f0-9]{24})[\]】](?:\([^\)]*\)?)?/g, '') + .replace(/[\[【]id[\]】]\(CITE\)/g, ''); +}; diff --git a/packages/global/core/ai/model.ts b/packages/global/core/ai/model.ts index 5de8129e0e6a..ca5aacde162d 100644 --- a/packages/global/core/ai/model.ts +++ b/packages/global/core/ai/model.ts @@ -56,7 +56,8 @@ export const defaultSTTModels: STTModelType[] = [ export const getModelFromList = ( modelList: { provider: ModelProviderIdType; name: string; model: string }[], - model: string + model: string, + language: string ): | { avatar: string; @@ -69,7 +70,7 @@ export const getModelFromList = ( if (!modelData) { return; } - const provider = getModelProvider(modelData.provider); + const provider = getModelProvider(modelData.provider, language); return { ...modelData, avatar: provider.avatar diff --git a/packages/global/core/ai/provider.ts b/packages/global/core/ai/provider.ts index 55270a0b9c10..9c8a7216ab27 100644 --- a/packages/global/core/ai/provider.ts +++ b/packages/global/core/ai/provider.ts @@ -1,224 +1,77 @@ -import { i18nT } from '../../../web/i18n/utils'; +import { ModelProviders } from '../../sdk/fastgpt-plugin'; -export type ModelProviderIdType = - | 'OpenAI' - | 'Claude' - | 'Gemini' - | 'Meta' - | 'MistralAI' - | 'Groq' - | 'Grok' - | 'Jina' - | 'AliCloud' - | 'Qwen' - | 'Doubao' - | 'DeepSeek' - | 'ChatGLM' - | 'Ernie' - | 'Moonshot' - | 'MiniMax' - | 'SparkDesk' - | 'Hunyuan' - | 'Baichuan' - | 'StepFun' - | 'ai360' - | 'Yi' - | 'Siliconflow' - | 'PPIO' - | 'OpenRouter' - | 'Ollama' - | 'novita' - | 'vertexai' - | 'BAAI' - | 'FishAudio' - | 'Intern' - | 'Moka' - | 'Jina' - | 'Other'; +export type ModelProviderIdType = keyof typeof ModelProviders; +type ProviderValueTypes = (typeof ModelProviders)[ModelProviderIdType]; +type langType = 'en' | 'zh-CN' | 'zh-Hant'; export type ModelProviderType = { id: ModelProviderIdType; name: any; avatar: string; + order: number; }; -export const ModelProviderList: ModelProviderType[] = [ - { - id: 'OpenAI', - name: 'OpenAI', - avatar: 'model/openai' - }, - { - id: 'Claude', - name: 'Claude', - avatar: 'model/claude' - }, - { - id: 'Gemini', - name: 'Gemini', - avatar: 'model/gemini' - }, - { - id: 'Meta', - name: 'Meta', - avatar: 'model/meta' - }, - { - id: 'MistralAI', - name: 'MistralAI', - avatar: 'model/mistral' - }, - { - id: 'Grok', - name: 'Grok', - avatar: 'model/grok' - }, - { - id: 'Groq', - name: 'Groq', - avatar: 'model/groq' - }, - { - id: 'Jina', - name: 'Jina', - avatar: 'model/jina' - }, - { - id: 'Qwen', - name: i18nT('common:model_qwen'), - avatar: 'model/qwen' - }, - { - id: 'Doubao', - name: i18nT('common:model_doubao'), - avatar: 'model/doubao' - }, - { - id: 'DeepSeek', - name: 'DeepSeek', - avatar: 'model/deepseek' - }, - { - id: 'ChatGLM', - name: i18nT('common:model_chatglm'), - avatar: 'model/chatglm' - }, - { - id: 'Ernie', - name: i18nT('common:model_ernie'), - avatar: 'model/ernie' - }, - { - id: 'Moonshot', - name: i18nT('common:model_moonshot'), - avatar: 'model/moonshot' - }, - { - id: 'MiniMax', - name: 'MiniMax', - avatar: 'model/minimax' - }, - { - id: 'SparkDesk', - name: i18nT('common:model_sparkdesk'), - avatar: 'model/sparkDesk' - }, - { - id: 'Hunyuan', - name: i18nT('common:model_hunyuan'), - avatar: 'model/hunyuan' - }, - { - id: 'Baichuan', - name: i18nT('common:model_baichuan'), - avatar: 'model/baichuan' - }, - { - id: 'StepFun', - name: i18nT('common:model_stepfun'), - avatar: 'model/stepfun' - }, - { - id: 'ai360', - name: '360 AI', - avatar: 'model/ai360' - }, - { - id: 'Yi', - name: i18nT('common:model_yi'), - avatar: 'model/yi' - }, - { - id: 'BAAI', - name: i18nT('common:model_baai'), - avatar: 'model/BAAI' - }, - { - id: 'FishAudio', - name: 'FishAudio', - avatar: 'model/fishaudio' - }, - { - id: 'Intern', - name: i18nT('common:model_intern'), - avatar: 'model/intern' - }, - { - id: 'Moka', - name: i18nT('common:model_moka'), - avatar: 'model/moka' - }, - { - id: 'Ollama', - name: 'Ollama', - avatar: 'model/ollama' - }, - { - id: 'OpenRouter', - name: 'OpenRouter', - avatar: 'model/openrouter' - }, - { - id: 'vertexai', - name: 'vertexai', - avatar: 'model/vertexai' - }, - { - id: 'novita', - name: 'novita', - avatar: 'model/novita' - }, - { - id: 'Jina', - name: 'Jina', - avatar: 'model/jina' - }, - { - id: 'AliCloud', - name: i18nT('common:model_alicloud'), - avatar: 'model/alicloud' - }, - { - id: 'Siliconflow', - name: i18nT('common:model_siliconflow'), - avatar: 'model/siliconflow' - }, - { - id: 'PPIO', - name: i18nT('common:model_ppio'), - avatar: 'model/ppio' - }, - { - id: 'Other', - name: i18nT('common:model_other'), - avatar: 'model/huggingface' +const getLocalizedName = (translations: ProviderValueTypes, language = 'en'): string => { + return translations[language as langType]; +}; + +export const formatModelProviderList = (language?: string) => { + return Object.entries(ModelProviders).map(([id, translations], index) => ({ + id: id as ModelProviderIdType, + name: getLocalizedName(translations, language), + avatar: `/api/system/plugin/models/${id}.svg`, + order: index + })); +}; +export const formatModelProviderMap = (language?: string) => { + const provider = {} as Record< + ModelProviderIdType, + { + id: string; + name: string; + avatar: string; + order: number; + } + >; + + Object.entries(ModelProviders).forEach(([id, translations], index) => { + provider[id as ModelProviderIdType] = { + id: id as ModelProviderIdType, + name: getLocalizedName(translations, language), + avatar: `/api/system/plugin/models/${id}.svg`, + order: index + }; + }); + + return provider; +}; + +const ModelProviderListCache = { + en: formatModelProviderList('en'), + 'zh-CN': formatModelProviderList('zh-CN'), + 'zh-Hant': formatModelProviderList('zh-Hant') +}; +const ModelProviderMapCache = { + en: formatModelProviderMap('en'), + 'zh-CN': formatModelProviderMap('zh-CN'), + 'zh-Hant': formatModelProviderMap('zh-Hant') +}; + +const defaultProvider = { + id: 'Other' as ModelProviderIdType, + name: 'Other', + avatar: 'model/other', + order: 0 +}; + +export const getModelProviders = (language = 'en') => { + return ModelProviderListCache[language as langType]; +}; + +export const getModelProvider = (provider?: ModelProviderIdType, language = 'en') => { + if (!provider) { + return defaultProvider; } -]; -export const ModelProviderMap = Object.fromEntries( - ModelProviderList.map((item, index) => [item.id, { ...item, order: index }]) -); -export const getModelProvider = (provider?: ModelProviderIdType) => { - if (!provider) return ModelProviderMap.Other; - return ModelProviderMap[provider] ?? ModelProviderMap.Other; + return ModelProviderMapCache[language as langType][provider] ?? defaultProvider; }; diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index eae74bcc8e34..39373a40fd87 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -6,7 +6,8 @@ import type { ChatCompletionContentPart as SdkChatCompletionContentPart, ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam, ChatCompletionToolMessageParam as SdkChatCompletionToolMessageParam, - ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam + ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam, + ChatCompletionTool } from 'openai/resources'; import { ChatMessageTypeEnum } from './constants'; import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; @@ -59,11 +60,7 @@ export type ChatCompletionAssistantToolParam = { role: 'assistant'; tool_calls: ChatCompletionMessageToolCall[]; }; -export type ChatCompletionMessageToolCall = ChatCompletionMessageToolCall & { - index?: number; - toolName?: string; - toolAvatar?: string; -}; + export type ChatCompletionMessageFunctionCall = SdkChatCompletionAssistantMessageParam.FunctionCall & { id?: string; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index a3514d88302f..624533289147 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -49,12 +49,17 @@ export type AppSchema = { teamTags: string[]; inheritPermission?: boolean; + // if access the app by favourite or quick + favourite?: boolean; + quick?: boolean; + // abandon defaultPermission?: number; }; export type AppListItemType = { _id: string; + parentId: ParentIdType; tmbId: string; name: string; avatar: string; diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 02176f9bca09..31dde95440e5 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -171,10 +171,15 @@ export const chats2GPTMessages = ({ return results; }; -export const GPTMessages2Chats = ( - messages: ChatCompletionMessageParam[], - reserveTool = true -): ChatItemType[] => { +export const GPTMessages2Chats = ({ + messages, + reserveTool = true, + getToolInfo +}: { + messages: ChatCompletionMessageParam[]; + reserveTool?: boolean; + getToolInfo?: (name: string) => { name: string; avatar: string }; +}): ChatItemType[] => { const chatMessages = messages .map((item) => { const obj = GPT2Chat[item.role]; @@ -280,10 +285,12 @@ export const GPTMessages2Chats = ( toolResponse = typeof toolResponse === 'string' ? toolResponse : JSON.stringify(toolResponse); + const toolInfo = getToolInfo?.(tool.function.name); + return { id: tool.id, - toolName: tool.toolName || '', - toolAvatar: tool.toolAvatar || '', + toolName: toolInfo?.name || '', + toolAvatar: toolInfo?.avatar || '', functionName: tool.function.name, params: tool.function.arguments, response: toolResponse as string diff --git a/packages/global/core/chat/favouriteApp/type.d.ts b/packages/global/core/chat/favouriteApp/type.d.ts new file mode 100644 index 000000000000..9fc8dce43b90 --- /dev/null +++ b/packages/global/core/chat/favouriteApp/type.d.ts @@ -0,0 +1,18 @@ +export type ChatFavouriteAppSchema = { + _id: string; + teamId: string; + appId: string; + favouriteTags: string[]; // tag id list + order: number; +}; + +export type ChatFavouriteAppUpdateParams = { + appId: string; + order: number; +}; + +export type ChatFavouriteApp = ChatFavouriteAppSchema & { + name: string; + avatar: string; + intro: string; +}; diff --git a/packages/global/core/chat/setting/type.d.ts b/packages/global/core/chat/setting/type.d.ts index 24f7d05eaf9d..ce0cf7b3e162 100644 --- a/packages/global/core/chat/setting/type.d.ts +++ b/packages/global/core/chat/setting/type.d.ts @@ -7,22 +7,30 @@ export type ChatSettingSchema = { homeTabTitle: string; wideLogoUrl?: string; squareLogoUrl?: string; + selectedTools: { pluginId: string; - name: string; - avatar: string; inputs?: Record<`${NodeInputKeyEnum}` | string, any>; }[]; + quickAppIds: string[]; + favouriteTags: { + id: string; + name: string; + }[]; }; -export type ChatSettingUpdateParams = { - slogan?: string; - dialogTips?: string; - homeTabTitle?: string; - wideLogoUrl?: string; - squareLogoUrl?: string; - selectedTools: { - pluginId: string; - inputs?: Record<`${NodeInputKeyEnum}` | string, any>; - }[]; +export type ChatSettingUpdateParams = Partial>; + +export type QuickAppType = { _id: string; name: string; avatar: string }; +export type ChatFavouriteTagType = ChatSettingSchema['favouriteTags'][number]; +export type SelectedToolType = ChatSettingSchema['selectedTools'][number] & { + name: string; + avatar: string; }; + +export type ChatSettingReturnType = + | (Omit & { + quickAppList: QuickAppType[]; + selectedTools: SelectedToolType[]; + }) + | undefined; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index 1e92b13f528b..56de2473518c 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -9,7 +9,7 @@ import { } from './type.d'; import { sliceStrStartEnd } from '../../common/string/tools'; import { PublishChannelEnum } from '../../support/outLink/constant'; -import { removeDatasetCiteText } from '../../../service/core/ai/utils'; +import { removeDatasetCiteText } from '../ai/llm/utils'; // Concat 2 -> 1, and sort by role export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => { diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 5ed81f7d479f..2b9865bc5aed 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -20,6 +20,17 @@ import type { StoreNodeItemType } from '../type/node'; import { isValidReferenceValueFormat } from '../utils'; import type { RuntimeEdgeItemType, RuntimeNodeItemType } from './type'; +export const checkIsBranchNode = (node: RuntimeNodeItemType) => { + if (node.catchError) return true; + + const map: Record = { + [FlowNodeTypeEnum.classifyQuestion]: true, + [FlowNodeTypeEnum.userSelect]: true, + [FlowNodeTypeEnum.ifElseNode]: true + }; + return !!map[node.flowNodeType]; +}; + export const extractDeepestInteractive = ( interactive: WorkflowInteractiveResponseType ): WorkflowInteractiveResponseType => { @@ -276,35 +287,41 @@ export const filterWorkflowEdges = (edges: RuntimeEdgeItemType[]) => { }; /* - 1. 输入线分类:普通线和递归线(可以追溯到自身) - 2. 起始线全部非 waiting 执行,或递归线全部非 waiting 执行 + 1. 输入线分类:普通线(实际上就是从 start 直接过来的分支)和递归线(可以追溯到自身的分支) + 2. 递归线,会根据最近的一个 target 分支进行分类,同一个分支的属于一组 + 2. 起始线全部非 waiting 执行,或递归线任意一组全部非 waiting 执行 */ export const checkNodeRunStatus = ({ + nodesMap, node, runtimeEdges }: { + nodesMap: Map; node: RuntimeNodeItemType; runtimeEdges: RuntimeEdgeItemType[]; }) => { - /* - 区分普通连线和递归连线 - 递归连线:可以通过往上查询 nodes,最终追溯到自身 - */ - const splitEdges2WorkflowEdges = ({ - sourceEdges, - allEdges, - currentNode - }: { - sourceEdges: RuntimeEdgeItemType[]; - allEdges: RuntimeEdgeItemType[]; - currentNode: RuntimeNodeItemType; - }) => { - const commonEdges: RuntimeEdgeItemType[] = []; - const recursiveEdges: RuntimeEdgeItemType[] = []; + const filterRuntimeEdges = filterWorkflowEdges(runtimeEdges); - const checkIsCircular = (startEdge: RuntimeEdgeItemType, initialVisited: string[]): boolean => { - const stack: Array<{ edge: RuntimeEdgeItemType; visited: Set }> = [ - { edge: startEdge, visited: new Set(initialVisited) } + const splitNodeEdges = (targetNode: RuntimeNodeItemType) => { + const commonEdges: RuntimeEdgeItemType[] = []; + const recursiveEdgeGroupsMap = new Map(); + + const getEdgeLastBranchHandle = ({ + startEdge, + targetNodeId + }: { + startEdge: RuntimeEdgeItemType; + targetNodeId: string; + }): string | '' | undefined => { + const stack: Array<{ + edge: RuntimeEdgeItemType; + visited: Set; + lasestBranchHandle?: string; + }> = [ + { + edge: startEdge, + visited: new Set([targetNodeId]) + } ]; const MAX_DEPTH = 3000; @@ -312,11 +329,18 @@ export const checkNodeRunStatus = ({ while (stack.length > 0 && iterations < MAX_DEPTH) { iterations++; - - const { edge, visited } = stack.pop()!; - - if (edge.source === currentNode.nodeId) { - return true; // 检测到环,并且环中包含当前节点 + const { edge, visited, lasestBranchHandle } = stack.pop()!; + + // Circle + if (edge.source === targetNode.nodeId) { + // 检查自身是否为分支节点 + const node = nodesMap.get(edge.source); + if (!node) return ''; + const isBranch = checkIsBranchNode(node); + if (isBranch) return edge.sourceHandle; + + // 检测到环,并且环中包含当前节点. 空字符代表是一个无分支循环,属于死循环,则忽略这个边。 + return lasestBranchHandle ?? ''; } if (visited.has(edge.source)) { @@ -327,54 +351,70 @@ export const checkNodeRunStatus = ({ newVisited.add(edge.source); // 查找目标节点的 source edges 并加入栈中 - const nextEdges = allEdges.filter((item) => item.target === edge.source); + const nextEdges = filterRuntimeEdges.filter((item) => item.target === edge.source); for (const nextEdge of nextEdges) { - stack.push({ edge: nextEdge, visited: newVisited }); + const node = nodesMap.get(nextEdge.target); + if (!node) continue; + const isBranch = checkIsBranchNode(node); + + stack.push({ + edge: nextEdge, + visited: newVisited, + lasestBranchHandle: isBranch ? edge.sourceHandle : lasestBranchHandle + }); } } - return false; + return; }; + const sourceEdges = filterRuntimeEdges.filter((item) => item.target === targetNode.nodeId); sourceEdges.forEach((edge) => { - if (checkIsCircular(edge, [currentNode.nodeId])) { - recursiveEdges.push(edge); - } else { + const lastBranchHandle = getEdgeLastBranchHandle({ + startEdge: edge, + targetNodeId: targetNode.nodeId + }); + + // 无效的循环,这条边则忽略 + if (lastBranchHandle === '') return; + + // 有效循环,则加入递归组 + if (lastBranchHandle) { + recursiveEdgeGroupsMap.set(lastBranchHandle, [ + ...(recursiveEdgeGroupsMap.get(lastBranchHandle) || []), + edge + ]); + } + // 无循环的连线,则加入普通组 + else { commonEdges.push(edge); } }); - return { commonEdges, recursiveEdges }; + return { commonEdges, recursiveEdgeGroups: Array.from(recursiveEdgeGroupsMap.values()) }; }; - const runtimeNodeSourceEdge = filterWorkflowEdges(runtimeEdges).filter( - (item) => item.target === node.nodeId - ); + // Classify edges + const { commonEdges, recursiveEdgeGroups } = splitNodeEdges(node); // Entry - if (runtimeNodeSourceEdge.length === 0) { + if (commonEdges.length === 0 && recursiveEdgeGroups.length === 0) { return 'run'; } - // Classify edges - const { commonEdges, recursiveEdges } = splitEdges2WorkflowEdges({ - sourceEdges: runtimeNodeSourceEdge, - allEdges: runtimeEdges, - currentNode: node - }); - // check active(其中一组边,至少有一个 active,且没有 waiting 即可运行) if ( - commonEdges.length > 0 && commonEdges.some((item) => item.status === 'active') && commonEdges.every((item) => item.status !== 'waiting') ) { return 'run'; } if ( - recursiveEdges.length > 0 && - recursiveEdges.some((item) => item.status === 'active') && - recursiveEdges.every((item) => item.status !== 'waiting') + recursiveEdgeGroups.some( + (item) => + item.some((item) => item.status === 'active') && + item.every((item) => item.status !== 'waiting') + ) ) { return 'run'; } @@ -383,7 +423,10 @@ export const checkNodeRunStatus = ({ if (commonEdges.length > 0 && commonEdges.every((item) => item.status === 'skipped')) { return 'skip'; } - if (recursiveEdges.length > 0 && recursiveEdges.every((item) => item.status === 'skipped')) { + if ( + recursiveEdgeGroups.length > 0 && + recursiveEdgeGroups.some((item) => item.every((item) => item.status === 'skipped')) + ) { return 'skip'; } diff --git a/packages/global/package.json b/packages/global/package.json index 93dd88a28f45..d70405d5f554 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -2,6 +2,7 @@ "name": "@fastgpt/global", "version": "1.0.0", "dependencies": { + "@fastgpt-sdk/plugin": "^0.1.12", "@apidevtools/swagger-parser": "^10.1.0", "@bany/curl-to-json": "^1.2.8", "axios": "^1.8.2", diff --git a/packages/global/sdk/fastgpt-plugin.ts b/packages/global/sdk/fastgpt-plugin.ts new file mode 100644 index 000000000000..e3577b174183 --- /dev/null +++ b/packages/global/sdk/fastgpt-plugin.ts @@ -0,0 +1 @@ +export * from '@fastgpt-sdk/plugin'; diff --git a/packages/service/common/redis/cache.ts b/packages/service/common/redis/cache.ts index 9dbf3b851df6..95fe68c866e0 100644 --- a/packages/service/common/redis/cache.ts +++ b/packages/service/common/redis/cache.ts @@ -45,12 +45,9 @@ export const getRedisCache = async (key: string) => { // Add value to cache export const incrValueToCache = async (key: string, increment: number) => { - if (!increment || increment === 0) return; + if (typeof increment !== 'number' || increment === 0) return; const redis = getGlobalRedisConnection(); try { - const exists = await redis.exists(getCacheKey(key)); - if (!exists) return; - await retryFn(() => redis.incrbyfloat(getCacheKey(key), increment)); } catch (error) {} }; diff --git a/packages/service/core/ai/config.ts b/packages/service/core/ai/config.ts index 3b7500c55157..79c83913b828 100644 --- a/packages/service/core/ai/config.ts +++ b/packages/service/core/ai/config.ts @@ -1,16 +1,5 @@ import OpenAI from '@fastgpt/global/core/ai'; -import type { - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - StreamChatType, - UnStreamChatType -} from '@fastgpt/global/core/ai/type'; -import { getErrText } from '@fastgpt/global/common/error/utils'; -import { addLog } from '../../common/system/log'; -import { i18nT } from '../../../web/i18n/utils'; import { type OpenaiAccountType } from '@fastgpt/global/support/user/team/type'; -import { getLLMModel } from './model'; -import { type LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; const aiProxyBaseUrl = process.env.AIPROXY_API_ENDPOINT ? `${process.env.AIPROXY_API_ENDPOINT}/v1` @@ -43,100 +32,3 @@ export const getAxiosConfig = (props?: { userKey?: OpenaiAccountType }) => { authorization: `Bearer ${apiKey}` }; }; - -export const createChatCompletion = async ({ - modelData, - body, - userKey, - timeout, - options -}: { - modelData?: LLMModelItemType; - body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; - userKey?: OpenaiAccountType; - timeout?: number; - options?: OpenAI.RequestOptions; -}): Promise< - { - getEmptyResponseTip: () => string; - } & ( - | { - response: StreamChatType; - isStreamResponse: true; - } - | { - response: UnStreamChatType; - isStreamResponse: false; - } - ) -> => { - try { - // Rewrite model - const modelConstantsData = modelData || getLLMModel(body.model); - if (!modelConstantsData) { - return Promise.reject(`${body.model} not found`); - } - body.model = modelConstantsData.model; - - const formatTimeout = timeout ? timeout : 600000; - const ai = getAIApi({ - userKey, - timeout: formatTimeout - }); - - addLog.debug(`Start create chat completion`, { - model: body.model - }); - - const response = await ai.chat.completions.create(body, { - ...options, - ...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}), - headers: { - ...options?.headers, - ...(modelConstantsData.requestAuth - ? { Authorization: `Bearer ${modelConstantsData.requestAuth}` } - : {}) - } - }); - - const isStreamResponse = - typeof response === 'object' && - response !== null && - ('iterator' in response || 'controller' in response); - - const getEmptyResponseTip = () => { - addLog.warn(`LLM response empty`, { - baseUrl: userKey?.baseUrl, - requestBody: body - }); - if (userKey?.baseUrl) { - return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`; - } - return i18nT('chat:LLM_model_response_empty'); - }; - - if (isStreamResponse) { - return { - response, - isStreamResponse: true, - getEmptyResponseTip - }; - } - - return { - response, - isStreamResponse: false, - getEmptyResponseTip - }; - } catch (error) { - addLog.error(`LLM response error`, error); - addLog.warn(`LLM response error`, { - baseUrl: userKey?.baseUrl, - requestBody: body - }); - if (userKey?.baseUrl) { - return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`); - } - return Promise.reject(error); - } -}; diff --git a/packages/service/core/ai/config/utils.ts b/packages/service/core/ai/config/utils.ts index 5636727ea797..e50bb49581fd 100644 --- a/packages/service/core/ai/config/utils.ts +++ b/packages/service/core/ai/config/utils.ts @@ -19,7 +19,7 @@ import { delay } from '@fastgpt/global/common/system/utils'; import { pluginClient } from '../../../thirdProvider/fastgptPlugin'; import { setCron } from '../../../common/system/cron'; -export const loadSystemModels = async (init = false) => { +export const loadSystemModels = async (init = false, language = 'en') => { const pushModel = (model: SystemModelItemType) => { global.systemModelList.push(model); @@ -113,7 +113,10 @@ export const loadSystemModels = async (init = false) => { const modelData: any = { ...model, ...dbModel?.metadata, - provider: getModelProvider(dbModel?.metadata?.provider || (model.provider as any)).id, + provider: getModelProvider( + dbModel?.metadata?.provider || (model.provider as any), + language + ).id, type: dbModel?.metadata?.type || model.type, isCustom: false, @@ -169,8 +172,8 @@ export const loadSystemModels = async (init = false) => { // Sort model list global.systemActiveModelList.sort((a, b) => { - const providerA = getModelProvider(a.provider); - const providerB = getModelProvider(b.provider); + const providerA = getModelProvider(a.provider, language); + const providerB = getModelProvider(b.provider, language); return providerA.order - providerB.order; }); global.systemActiveDesensitizedModels = global.systemActiveModelList.map((model) => ({ diff --git a/packages/service/core/ai/functions/createQuestionGuide.ts b/packages/service/core/ai/functions/createQuestionGuide.ts index f0ed6bf0d670..a2f57d0626bb 100644 --- a/packages/service/core/ai/functions/createQuestionGuide.ts +++ b/packages/service/core/ai/functions/createQuestionGuide.ts @@ -1,14 +1,11 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; -import { createChatCompletion } from '../config'; -import { countGptMessagesTokens, countPromptTokens } from '../../../common/string/tiktoken/index'; -import { loadRequestMessages } from '../../chat/utils'; -import { llmCompletionsBodyFormat, formatLLMResponse } from '../utils'; import { QuestionGuidePrompt, QuestionGuideFooterPrompt } from '@fastgpt/global/core/ai/prompt/agent'; import { addLog } from '../../../common/system/log'; import json5 from 'json5'; +import { createLLMResponse } from '../llm/request'; export async function createQuestionGuide({ messages, @@ -30,31 +27,23 @@ export async function createQuestionGuide({ content: `${customPrompt || QuestionGuidePrompt}\n${QuestionGuideFooterPrompt}` } ]; - const requestMessages = await loadRequestMessages({ - messages: concatMessages, - useVision: false - }); - const { response } = await createChatCompletion({ - body: llmCompletionsBodyFormat( - { - model, - temperature: 0.1, - max_tokens: 200, - messages: requestMessages, - stream: true - }, - model - ) + const { + answerText: answer, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model, + temperature: 0.1, + max_tokens: 200, + messages: concatMessages, + stream: true + } }); - const { text: answer, usage } = await formatLLMResponse(response); const start = answer.indexOf('['); const end = answer.lastIndexOf(']'); - const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(requestMessages)); - const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); - if (start === -1 || end === -1) { addLog.warn('Create question guide error', { answer }); return { diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index 50684f9e6bc1..165b5311043e 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -1,13 +1,11 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; -import { createChatCompletion } from '../config'; import { type ChatItemType } from '@fastgpt/global/core/chat/type'; -import { countGptMessagesTokens, countPromptTokens } from '../../../common/string/tiktoken/index'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getLLMModel } from '../model'; -import { llmCompletionsBodyFormat, formatLLMResponse } from '../utils'; import { addLog } from '../../../common/system/log'; -import { filterGPTMessageByMaxContext } from '../../chat/utils'; +import { filterGPTMessageByMaxContext } from '../llm/utils'; import json5 from 'json5'; +import { createLLMResponse } from '../llm/request'; /* query extension - 问题扩展 @@ -167,20 +165,17 @@ assistant: ${chatBg} } ] as any; - const { response } = await createChatCompletion({ - body: llmCompletionsBodyFormat( - { - stream: true, - model: modelData.model, - temperature: 0.1, - messages - }, - modelData - ) + const { + answerText: answer, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + stream: true, + model: modelData.model, + temperature: 0.1, + messages + } }); - const { text: answer, usage } = await formatLLMResponse(response); - const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(messages)); - const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); if (!answer) { return { diff --git a/packages/service/core/ai/llm/prompt.ts b/packages/service/core/ai/llm/prompt.ts new file mode 100644 index 000000000000..f87fc2a4b60b --- /dev/null +++ b/packages/service/core/ai/llm/prompt.ts @@ -0,0 +1,41 @@ +import { replaceVariable } from '@fastgpt/global/common/string/tools'; +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; + +export const getPromptToolCallPrompt = (tools: ChatCompletionTool['function'][]) => { + const prompt = ` +你是一个智能机器人,除了可以回答用户问题外,你还掌握工具的使用能力。有时候,你可以依赖工具的运行结果,来更准确的回答用户。 + +工具使用了 JSON Schema 的格式声明,格式为:{name: 工具名; description: 工具描述; parameters: 工具参数},其中 name 是工具的唯一标识,parameters 包含工具的参数、类型、描述、必填项等。 + +请你根据工具描述,决定回答问题或是使用工具。你的每次输出都必须以0,1开头,代表是否需要调用工具: +0: 不使用工具,直接回答内容。 +1: 使用工具,返回工具调用的参数。 + +## 回答示例 + +- 0: 你好,有什么可以帮助你的么? +- 1: ${JSON.stringify({ name: 'searchToolId1' })} +- 0: 现在是2022年5月5日,星期四,中午12点。 +- 1: ${JSON.stringify({ name: 'searchToolId2', arguments: { city: '杭州' } })} +- 0: 今天杭州是晴天。 +- 1: ${JSON.stringify({ name: 'searchToolId3', arguments: { query: '杭州 天气 去哪里玩' } })} +- 0: 今天杭州是晴天,适合去西湖、灵隐寺、千岛湖等地玩。 + +## 可用工具列表 + +""" +{{toolSchema}} +""" + +`; + + const schema = tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.parameters + })); + + return replaceVariable(prompt, { + toolSchema: JSON.stringify(schema) + }); +}; diff --git a/packages/service/core/ai/llm/promptToolCall.ts b/packages/service/core/ai/llm/promptToolCall.ts new file mode 100644 index 000000000000..3d82fb669495 --- /dev/null +++ b/packages/service/core/ai/llm/promptToolCall.ts @@ -0,0 +1,118 @@ +import { getNanoid, sliceJsonStr } from '@fastgpt/global/common/string/tools'; +import json5 from 'json5'; +import type { + ChatCompletionMessageParam, + ChatCompletionMessageToolCall, + ChatCompletionSystemMessageParam, + ChatCompletionTool +} from '@fastgpt/global/core/ai/type'; +import { getPromptToolCallPrompt } from './prompt'; +import { cloneDeep } from 'lodash'; + +export const promptToolCallMessageRewrite = ( + messages: ChatCompletionMessageParam[], + tools: ChatCompletionTool[] +) => { + const cloneMessages = cloneDeep(messages); + + // Add system prompt too messages + let systemMessage = cloneMessages.find( + (item) => item.role === 'system' + ) as ChatCompletionSystemMessageParam; + + if (!systemMessage) { + systemMessage = { + role: 'system', + content: '' + }; + cloneMessages.unshift(systemMessage); + } + + if (typeof systemMessage?.content === 'string') { + systemMessage.content = + `${systemMessage.content}\n\n${getPromptToolCallPrompt(tools.map((tool) => tool.function))}`.trim(); + } else if (Array.isArray(systemMessage.content)) { + systemMessage.content.push({ + type: 'text', + text: getPromptToolCallPrompt(tools.map((tool) => tool.function)) + }); + } else { + throw new Error('Prompt call invalid input'); + } + + /* + Format tool messages, rewrite assistant/tool message + 1. Assistant, not tool_calls: skip + 2. Assistant, tool_calls: rewrite to assistant text + 3. Tool: rewrite to user text + */ + for (let i = 0; i < cloneMessages.length; i++) { + const message = cloneMessages[i]; + if (message.role === 'assistant') { + if (message.content && typeof message.content === 'string') { + message.content = `0: ${message.content}`; + } else if (message.tool_calls?.length) { + message.content = `1: ${JSON.stringify(message.tool_calls[0].function)}`; + delete message.tool_calls; + } + } else if (message.role === 'tool') { + cloneMessages.splice(i, 1, { + role: 'user', + content: `\n${message.content}\n` + }); + } + } + + return cloneMessages; +}; + +const ERROR_TEXT = 'Tool run error'; +export const parsePromptToolCall = ( + str: string +): { + answer: string; + toolCalls?: ChatCompletionMessageToolCall[]; +} => { + str = str.trim(); + // 首先,使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS + const prefixReg = /1(:|:)/; + + if (prefixReg.test(str)) { + const toolString = sliceJsonStr(str); + + try { + const toolCall = json5.parse(toolString) as { name: string; arguments: Object }; + + return { + answer: '', + toolCalls: [ + { + id: getNanoid(), + type: 'function' as const, + function: { + name: toolCall.name, + arguments: JSON.stringify(toolCall.arguments) + } + } + ] + }; + } catch (error) { + if (prefixReg.test(str)) { + return { + answer: ERROR_TEXT + }; + } else { + return { + answer: str + }; + } + } + } else { + const firstIndex = str.indexOf('0:') !== -1 ? str.indexOf('0:') : str.indexOf('0:'); + if (firstIndex > -1 && firstIndex < 6) { + str = str.substring(firstIndex + 2).trim(); + } + + return { answer: str }; + } +}; diff --git a/packages/service/core/ai/llm/request.ts b/packages/service/core/ai/llm/request.ts new file mode 100644 index 000000000000..238ff44e9198 --- /dev/null +++ b/packages/service/core/ai/llm/request.ts @@ -0,0 +1,648 @@ +import type { + ChatCompletion, + ChatCompletionCreateParamsNonStreaming, + ChatCompletionCreateParamsStreaming, + ChatCompletionMessageParam, + ChatCompletionMessageToolCall, + CompletionFinishReason, + CompletionUsage, + OpenAI, + StreamChatType, + UnStreamChatType +} from '@fastgpt/global/core/ai/type'; +import { computedTemperature, parseLLMStreamResponse, parseReasoningContent } from '../utils'; +import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils'; +import { getAIApi } from '../config'; +import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { parsePromptToolCall, promptToolCallMessageRewrite } from './promptToolCall'; +import { getLLMModel } from '../model'; +import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; +import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; +import { loadRequestMessages } from './utils'; +import { addLog } from '../../../common/system/log'; +import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; +import { i18nT } from '../../../../web/i18n/utils'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import json5 from 'json5'; + +type ResponseEvents = { + onStreaming?: ({ text }: { text: string }) => void; + onReasoning?: ({ text }: { text: string }) => void; + onToolCall?: ({ call }: { call: ChatCompletionMessageToolCall }) => void; + onToolParam?: ({ tool, params }: { tool: ChatCompletionMessageToolCall; params: string }) => void; +}; + +type CreateLLMResponseProps = { + userKey?: OpenaiAccountType; + body: LLMRequestBodyType; + isAborted?: () => boolean | undefined; + custonHeaders?: Record; +} & ResponseEvents; + +type LLMResponse = { + isStreamResponse: boolean; + answerText: string; + reasoningText: string; + toolCalls?: ChatCompletionMessageToolCall[]; + finish_reason: CompletionFinishReason; + getEmptyResponseTip: () => string; + usage: { + inputTokens: number; + outputTokens: number; + }; + + requestMessages: ChatCompletionMessageParam[]; + assistantMessage: ChatCompletionMessageParam[]; + completeMessages: ChatCompletionMessageParam[]; +}; + +/* + 底层封装 LLM 调用 帮助上层屏蔽 stream 和非 stream,以及 toolChoice 和 promptTool 模式。 + 工具调用无论哪种模式,都存 toolChoice 的格式,promptTool 通过修改 toolChoice 的结构,形成特定的 messages 进行调用。 +*/ +export const createLLMResponse = async ( + args: CreateLLMResponseProps +): Promise => { + const { body, custonHeaders, userKey } = args; + const { messages, useVision, requestOrigin, tools, toolCallMode } = body; + + const modelData = getLLMModel(body.model); + + // Messages process + const requestMessages = await loadRequestMessages({ + messages, + useVision, + origin: requestOrigin + }); + // Message process + const rewriteMessages = (() => { + if (tools?.length && toolCallMode === 'prompt') { + return promptToolCallMessageRewrite(requestMessages, tools); + } + return requestMessages; + })(); + + const requestBody = await llmCompletionsBodyFormat({ + ...body, + messages: rewriteMessages + }); + + // console.log(JSON.stringify(requestBody, null, 2)); + const { response, isStreamResponse, getEmptyResponseTip } = await createChatCompletion({ + body: requestBody, + userKey, + options: { + headers: { + Accept: 'application/json, text/plain, */*', + ...custonHeaders + } + } + }); + + const { answerText, reasoningText, toolCalls, finish_reason, usage } = await (async () => { + if (isStreamResponse) { + return createStreamResponse({ + response, + body, + isAborted: args.isAborted, + onStreaming: args.onStreaming, + onReasoning: args.onReasoning, + onToolCall: args.onToolCall, + onToolParam: args.onToolParam + }); + } else { + return createCompleteResponse({ + response, + body, + onStreaming: args.onStreaming, + onReasoning: args.onReasoning, + onToolCall: args.onToolCall + }); + } + })(); + + const assistantMessage: ChatCompletionMessageParam[] = [ + ...(answerText || reasoningText + ? [ + { + role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant', + content: answerText, + reasoning_text: reasoningText + } + ] + : []), + ...(toolCalls?.length + ? [ + { + role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant', + tool_calls: toolCalls + } + ] + : []) + ]; + + // Usage count + const inputTokens = + usage?.prompt_tokens ?? (await countGptMessagesTokens(requestBody.messages, requestBody.tools)); + const outputTokens = usage?.completion_tokens ?? (await countGptMessagesTokens(assistantMessage)); + + return { + isStreamResponse, + getEmptyResponseTip, + answerText, + reasoningText, + toolCalls, + finish_reason, + usage: { + inputTokens, + outputTokens + }, + + requestMessages, + assistantMessage, + completeMessages: [...requestMessages, ...assistantMessage] + }; +}; + +type CompleteParams = Pick, 'body'> & ResponseEvents; + +type CompleteResponse = Pick< + LLMResponse, + 'answerText' | 'reasoningText' | 'toolCalls' | 'finish_reason' +> & { + usage?: CompletionUsage; +}; + +export const createStreamResponse = async ({ + body, + response, + isAborted, + onStreaming, + onReasoning, + onToolCall, + onToolParam +}: CompleteParams & { + response: StreamChatType; + isAborted?: () => boolean | undefined; +}): Promise => { + const { retainDatasetCite = true, tools, toolCallMode = 'toolChoice', model } = body; + const modelData = getLLMModel(model); + + const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse(); + + if (tools?.length) { + if (toolCallMode === 'toolChoice') { + let callingTool: ChatCompletionMessageToolCall['function'] | null = null; + const toolCalls: ChatCompletionMessageToolCall[] = []; + + for await (const part of response) { + if (isAborted?.()) { + response.controller?.abort(); + updateFinishReason('close'); + break; + } + + const { reasoningContent, responseContent } = parsePart({ + part, + parseThinkTag: modelData.reasoning, + retainDatasetCite + }); + + if (reasoningContent) { + onReasoning?.({ text: reasoningContent }); + } + if (responseContent) { + onStreaming?.({ text: responseContent }); + } + + const responseChoice = part.choices?.[0]?.delta; + + // Parse tool calls + if (responseChoice?.tool_calls?.length) { + responseChoice.tool_calls.forEach((toolCall, i) => { + const index = toolCall.index ?? i; + + // Call new tool + const hasNewTool = toolCall?.function?.name || callingTool; + if (hasNewTool) { + // Call new tool + if (toolCall?.function?.name) { + callingTool = { + name: toolCall.function?.name || '', + arguments: toolCall.function?.arguments || '' + }; + } else if (callingTool) { + // Continue call(Perhaps the name of the previous function was incomplete) + callingTool.name += toolCall.function?.name || ''; + callingTool.arguments += toolCall.function?.arguments || ''; + } + + // New tool, add to list. + if (tools.find((item) => item.function.name === callingTool!.name)) { + const call: ChatCompletionMessageToolCall = { + id: getNanoid(), + type: 'function', + function: callingTool! + }; + toolCalls.push(call); + onToolCall?.({ call }); + callingTool = null; + } + } else { + /* arg 追加到当前工具的参数里 */ + const arg: string = toolCall?.function?.arguments ?? ''; + const currentTool = toolCalls[index]; + if (currentTool && arg) { + currentTool.function.arguments += arg; + + onToolParam?.({ tool: currentTool, params: arg }); + } + } + }); + } + } + + const { reasoningContent, content, finish_reason, usage } = getResponseData(); + + return { + answerText: content, + reasoningText: reasoningContent, + finish_reason, + usage, + toolCalls + }; + } else { + let startResponseWrite = false; + let answer = ''; + + for await (const part of response) { + if (isAborted?.()) { + response.controller?.abort(); + updateFinishReason('close'); + break; + } + + const { reasoningContent, content, responseContent } = parsePart({ + part, + parseThinkTag: modelData.reasoning, + retainDatasetCite + }); + answer += content; + + if (reasoningContent) { + onReasoning?.({ text: reasoningContent }); + } + + if (content) { + if (startResponseWrite) { + if (responseContent) { + onStreaming?.({ text: responseContent }); + } + } else if (answer.length >= 3) { + answer = answer.trimStart(); + + // Not call tool + if (/0(:|:)/.test(answer)) { + startResponseWrite = true; + + // find first : index + const firstIndex = + answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0:'); + answer = answer.substring(firstIndex + 2).trim(); + + onStreaming?.({ text: answer }); + } + // Not response tool + else if (/1(:|:)/.test(answer)) { + } + // Not start 1/0, start response + else { + startResponseWrite = true; + onStreaming?.({ text: answer }); + } + } + } + } + + const { reasoningContent, content, finish_reason, usage } = getResponseData(); + const { answer: llmAnswer, toolCalls } = parsePromptToolCall(content); + + toolCalls?.forEach((call) => { + onToolCall?.({ call }); + }); + + return { + answerText: llmAnswer, + reasoningText: reasoningContent, + finish_reason, + usage, + toolCalls + }; + } + } else { + // Not use tool + for await (const part of response) { + if (isAborted?.()) { + response.controller?.abort(); + updateFinishReason('close'); + break; + } + + const { reasoningContent, responseContent } = parsePart({ + part, + parseThinkTag: modelData.reasoning, + retainDatasetCite + }); + + if (reasoningContent) { + onReasoning?.({ text: reasoningContent }); + } + if (responseContent) { + onStreaming?.({ text: responseContent }); + } + } + + const { reasoningContent, content, finish_reason, usage } = getResponseData(); + + return { + answerText: content, + reasoningText: reasoningContent, + finish_reason, + usage + }; + } +}; + +export const createCompleteResponse = async ({ + body, + response, + onStreaming, + onReasoning, + onToolCall +}: CompleteParams & { response: ChatCompletion }): Promise => { + const { tools, toolCallMode = 'toolChoice', retainDatasetCite = true } = body; + const modelData = getLLMModel(body.model); + + const finish_reason = response.choices?.[0]?.finish_reason as CompletionFinishReason; + const usage = response.usage; + + // Content and think parse + const { content, reasoningContent } = (() => { + const content = response.choices?.[0]?.message?.content || ''; + const reasoningContent: string = + (response.choices?.[0]?.message as any)?.reasoning_content || ''; + + // API already parse reasoning content + if (reasoningContent || !modelData.reasoning) { + return { + content, + reasoningContent + }; + } + + const [think, answer] = parseReasoningContent(content); + return { + content: answer, + reasoningContent: think + }; + })(); + const formatReasonContent = removeDatasetCiteText(reasoningContent, retainDatasetCite); + let formatContent = removeDatasetCiteText(content, retainDatasetCite); + + // Tool parse + const { toolCalls } = (() => { + if (tools?.length) { + if (toolCallMode === 'toolChoice') { + return { + toolCalls: response.choices?.[0]?.message?.tool_calls || [] + }; + } + + // Prompt call + const { answer, toolCalls } = parsePromptToolCall(formatContent); + formatContent = answer; + + return { + toolCalls + }; + } + + return { + toolCalls: undefined + }; + })(); + + // Event response + if (formatReasonContent) { + onReasoning?.({ text: formatReasonContent }); + } + if (formatContent) { + onStreaming?.({ text: formatContent }); + } + if (toolCalls?.length && onToolCall) { + toolCalls.forEach((call) => { + onToolCall({ call }); + }); + } + + return { + reasoningText: formatReasonContent, + answerText: formatContent, + toolCalls, + finish_reason, + usage + }; +}; + +type CompletionsBodyType = + | ChatCompletionCreateParamsNonStreaming + | ChatCompletionCreateParamsStreaming; +type InferCompletionsBody = T extends { stream: true } + ? ChatCompletionCreateParamsStreaming + : T extends { stream: false } + ? ChatCompletionCreateParamsNonStreaming + : ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; + +type LLMRequestBodyType = Omit & { + model: string | LLMModelItemType; + stop?: string; + response_format?: { + type?: string; + json_schema?: string; + }; + messages: ChatCompletionMessageParam[]; + + // Custom field + retainDatasetCite?: boolean; + reasoning?: boolean; // Whether to response reasoning content + toolCallMode?: 'toolChoice' | 'prompt'; + useVision?: boolean; + requestOrigin?: string; +}; +const llmCompletionsBodyFormat = async ({ + reasoning, + retainDatasetCite, + useVision, + requestOrigin, + + tools, + tool_choice, + parallel_tool_calls, + toolCallMode, + ...body +}: LLMRequestBodyType): Promise> => { + const modelData = getLLMModel(body.model); + if (!modelData) { + return body as unknown as InferCompletionsBody; + } + + const response_format = (() => { + if (!body.response_format?.type) return undefined; + if (body.response_format.type === 'json_schema') { + try { + return { + type: 'json_schema', + json_schema: json5.parse(body.response_format?.json_schema as unknown as string) + }; + } catch (error) { + throw new Error('Json schema error'); + } + } + if (body.response_format.type) { + return { + type: body.response_format.type + }; + } + return undefined; + })(); + const stop = body.stop ?? undefined; + + const requestBody = { + ...body, + model: modelData.model, + temperature: + typeof body.temperature === 'number' + ? computedTemperature({ + model: modelData, + temperature: body.temperature + }) + : undefined, + ...modelData?.defaultConfig, + response_format, + stop: stop?.split('|'), + ...(toolCallMode === 'toolChoice' && { + tools, + tool_choice, + parallel_tool_calls + }) + } as T; + + // field map + if (modelData.fieldMap) { + Object.entries(modelData.fieldMap).forEach(([sourceKey, targetKey]) => { + // @ts-ignore + requestBody[targetKey] = body[sourceKey]; + // @ts-ignore + delete requestBody[sourceKey]; + }); + } + + return requestBody as unknown as InferCompletionsBody; +}; +const createChatCompletion = async ({ + modelData, + body, + userKey, + timeout, + options +}: { + modelData?: LLMModelItemType; + body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; + userKey?: OpenaiAccountType; + timeout?: number; + options?: OpenAI.RequestOptions; +}): Promise< + { + getEmptyResponseTip: () => string; + } & ( + | { + response: StreamChatType; + isStreamResponse: true; + } + | { + response: UnStreamChatType; + isStreamResponse: false; + } + ) +> => { + try { + // Rewrite model + const modelConstantsData = modelData || getLLMModel(body.model); + if (!modelConstantsData) { + return Promise.reject(`${body.model} not found`); + } + body.model = modelConstantsData.model; + + const formatTimeout = timeout ? timeout : 600000; + const ai = getAIApi({ + userKey, + timeout: formatTimeout + }); + + addLog.debug(`Start create chat completion`, { + model: body.model + }); + + const response = await ai.chat.completions.create(body, { + ...options, + ...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}), + headers: { + ...options?.headers, + ...(modelConstantsData.requestAuth + ? { Authorization: `Bearer ${modelConstantsData.requestAuth}` } + : {}) + } + }); + + const isStreamResponse = + typeof response === 'object' && + response !== null && + ('iterator' in response || 'controller' in response); + + const getEmptyResponseTip = () => { + addLog.warn(`LLM response empty`, { + baseUrl: userKey?.baseUrl, + requestBody: body + }); + if (userKey?.baseUrl) { + return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`; + } + return i18nT('chat:LLM_model_response_empty'); + }; + + if (isStreamResponse) { + return { + response, + isStreamResponse: true, + getEmptyResponseTip + }; + } + + return { + response, + isStreamResponse: false, + getEmptyResponseTip + }; + } catch (error) { + addLog.error(`LLM response error`, error); + addLog.warn(`LLM response error`, { + baseUrl: userKey?.baseUrl, + requestBody: body + }); + if (userKey?.baseUrl) { + return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`); + } + return Promise.reject(error); + } +}; diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/ai/llm/utils.ts similarity index 98% rename from packages/service/core/chat/utils.ts rename to packages/service/core/ai/llm/utils.ts index 6d865be77c76..1e8be1846813 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/ai/llm/utils.ts @@ -1,4 +1,4 @@ -import { countGptMessagesTokens } from '../../common/string/tiktoken/index'; +import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; import type { ChatCompletionAssistantMessageParam, ChatCompletionContentPart, @@ -9,9 +9,9 @@ import type { } from '@fastgpt/global/core/ai/type.d'; import axios from 'axios'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; -import { i18nT } from '../../../web/i18n/utils'; -import { addLog } from '../../common/system/log'; -import { getImageBase64 } from '../../common/file/image/utils'; +import { i18nT } from '../../../../web/i18n/utils'; +import { addLog } from '../../../common/system/log'; +import { getImageBase64 } from '../../../common/file/image/utils'; export const filterGPTMessageByMaxContext = async ({ messages = [], @@ -106,7 +106,7 @@ export const loadRequestMessages = async ({ const arrayContent = content .filter((item) => item.text) .map((item) => item.text) - .join('\n'); + .join('\n\n'); return arrayContent; }; diff --git a/packages/service/core/ai/model.ts b/packages/service/core/ai/model.ts index 7821b9e73e36..5f3ced278b11 100644 --- a/packages/service/core/ai/model.ts +++ b/packages/service/core/ai/model.ts @@ -1,10 +1,12 @@ import { cloneDeep } from 'lodash'; import { type SystemModelItemType } from './type'; +import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; export const getDefaultLLMModel = () => global?.systemDefaultModel.llm!; -export const getLLMModel = (model?: string) => { +export const getLLMModel = (model?: string | LLMModelItemType) => { if (!model) return getDefaultLLMModel(); - return global.llmModelMap.get(model) || getDefaultLLMModel(); + + return typeof model === 'string' ? global.llmModelMap.get(model) || getDefaultLLMModel() : model; }; export const getDatasetModel = (model?: string) => { diff --git a/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts index e9a0e7f303d0..a8a84a8670cf 100644 --- a/packages/service/core/ai/utils.ts +++ b/packages/service/core/ai/utils.ts @@ -1,17 +1,7 @@ import { type LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; -import type { - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - CompletionFinishReason, - StreamChatType, - UnStreamChatType, - CompletionUsage, - ChatCompletionMessageToolCall -} from '@fastgpt/global/core/ai/type'; -import { getLLMModel } from './model'; +import type { CompletionFinishReason, CompletionUsage } from '@fastgpt/global/core/ai/type'; import { getLLMDefaultUsage } from '@fastgpt/global/core/ai/constants'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; -import json5 from 'json5'; +import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils'; /* Count response max token @@ -46,168 +36,7 @@ export const computedTemperature = ({ return temperature; }; -type CompletionsBodyType = - | ChatCompletionCreateParamsNonStreaming - | ChatCompletionCreateParamsStreaming; -type InferCompletionsBody = T extends { stream: true } - ? ChatCompletionCreateParamsStreaming - : T extends { stream: false } - ? ChatCompletionCreateParamsNonStreaming - : ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; - -export const llmCompletionsBodyFormat = ( - body: T & { - stop?: string; - }, - model: string | LLMModelItemType -): InferCompletionsBody => { - const modelData = typeof model === 'string' ? getLLMModel(model) : model; - if (!modelData) { - return body as unknown as InferCompletionsBody; - } - - const response_format = (() => { - if (!body.response_format?.type) return undefined; - if (body.response_format.type === 'json_schema') { - try { - return { - type: 'json_schema', - json_schema: json5.parse(body.response_format?.json_schema as unknown as string) - }; - } catch (error) { - throw new Error('Json schema error'); - } - } - if (body.response_format.type) { - return { - type: body.response_format.type - }; - } - return undefined; - })(); - - const stop = body.stop ?? undefined; - - const requestBody: T = { - ...body, - model: modelData.model, - temperature: - typeof body.temperature === 'number' - ? computedTemperature({ - model: modelData, - temperature: body.temperature - }) - : undefined, - ...modelData?.defaultConfig, - response_format, - stop: stop?.split('|') - }; - - // field map - if (modelData.fieldMap) { - Object.entries(modelData.fieldMap).forEach(([sourceKey, targetKey]) => { - // @ts-ignore - requestBody[targetKey] = body[sourceKey]; - // @ts-ignore - delete requestBody[sourceKey]; - }); - } - - return requestBody as unknown as InferCompletionsBody; -}; - -export const llmStreamResponseToAnswerText = async ( - response: StreamChatType -): Promise<{ - text: string; - usage?: CompletionUsage; - toolCalls?: ChatCompletionMessageToolCall[]; -}> => { - let answer = ''; - let usage = getLLMDefaultUsage(); - let toolCalls: ChatCompletionMessageToolCall[] = []; - let callingTool: { name: string; arguments: string } | null = null; - - for await (const part of response) { - usage = part.usage || usage; - const responseChoice = part.choices?.[0]?.delta; - - const content = responseChoice?.content || ''; - answer += content; - - // Tool calls - if (responseChoice?.tool_calls?.length) { - responseChoice.tool_calls.forEach((toolCall, i) => { - const index = toolCall.index ?? i; - - // Call new tool - const hasNewTool = toolCall?.function?.name || callingTool; - if (hasNewTool) { - // 有 function name,代表新 call 工具 - if (toolCall?.function?.name) { - callingTool = { - name: toolCall.function?.name || '', - arguments: toolCall.function?.arguments || '' - }; - } else if (callingTool) { - // Continue call(Perhaps the name of the previous function was incomplete) - callingTool.name += toolCall.function?.name || ''; - callingTool.arguments += toolCall.function?.arguments || ''; - } - - if (!callingTool) { - return; - } - - // New tool, add to list. - const toolId = getNanoid(); - toolCalls[index] = { - ...toolCall, - id: toolId, - type: 'function', - function: callingTool - }; - callingTool = null; - } else { - /* arg 追加到当前工具的参数里 */ - const arg: string = toolCall?.function?.arguments ?? ''; - const currentTool = toolCalls[index]; - if (currentTool && arg) { - currentTool.function.arguments += arg; - } - } - }); - } - } - return { - text: removeDatasetCiteText(parseReasoningContent(answer)[1], false), - usage, - toolCalls - }; -}; -export const llmUnStreamResponseToAnswerText = async ( - response: UnStreamChatType -): Promise<{ - text: string; - toolCalls?: ChatCompletionMessageToolCall[]; - usage?: CompletionUsage; -}> => { - const answer = response.choices?.[0]?.message?.content || ''; - const toolCalls = response.choices?.[0]?.message?.tool_calls; - - return { - text: removeDatasetCiteText(parseReasoningContent(answer)[1], false), - usage: response.usage, - toolCalls - }; -}; -export const formatLLMResponse = async (response: StreamChatType | UnStreamChatType) => { - if ('iterator' in response) { - return llmStreamResponseToAnswerText(response); - } - return llmUnStreamResponseToAnswerText(response); -}; - +// LLM utils // Parse tags to think and answer - unstream response export const parseReasoningContent = (text: string): [string, string] => { const regex = /([\s\S]*?)<\/think>/; @@ -225,14 +54,6 @@ export const parseReasoningContent = (text: string): [string, string] => { return [thinkContent, answerContent]; }; -export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean) => { - return retainDatasetCite - ? text.replace(/[\[【]id[\]】]\(CITE\)/g, '') - : text - .replace(/[\[【]([a-f0-9]{24})[\]】](?:\([^\)]*\)?)?/g, '') - .replace(/[\[【]id[\]】]\(CITE\)/g, ''); -}; - // Parse llm stream part export const parseLLMStreamResponse = () => { let isInThinkTag: boolean | undefined = undefined; @@ -274,8 +95,8 @@ export const parseLLMStreamResponse = () => { retainDatasetCite?: boolean; }): { reasoningContent: string; - content: string; - responseContent: string; + content: string; // 原始内容,不去掉 cite + responseContent: string; // 响应的内容,会去掉 cite finishReason: CompletionFinishReason; } => { const data = (() => { diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index 55aab10f7f5d..303b20e28d94 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -16,6 +16,8 @@ import { MongoOutLink } from '../../support/outLink/schema'; import { MongoOpenApi } from '../../support/openapi/schema'; import { MongoAppVersion } from './version/schema'; import { MongoChatInputGuide } from '../chat/inputGuide/schema'; +import { MongoChatFavouriteApp } from '../chat/favouriteApp/schema'; +import { MongoChatSetting } from '../chat/setting/schema'; import { MongoResourcePermission } from '../../support/permission/schema'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { removeImageByPath } from '../../common/file/image/controller'; @@ -191,6 +193,18 @@ export const onDelOneApp = async ({ appId }).session(session); + // 删除精选应用记录 + await MongoChatFavouriteApp.deleteMany({ + teamId, + appId + }).session(session); + + // 从快捷应用中移除对应应用 + await MongoChatSetting.updateMany( + { teamId }, + { $pull: { quickAppIds: { id: String(appId) } } } + ).session(session); + await MongoResourcePermission.deleteMany({ resourceType: PerResourceTypeEnum.app, teamId, diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index 621ac4e34aa2..7f2fe4d4c6d7 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -42,7 +42,7 @@ import type { import { isProduction } from '@fastgpt/global/common/system/constants'; import { Output_Template_Error_Message } from '@fastgpt/global/core/workflow/template/output'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; -import { getMCPParentId, getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils'; +import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { getMCPChildren } from '../mcp'; import { cloneDeep } from 'lodash'; diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index d6e6f0cb742a..2e805ff93aea 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -116,6 +116,9 @@ const AppSchema = new Schema( default: true }, + favourite: Boolean, + quick: Boolean, + // abandoned defaultPermission: Number }, diff --git a/packages/service/core/app/tool/api.ts b/packages/service/core/app/tool/api.ts index 4e8592e7fac2..31c69b182d9c 100644 --- a/packages/service/core/app/tool/api.ts +++ b/packages/service/core/app/tool/api.ts @@ -1,4 +1,4 @@ -import { RunToolWithStream } from '@fastgpt-sdk/plugin'; +import { RunToolWithStream } from '@fastgpt/global/sdk/fastgpt-plugin'; import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; import { pluginClient, BASE_URL, TOKEN } from '../../../thirdProvider/fastgptPlugin'; @@ -13,7 +13,7 @@ export async function APIGetSystemToolList() { parentId: item.parentId ? `${PluginSourceEnum.systemTool}-${item.parentId}` : undefined, avatar: item.avatar && item.avatar.startsWith('/imgs/tools/') - ? `/api/system/pluginImgs/${item.avatar.replace('/imgs/tools/', '')}` + ? `/api/system/plugin/tools/${item.avatar.replace('/imgs/tools/', '')}` : item.avatar }; }); diff --git a/packages/service/core/chat/favouriteApp/schema.ts b/packages/service/core/chat/favouriteApp/schema.ts new file mode 100644 index 000000000000..8b6f0aab7d56 --- /dev/null +++ b/packages/service/core/chat/favouriteApp/schema.ts @@ -0,0 +1,36 @@ +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { type ChatFavouriteAppSchema as ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type'; +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { AppCollectionName } from '../../app/schema'; + +const { Schema } = connectionMongo; + +export const ChatFavouriteAppCollectionName = 'chat_favourite_apps'; + +const ChatFavouriteAppSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + appId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + required: true + }, + favouriteTags: { + type: [String], + default: [] + }, + order: { + type: Number, + default: 10000000 + } +}); + +ChatFavouriteAppSchema.index({ teamId: 1, appId: 1 }); + +export const MongoChatFavouriteApp = getMongoModel( + ChatFavouriteAppCollectionName, + ChatFavouriteAppSchema +); diff --git a/packages/service/core/chat/setting/schema.ts b/packages/service/core/chat/setting/schema.ts index af926a033115..e421137fe113 100644 --- a/packages/service/core/chat/setting/schema.ts +++ b/packages/service/core/chat/setting/schema.ts @@ -26,7 +26,27 @@ const ChatSettingSchema = new Schema({ }, homeTabTitle: String, wideLogoUrl: String, - squareLogoUrl: String + squareLogoUrl: String, + quickAppIds: { + type: [String], + default: [] + }, + favouriteTags: { + type: [ + { + id: String, + name: String + } + ], + default: [], + _id: false + } +}); + +ChatSettingSchema.virtual('quickAppList', { + ref: AppCollectionName, + localField: 'quickAppIds', + foreignField: '_id' }); ChatSettingSchema.index({ teamId: 1 }); diff --git a/packages/service/core/workflow/dispatch/ai/agent/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/constants.ts index 03b4aade77e1..c4a178a42c2f 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/constants.ts @@ -1,52 +1,5 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; -export const Prompt_Tool_Call = ` -你是一个智能机器人,除了可以回答用户问题外,你还掌握工具的使用能力。有时候,你可以依赖工具的运行结果,来更准确的回答用户。 - -工具使用了 JSON Schema 的格式声明,其中 toolId 是工具的唯一标识, description 是工具的描述,parameters 是工具的参数及参数表述,required 是必填参数的列表。 - -请你根据工具描述,决定回答问题或是使用工具。在完成任务过程中,USER代表用户的输入,TOOL_RESPONSE代表工具运行结果,ANSWER 代表你的输出。 -你的每次输出都必须以0,1开头,代表是否需要调用工具: -0: 不使用工具,直接回答内容。 -1: 使用工具,返回工具调用的参数。 - -例如: - -USER: 你好呀 -ANSWER: 0: 你好,有什么可以帮助你的么? -USER: 现在几点了? -ANSWER: 1: {"toolId":"searchToolId1"} -TOOL_RESPONSE: """ -2022/5/5 12:00 Thursday -""" -ANSWER: 0: 现在是2022年5月5日,星期四,中午12点。 -USER: 今天杭州的天气如何? -ANSWER: 1: {"toolId":"searchToolId2","arguments":{"city": "杭州"}} -TOOL_RESPONSE: """ -晴天...... -""" -ANSWER: 0: 今天杭州是晴天。 -USER: 今天杭州的天气适合去哪里玩? -ANSWER: 1: {"toolId":"searchToolId3","arguments":{"query": "杭州 天气 去哪里玩"}} -TOOL_RESPONSE: """ -晴天. 西湖、灵隐寺、千岛湖…… -""" -ANSWER: 0: 今天杭州是晴天,适合去西湖、灵隐寺、千岛湖等地玩。 - - ------- - -现在,我们开始吧!下面是你本次可以使用的工具: - -""" -{{toolsPrompt}} -""" - -下面是正式的对话内容: - -USER: {{question}} -ANSWER: `; - export const getMultiplePrompt = (obj: { fileCount: number; imgCount: number; diff --git a/packages/service/core/workflow/dispatch/ai/agent/functionCall.ts b/packages/service/core/workflow/dispatch/ai/agent/functionCall.ts deleted file mode 100644 index 9ac2479e7014..000000000000 --- a/packages/service/core/workflow/dispatch/ai/agent/functionCall.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; -import type { - ChatCompletion, - StreamChatType, - ChatCompletionMessageParam, - ChatCompletionCreateParams, - ChatCompletionMessageFunctionCall, - ChatCompletionFunctionMessageParam, - ChatCompletionAssistantMessageParam, - CompletionFinishReason -} from '@fastgpt/global/core/ai/type.d'; -import { type NextApiResponse } from 'next'; -import { responseWriteController } from '../../../../../common/response'; -import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; -import { - ChatCompletionRequestMessageRoleEnum, - getLLMDefaultUsage -} from '@fastgpt/global/core/ai/constants'; -import { dispatchWorkFlow } from '../../index'; -import { type DispatchToolModuleProps, type RunToolResponse, type ToolNodeItemType } from './type'; -import json5 from 'json5'; -import { type DispatchFlowResponse, type WorkflowResponseType } from '../../type'; -import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; -import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; -import { type AIChatItemType } from '@fastgpt/global/core/chat/type'; -import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; -import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; -import { - computedMaxToken, - llmCompletionsBodyFormat, - removeDatasetCiteText, - parseLLMStreamResponse -} from '../../../../ai/utils'; -import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants'; -import { type WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; -import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; - -type FunctionRunResponseType = { - toolRunResponse: DispatchFlowResponse; - functionCallMsg: ChatCompletionFunctionMessageParam; -}[]; - -export const runToolWithFunctionCall = async ( - props: DispatchToolModuleProps, - response?: RunToolResponse -): Promise => { - const { messages, toolNodes, toolModel, interactiveEntryToolParams, ...workflowProps } = props; - const { - res, - requestOrigin, - runtimeNodes, - runtimeEdges, - externalProvider, - stream, - retainDatasetCite = true, - workflowStreamResponse, - params: { - temperature, - maxToken, - aiChatVision, - aiChatTopP, - aiChatStopSign, - aiChatResponseFormat, - aiChatJsonSchema - } - } = workflowProps; - - // Interactive - if (interactiveEntryToolParams) { - initToolNodes(runtimeNodes, interactiveEntryToolParams.entryNodeIds); - initToolCallEdges(runtimeEdges, interactiveEntryToolParams.entryNodeIds); - - // Run entry tool - const toolRunResponse = await dispatchWorkFlow({ - ...workflowProps, - isToolCall: true - }); - const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - id: interactiveEntryToolParams.toolCallId, - toolName: '', - toolAvatar: '', - params: '', - response: sliceStrStartEnd(stringToolResponse, 5000, 5000) - } - } - }); - - // Check stop signal - const hasStopSignal = toolRunResponse.flowResponses?.some((item) => item.toolStop); - // Check interactive response(Only 1 interaction is reserved) - const workflowInteractiveResponse = toolRunResponse.workflowInteractiveResponse; - - const requestMessages = [ - ...messages, - ...interactiveEntryToolParams.memoryMessages.map((item) => - !workflowInteractiveResponse && - item.role === 'function' && - item.name === interactiveEntryToolParams.toolCallId - ? { - ...item, - content: stringToolResponse - } - : item - ) - ]; - - if (hasStopSignal || workflowInteractiveResponse) { - // Get interactive tool data - const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = - workflowInteractiveResponse - ? { - ...workflowInteractiveResponse, - toolParams: { - entryNodeIds: workflowInteractiveResponse.entryNodeIds, - toolCallId: interactiveEntryToolParams.toolCallId, - memoryMessages: [...interactiveEntryToolParams.memoryMessages] - } - } - : undefined; - - return { - dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, - completeMessages: requestMessages, - assistantResponses: toolRunResponse.assistantResponses, - runTimes: toolRunResponse.runTimes, - toolWorkflowInteractiveResponse - }; - } - - return runToolWithFunctionCall( - { - ...props, - interactiveEntryToolParams: undefined, - // Rewrite toolCall messages - messages: requestMessages - }, - { - dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, - assistantResponses: toolRunResponse.assistantResponses, - runTimes: toolRunResponse.runTimes - } - ); - } - - // ------------------------------------------------------------ - - const assistantResponses = response?.assistantResponses || []; - - const functions: ChatCompletionCreateParams.Function[] = toolNodes.map((item) => { - if (item.jsonSchema) { - return { - name: item.nodeId, - description: item.intro, - parameters: item.jsonSchema - }; - } - - const properties: Record< - string, - { - type: string; - description: string; - required?: boolean; - enum?: string[]; - } - > = {}; - item.toolParams.forEach((item) => { - const jsonSchema = item.valueType - ? valueTypeJsonSchemaMap[item.valueType] || toolValueTypeList[0].jsonSchema - : toolValueTypeList[0].jsonSchema; - - properties[item.key] = { - ...jsonSchema, - description: item.toolDescription || '', - enum: item.enum?.split('\n').filter(Boolean) || [] - }; - }); - - return { - name: item.nodeId, - description: item.toolDescription || item.intro, - parameters: { - type: 'object', - properties, - required: item.toolParams.filter((item) => item.required).map((item) => item.key) - } - }; - }); - - const max_tokens = computedMaxToken({ - model: toolModel, - maxToken - }); - const filterMessages = ( - await filterGPTMessageByMaxContext({ - messages, - maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken - }) - ).map((item) => { - if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant && item.function_call) { - return { - ...item, - function_call: { - name: item.function_call?.name, - arguments: item.function_call?.arguments - }, - content: '' - }; - } - return item; - }); - const [requestMessages] = await Promise.all([ - loadRequestMessages({ - messages: filterMessages, - useVision: toolModel.vision && aiChatVision, - origin: requestOrigin - }) - ]); - const requestBody = llmCompletionsBodyFormat( - { - model: toolModel.model, - - stream, - messages: requestMessages, - functions, - function_call: 'auto', - - temperature, - max_tokens, - top_p: aiChatTopP, - stop: aiChatStopSign, - response_format: { - type: aiChatResponseFormat as any, - json_schema: aiChatJsonSchema - } - }, - toolModel - ); - - // console.log(JSON.stringify(requestMessages, null, 2)); - /* Run llm */ - const { - response: aiResponse, - isStreamResponse, - getEmptyResponseTip - } = await createChatCompletion({ - body: requestBody, - userKey: externalProvider.openaiAccount, - options: { - headers: { - Accept: 'application/json, text/plain, */*' - } - } - }); - - let { answer, functionCalls, inputTokens, outputTokens, finish_reason } = await (async () => { - if (isStreamResponse) { - if (!res || res.closed) { - return { - answer: '', - functionCalls: [], - inputTokens: 0, - outputTokens: 0, - finish_reason: 'close' as const - }; - } - const result = await streamResponse({ - res, - toolNodes, - stream: aiResponse, - workflowStreamResponse, - retainDatasetCite - }); - - return { - answer: result.answer, - functionCalls: result.functionCalls, - inputTokens: result.usage.prompt_tokens, - outputTokens: result.usage.completion_tokens, - finish_reason: result.finish_reason - }; - } else { - const result = aiResponse as ChatCompletion; - const finish_reason = result.choices?.[0]?.finish_reason as CompletionFinishReason; - const function_call = result.choices?.[0]?.message?.function_call; - const usage = result.usage; - - const toolNode = toolNodes.find((node) => node.nodeId === function_call?.name); - - const toolCalls = function_call - ? [ - { - ...function_call, - id: getNanoid(), - toolName: toolNode?.name, - toolAvatar: toolNode?.avatar - } - ] - : []; - - const answer = result.choices?.[0]?.message?.content || ''; - if (answer) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: removeDatasetCiteText(answer, retainDatasetCite) - }) - }); - } - - return { - answer, - functionCalls: toolCalls, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens, - finish_reason - }; - } - })(); - if (!answer && functionCalls.length === 0) { - return Promise.reject(getEmptyResponseTip()); - } - - // Run the selected tool. - const toolsRunResponse = ( - await Promise.all( - functionCalls.map(async (tool) => { - if (!tool) return; - - const toolNode = toolNodes.find((node) => node.nodeId === tool.name); - - if (!toolNode) return; - - const startParams = (() => { - try { - return json5.parse(tool.arguments); - } catch (error) { - return {}; - } - })(); - - initToolNodes(runtimeNodes, [toolNode.nodeId], startParams); - const toolRunResponse = await dispatchWorkFlow({ - ...workflowProps, - isToolCall: true - }); - - const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); - - const functionCallMsg: ChatCompletionFunctionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Function, - name: tool.name, - content: stringToolResponse - }; - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - id: tool.id, - toolName: '', - toolAvatar: '', - params: '', - response: sliceStrStartEnd(stringToolResponse, 500, 500) - } - } - }); - - return { - toolRunResponse, - functionCallMsg - }; - }) - ) - ).filter(Boolean) as FunctionRunResponseType; - - const flatToolsResponseData = toolsRunResponse.map((item) => item.toolRunResponse).flat(); - // concat tool responses - const dispatchFlowResponse = response - ? response.dispatchFlowResponse.concat(flatToolsResponseData) - : flatToolsResponseData; - - const functionCall = functionCalls[0]; - if (functionCall) { - // Run the tool, combine its results, and perform another round of AI calls - const assistantToolMsgParams: ChatCompletionAssistantMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - function_call: functionCall - }; - - /* - ... - user - assistant: tool data - */ - const concatToolMessages = [ - ...requestMessages, - assistantToolMsgParams - ] as ChatCompletionMessageParam[]; - // Only toolCall tokens are counted here, Tool response tokens count towards the next reply - // const tokens = await countGptMessagesTokens(concatToolMessages, undefined, functions); - inputTokens = - inputTokens || (await countGptMessagesTokens(requestMessages, undefined, functions)); - outputTokens = outputTokens || (await countGptMessagesTokens([assistantToolMsgParams])); - /* - ... - user - assistant: tool data - tool: tool response - */ - const completeMessages = [ - ...concatToolMessages, - ...toolsRunResponse.map((item) => item?.functionCallMsg) - ]; - - /* - Get tool node assistant response - history assistant - current tool assistant - tool child assistant - */ - const toolNodeAssistant = GPTMessages2Chats([ - assistantToolMsgParams, - ...toolsRunResponse.map((item) => item?.functionCallMsg) - ])[0] as AIChatItemType; - const toolChildAssistants = flatToolsResponseData - .map((item) => item.assistantResponses) - .flat() - .filter((item) => item.type !== ChatItemValueTypeEnum.interactive); - const toolNodeAssistants = [ - ...assistantResponses, - ...toolNodeAssistant.value, - ...toolChildAssistants - ]; - - const runTimes = - (response?.runTimes || 0) + - flatToolsResponseData.reduce((sum, item) => sum + item.runTimes, 0); - const toolNodeInputTokens = response?.toolNodeInputTokens - ? response.toolNodeInputTokens + inputTokens - : inputTokens; - const toolNodeOutputTokens = response?.toolNodeOutputTokens - ? response.toolNodeOutputTokens + outputTokens - : outputTokens; - - // Check stop signal - const hasStopSignal = flatToolsResponseData.some( - (item) => !!item.flowResponses?.find((item) => item.toolStop) - ); - // Check interactive response(Only 1 interaction is reserved) - const workflowInteractiveResponseItem = toolsRunResponse.find( - (item) => item.toolRunResponse.workflowInteractiveResponse - ); - if (hasStopSignal || workflowInteractiveResponseItem) { - // Get interactive tool data - const workflowInteractiveResponse = - workflowInteractiveResponseItem?.toolRunResponse.workflowInteractiveResponse; - - // Flashback traverses completeMessages, intercepting messages that know the first user - const firstUserIndex = completeMessages.findLastIndex((item) => item.role === 'user'); - const newMessages = completeMessages.slice(firstUserIndex + 1); - - const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = - workflowInteractiveResponse - ? { - ...workflowInteractiveResponse, - toolParams: { - entryNodeIds: workflowInteractiveResponse.entryNodeIds, - toolCallId: workflowInteractiveResponseItem?.functionCallMsg.name, - memoryMessages: newMessages - } - } - : undefined; - - return { - dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - completeMessages, - assistantResponses: toolNodeAssistants, - runTimes, - toolWorkflowInteractiveResponse, - finish_reason - }; - } - - return runToolWithFunctionCall( - { - ...props, - messages: completeMessages - }, - { - dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - assistantResponses: toolNodeAssistants, - runTimes, - finish_reason - } - ); - } else { - // No tool is invoked, indicating that the process is over - const gptAssistantResponse: ChatCompletionAssistantMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: answer - }; - const completeMessages = filterMessages.concat(gptAssistantResponse); - inputTokens = - inputTokens || (await countGptMessagesTokens(requestMessages, undefined, functions)); - outputTokens = outputTokens || (await countGptMessagesTokens([gptAssistantResponse])); - // console.log(tokens, 'response token'); - - // concat tool assistant - const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType; - - return { - dispatchFlowResponse: response?.dispatchFlowResponse || [], - toolNodeInputTokens: response?.toolNodeInputTokens - ? response.toolNodeInputTokens + inputTokens - : inputTokens, - toolNodeOutputTokens: response?.toolNodeOutputTokens - ? response.toolNodeOutputTokens + outputTokens - : outputTokens, - completeMessages, - assistantResponses: [...assistantResponses, ...toolNodeAssistant.value], - runTimes: (response?.runTimes || 0) + 1, - finish_reason - }; - } -}; - -async function streamResponse({ - res, - toolNodes, - stream, - workflowStreamResponse, - retainDatasetCite -}: { - res: NextApiResponse; - toolNodes: ToolNodeItemType[]; - stream: StreamChatType; - workflowStreamResponse?: WorkflowResponseType; - retainDatasetCite?: boolean; -}) { - const write = responseWriteController({ - res, - readStream: stream - }); - - let functionCalls: ChatCompletionMessageFunctionCall[] = []; - let functionId = getNanoid(); - - const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse(); - - for await (const part of stream) { - if (res.closed) { - stream.controller?.abort(); - updateFinishReason('close'); - break; - } - - const { responseContent } = parsePart({ - part, - parseThinkTag: false, - retainDatasetCite - }); - - const responseChoice = part.choices?.[0]?.delta; - - if (responseContent) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: responseContent - }) - }); - } else if (responseChoice?.function_call) { - const functionCall: { - arguments?: string; - name?: string; - } = responseChoice.function_call; - - // 流响应中,每次只会返回一个函数,如果带了name,说明触发某个函数 - if (functionCall?.name) { - functionId = getNanoid(); - const toolNode = toolNodes.find((item) => item.nodeId === functionCall?.name); - - if (toolNode) { - functionCalls.push({ - ...functionCall, - arguments: functionCall.arguments || '', - id: functionId, - name: functionCall.name, - toolName: toolNode.name, - toolAvatar: toolNode.avatar - }); - - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: functionId, - toolName: toolNode.name, - toolAvatar: toolNode.avatar, - functionName: functionCall.name, - params: functionCall.arguments || '', - response: '' - } - } - }); - } - - continue; - } - - /* arg 插入最后一个工具的参数里 */ - const arg: string = functionCall?.arguments || ''; - const currentTool = functionCalls[functionCalls.length - 1]; - if (currentTool) { - currentTool.arguments += arg; - - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.toolParams, - data: { - tool: { - id: functionId, - toolName: '', - toolAvatar: '', - params: arg, - response: '' - } - } - }); - } - } - } - - const { content, finish_reason, usage } = getResponseData(); - - return { answer: content, functionCalls, finish_reason, usage }; -} diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 867f2d2756a7..b9dab2c68129 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -7,7 +7,7 @@ import type { } from '@fastgpt/global/core/workflow/runtime/type'; import { getLLMModel } from '../../../../ai/model'; import { filterToolNodeIdByEdges, getNodeErrResponse, getHistories } from '../../utils'; -import { runToolWithToolChoice } from './toolChoice'; +import { runToolCall } from './toolCall'; import { type DispatchToolModuleProps, type ToolNodeItemType } from './type'; import { type ChatItemType, type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -20,15 +20,12 @@ import { } from '@fastgpt/global/core/chat/adapt'; import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils'; import { getHistoryPreview } from '@fastgpt/global/core/chat/utils'; -import { runToolWithFunctionCall } from './functionCall'; -import { runToolWithPromptCall } from './promptCall'; import { replaceVariable } from '@fastgpt/global/common/string/tools'; -import { getMultiplePrompt, Prompt_Tool_Call } from './constants'; +import { getMultiplePrompt } from './constants'; import { filterToolResponseToPreview } from './utils'; import { getFileContentFromLinks, getHistoryFileLinks } from '../../tools/readFiles'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { getDocumentQuotePrompt } from '@fastgpt/global/core/ai/prompt/AIChat'; import { postTextCensor } from '../../../../chat/postTextCensor'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; @@ -180,8 +177,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< const { toolWorkflowInteractiveResponse, dispatchFlowResponse, // tool flow response - toolNodeInputTokens, - toolNodeOutputTokens, + toolCallInputTokens, + toolCallOutputTokens, completeMessages = [], // The actual message sent to AI(just save text) assistantResponses = [], // FastGPT system store assistant.value response runTimes, @@ -201,64 +198,25 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< interactiveEntryToolParams: lastInteractive?.toolParams }; - if (toolModel.toolChoice) { - return runToolWithToolChoice({ - ...props, - ...requestParams, - maxRunToolTimes: 30 - }); - } - if (toolModel.functionCall) { - return runToolWithFunctionCall({ - ...props, - ...requestParams - }); - } - - const lastMessage = adaptMessages[adaptMessages.length - 1]; - if (typeof lastMessage?.content === 'string') { - lastMessage.content = replaceVariable(Prompt_Tool_Call, { - question: lastMessage.content - }); - } else if (Array.isArray(lastMessage.content)) { - // array, replace last element - const lastText = lastMessage.content[lastMessage.content.length - 1]; - if (lastText.type === 'text') { - lastText.text = replaceVariable(Prompt_Tool_Call, { - question: lastText.text - }); - } else { - return Promise.reject('Prompt call invalid input'); - } - } else { - return Promise.reject('Prompt call invalid input'); - } - - return runToolWithPromptCall({ + return runToolCall({ ...props, - ...requestParams + ...requestParams, + maxRunToolTimes: 30 }); })(); - const { totalPoints, modelName } = formatModelChars2Points({ + const { totalPoints: modelTotalPoints, modelName } = formatModelChars2Points({ model, - inputTokens: toolNodeInputTokens, - outputTokens: toolNodeOutputTokens, - modelType: ModelTypeEnum.llm + inputTokens: toolCallInputTokens, + outputTokens: toolCallOutputTokens }); - const toolAIUsage = externalProvider.openaiAccount?.key ? 0 : totalPoints; + const modelUsage = externalProvider.openaiAccount?.key ? 0 : modelTotalPoints; - // flat child tool response - const childToolResponse = dispatchFlowResponse.map((item) => item.flowResponses).flat(); + const toolUsages = dispatchFlowResponse.map((item) => item.flowUsages).flat(); + const toolTotalPoints = toolUsages.reduce((sum, item) => sum + item.totalPoints, 0); // concat tool usage - const totalPointsUsage = - toolAIUsage + - dispatchFlowResponse.reduce((sum, item) => { - const childrenTotal = item.flowUsages.reduce((sum, item) => sum + item.totalPoints, 0); - return sum + childrenTotal; - }, 0); - const flatUsages = dispatchFlowResponse.map((item) => item.flowUsages).flat(); + const totalPointsUsage = modelUsage + toolTotalPoints; const previewAssistantResponses = filterToolResponseToPreview(assistantResponses); @@ -274,31 +232,31 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< [DispatchNodeResponseKeyEnum.nodeResponse]: { // 展示的积分消耗 totalPoints: totalPointsUsage, - toolCallInputTokens: toolNodeInputTokens, - toolCallOutputTokens: toolNodeOutputTokens, - childTotalPoints: flatUsages.reduce((sum, item) => sum + item.totalPoints, 0), + toolCallInputTokens: toolCallInputTokens, + toolCallOutputTokens: toolCallOutputTokens, + childTotalPoints: toolTotalPoints, model: modelName, query: userChatInput, historyPreview: getHistoryPreview( - GPTMessages2Chats(completeMessages, false), + GPTMessages2Chats({ messages: completeMessages, reserveTool: false }), 10000, useVision ), - toolDetail: childToolResponse, + toolDetail: dispatchFlowResponse.map((item) => item.flowResponses).flat(), mergeSignId: nodeId, finishReason: finish_reason }, [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ - // 工具调用本身的积分消耗 + // 模型本身的积分消耗 { moduleName: name, model: modelName, - totalPoints: toolAIUsage, - inputTokens: toolNodeInputTokens, - outputTokens: toolNodeOutputTokens + totalPoints: modelUsage, + inputTokens: toolCallInputTokens, + outputTokens: toolCallOutputTokens }, // 工具的消耗 - ...flatUsages + ...toolUsages ], [DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/promptCall.ts b/packages/service/core/workflow/dispatch/ai/agent/promptCall.ts deleted file mode 100644 index e7ec8bcf860a..000000000000 --- a/packages/service/core/workflow/dispatch/ai/agent/promptCall.ts +++ /dev/null @@ -1,709 +0,0 @@ -import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; -import { - type StreamChatType, - type ChatCompletionMessageParam, - type CompletionFinishReason -} from '@fastgpt/global/core/ai/type'; -import { type NextApiResponse } from 'next'; -import { responseWriteController } from '../../../../../common/response'; -import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; -import { - ChatCompletionRequestMessageRoleEnum, - getLLMDefaultUsage -} from '@fastgpt/global/core/ai/constants'; -import { dispatchWorkFlow } from '../../index'; -import { type DispatchToolModuleProps, type RunToolResponse, type ToolNodeItemType } from './type'; -import json5 from 'json5'; -import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; -import { - getNanoid, - replaceVariable, - sliceJsonStr, - sliceStrStartEnd -} from '@fastgpt/global/common/string/tools'; -import { type AIChatItemType } from '@fastgpt/global/core/chat/type'; -import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; -import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; -import { - computedMaxToken, - llmCompletionsBodyFormat, - removeDatasetCiteText, - parseReasoningContent, - parseLLMStreamResponse -} from '../../../../ai/utils'; -import { type WorkflowResponseType } from '../../type'; -import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants'; -import { type WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; -import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; - -type FunctionCallCompletion = { - id: string; - name: string; - arguments: string; - toolName?: string; - toolAvatar?: string; -}; - -const ERROR_TEXT = 'Tool run error'; -const INTERACTIVE_STOP_SIGNAL = 'INTERACTIVE_STOP_SIGNAL'; - -export const runToolWithPromptCall = async ( - props: DispatchToolModuleProps, - response?: RunToolResponse -): Promise => { - const { messages, toolNodes, toolModel, interactiveEntryToolParams, ...workflowProps } = props; - const { - res, - requestOrigin, - runtimeNodes, - runtimeEdges, - externalProvider, - stream, - retainDatasetCite = true, - workflowStreamResponse, - params: { - temperature, - maxToken, - aiChatVision, - aiChatReasoning, - aiChatTopP, - aiChatStopSign, - aiChatResponseFormat, - aiChatJsonSchema - } - } = workflowProps; - - if (interactiveEntryToolParams) { - initToolNodes(runtimeNodes, interactiveEntryToolParams.entryNodeIds); - initToolCallEdges(runtimeEdges, interactiveEntryToolParams.entryNodeIds); - - // Run entry tool - const toolRunResponse = await dispatchWorkFlow({ - ...workflowProps, - isToolCall: true - }); - const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - id: interactiveEntryToolParams.toolCallId, - toolName: '', - toolAvatar: '', - params: '', - response: sliceStrStartEnd(stringToolResponse, 5000, 5000) - } - } - }); - - // Check interactive response(Only 1 interaction is reserved) - const workflowInteractiveResponseItem = toolRunResponse?.workflowInteractiveResponse - ? toolRunResponse - : undefined; - - // Rewrite toolCall messages - const concatMessages = [...messages.slice(0, -1), ...interactiveEntryToolParams.memoryMessages]; - const lastMessage = concatMessages[concatMessages.length - 1]; - lastMessage.content = workflowInteractiveResponseItem - ? lastMessage.content - : replaceVariable(lastMessage.content, { - [INTERACTIVE_STOP_SIGNAL]: stringToolResponse - }); - - // Check stop signal - const hasStopSignal = toolRunResponse.flowResponses.some((item) => !!item.toolStop); - if (hasStopSignal || workflowInteractiveResponseItem) { - // Get interactive tool data - const workflowInteractiveResponse = - workflowInteractiveResponseItem?.workflowInteractiveResponse; - const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = - workflowInteractiveResponse - ? { - ...workflowInteractiveResponse, - toolParams: { - entryNodeIds: workflowInteractiveResponse.entryNodeIds, - toolCallId: '', - memoryMessages: [lastMessage] - } - } - : undefined; - - return { - dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, - completeMessages: concatMessages, - assistantResponses: toolRunResponse.assistantResponses, - runTimes: toolRunResponse.runTimes, - toolWorkflowInteractiveResponse - }; - } - - return runToolWithPromptCall( - { - ...props, - interactiveEntryToolParams: undefined, - messages: concatMessages - }, - { - dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, - assistantResponses: toolRunResponse.assistantResponses, - runTimes: toolRunResponse.runTimes - } - ); - } - - const assistantResponses = response?.assistantResponses || []; - - const toolsPrompt = JSON.stringify( - toolNodes.map((item) => { - if (item.jsonSchema) { - return { - toolId: item.nodeId, - description: item.intro, - parameters: item.jsonSchema - }; - } - - const properties: Record< - string, - { - type: string; - description: string; - required?: boolean; - enum?: string[]; - } - > = {}; - item.toolParams.forEach((item) => { - const jsonSchema = item.valueType - ? valueTypeJsonSchemaMap[item.valueType] || toolValueTypeList[0].jsonSchema - : toolValueTypeList[0].jsonSchema; - - properties[item.key] = { - ...jsonSchema, - description: item.toolDescription || '', - enum: item.enum?.split('\n').filter(Boolean) || [] - }; - }); - - return { - toolId: item.nodeId, - description: item.toolDescription || item.intro, - parameters: { - type: 'object', - properties, - required: item.toolParams.filter((item) => item.required).map((item) => item.key) - } - }; - }) - ); - - const lastMessage = messages[messages.length - 1]; - if (typeof lastMessage.content === 'string') { - lastMessage.content = replaceVariable(lastMessage.content, { - toolsPrompt - }); - } else if (Array.isArray(lastMessage.content)) { - // array, replace last element - const lastText = lastMessage.content[lastMessage.content.length - 1]; - if (lastText.type === 'text') { - lastText.text = replaceVariable(lastText.text, { - toolsPrompt - }); - } else { - return Promise.reject('Prompt call invalid input'); - } - } else { - return Promise.reject('Prompt call invalid input'); - } - - const max_tokens = computedMaxToken({ - model: toolModel, - maxToken, - min: 100 - }); - const filterMessages = await filterGPTMessageByMaxContext({ - messages, - maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken - }); - - const [requestMessages] = await Promise.all([ - loadRequestMessages({ - messages: filterMessages, - useVision: aiChatVision, - origin: requestOrigin - }) - ]); - const requestBody = llmCompletionsBodyFormat( - { - model: toolModel.model, - stream, - messages: requestMessages, - temperature, - max_tokens, - top_p: aiChatTopP, - stop: aiChatStopSign, - response_format: { - type: aiChatResponseFormat as any, - json_schema: aiChatJsonSchema - } - }, - toolModel - ); - - // console.log(JSON.stringify(requestMessages, null, 2)); - /* Run llm */ - const { - response: aiResponse, - isStreamResponse, - getEmptyResponseTip - } = await createChatCompletion({ - body: requestBody, - userKey: externalProvider.openaiAccount, - options: { - headers: { - Accept: 'application/json, text/plain, */*' - } - } - }); - - let { answer, reasoning, finish_reason, inputTokens, outputTokens } = await (async () => { - if (isStreamResponse) { - if (!res || res.closed) { - return { - answer: '', - reasoning: '', - finish_reason: 'close' as const, - inputTokens: 0, - outputTokens: 0 - }; - } - const { answer, reasoning, finish_reason, usage } = await streamResponse({ - res, - toolNodes, - stream: aiResponse, - workflowStreamResponse, - aiChatReasoning, - retainDatasetCite - }); - - return { - answer, - reasoning, - finish_reason, - inputTokens: usage.prompt_tokens, - outputTokens: usage.completion_tokens - }; - } else { - const finish_reason = aiResponse.choices?.[0]?.finish_reason as CompletionFinishReason; - const content = aiResponse.choices?.[0]?.message?.content || ''; - // @ts-ignore - const reasoningContent: string = aiResponse.choices?.[0]?.message?.reasoning_content || ''; - const usage = aiResponse.usage; - - const formatReasonContent = removeDatasetCiteText(reasoningContent, retainDatasetCite); - const formatContent = removeDatasetCiteText(content, retainDatasetCite); - - // API already parse reasoning content - if (formatReasonContent || !aiChatReasoning) { - return { - answer: formatContent, - reasoning: formatReasonContent, - finish_reason, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens - }; - } - - const [think, answer] = parseReasoningContent(formatContent); - return { - answer, - reasoning: think, - finish_reason, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens - }; - } - })(); - - if (stream && !isStreamResponse && aiChatReasoning && reasoning) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - reasoning_content: reasoning - }) - }); - } - - const { answer: replaceAnswer, toolJson } = parseAnswer(answer); - if (!answer && !toolJson) { - return Promise.reject(getEmptyResponseTip()); - } - - // No tools - if (!toolJson) { - if (replaceAnswer === ERROR_TEXT) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: replaceAnswer - }) - }); - } - - // 不支持 stream 模式的模型的流失响应 - if (stream && !isStreamResponse) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: removeDatasetCiteText(replaceAnswer, retainDatasetCite) - }) - }); - } - - // No tool is invoked, indicating that the process is over - const gptAssistantResponse: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: replaceAnswer, - reasoning_text: reasoning - }; - const completeMessages = filterMessages.concat({ - ...gptAssistantResponse, - reasoning_text: undefined - }); - - inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages)); - outputTokens = outputTokens || (await countGptMessagesTokens([gptAssistantResponse])); - - // concat tool assistant - const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType; - - return { - dispatchFlowResponse: response?.dispatchFlowResponse || [], - toolNodeInputTokens: response?.toolNodeInputTokens - ? response.toolNodeInputTokens + inputTokens - : inputTokens, - toolNodeOutputTokens: response?.toolNodeOutputTokens - ? response.toolNodeOutputTokens + outputTokens - : outputTokens, - completeMessages, - assistantResponses: [...assistantResponses, ...toolNodeAssistant.value], - runTimes: (response?.runTimes || 0) + 1 - }; - } - - // Run the selected tool. - const toolsRunResponse = await (async () => { - const toolNode = toolNodes.find((item) => item.nodeId === toolJson.name); - if (!toolNode) return Promise.reject('tool not found'); - - toolJson.toolName = toolNode.name; - toolJson.toolAvatar = toolNode.avatar; - - // run tool flow - const startParams = (() => { - try { - return json5.parse(toolJson.arguments); - } catch (error) { - return {}; - } - })(); - - // SSE response to client - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: toolJson.id, - toolName: toolNode.name, - toolAvatar: toolNode.avatar, - functionName: toolJson.name, - params: toolJson.arguments, - response: '' - } - } - }); - - initToolNodes(runtimeNodes, [toolNode.nodeId], startParams); - const toolResponse = await dispatchWorkFlow({ - ...workflowProps, - isToolCall: true - }); - - const stringToolResponse = formatToolResponse(toolResponse.toolResponses); - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - id: toolJson.id, - toolName: '', - toolAvatar: '', - params: '', - response: sliceStrStartEnd(stringToolResponse, 500, 500) - } - } - }); - - return { - toolResponse, - toolResponsePrompt: stringToolResponse - }; - })(); - - // 合并工具调用的结果,使用 functionCall 格式存储。 - const assistantToolMsgParams: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - function_call: toolJson, - reasoning_text: reasoning - }; - - // Only toolCall tokens are counted here, Tool response tokens count towards the next reply - inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages)); - outputTokens = outputTokens || (await countGptMessagesTokens([assistantToolMsgParams])); - - /* - ... - user - assistant: tool data - function: tool response - */ - const functionResponseMessage: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Function, - name: toolJson.name, - content: toolsRunResponse.toolResponsePrompt - }; - - // tool node assistant - const toolNodeAssistant = GPTMessages2Chats([ - assistantToolMsgParams, - functionResponseMessage - ])[0] as AIChatItemType; - const toolChildAssistants = toolsRunResponse.toolResponse.assistantResponses.filter( - (item) => item.type !== ChatItemValueTypeEnum.interactive - ); - const toolNodeAssistants = [ - ...assistantResponses, - ...toolNodeAssistant.value, - ...toolChildAssistants - ]; - - const dispatchFlowResponse = response - ? [...response.dispatchFlowResponse, toolsRunResponse.toolResponse] - : [toolsRunResponse.toolResponse]; - - // Check interactive response(Only 1 interaction is reserved) - const workflowInteractiveResponseItem = toolsRunResponse.toolResponse?.workflowInteractiveResponse - ? toolsRunResponse.toolResponse - : undefined; - - // get the next user prompt - if (typeof lastMessage.content === 'string') { - lastMessage.content += `${replaceAnswer} -TOOL_RESPONSE: """ -${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt} -""" -ANSWER: `; - } else if (Array.isArray(lastMessage.content)) { - // array, replace last element - const lastText = lastMessage.content[lastMessage.content.length - 1]; - if (lastText.type === 'text') { - lastText.text += `${replaceAnswer} -TOOL_RESPONSE: """ -${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt} -""" -ANSWER: `; - } else { - return Promise.reject('Prompt call invalid input'); - } - } else { - return Promise.reject('Prompt call invalid input'); - } - - const runTimes = (response?.runTimes || 0) + toolsRunResponse.toolResponse.runTimes; - const toolNodeInputTokens = response?.toolNodeInputTokens - ? response.toolNodeInputTokens + inputTokens - : inputTokens; - const toolNodeOutputTokens = response?.toolNodeOutputTokens - ? response.toolNodeOutputTokens + outputTokens - : outputTokens; - - // Check stop signal - const hasStopSignal = toolsRunResponse.toolResponse.flowResponses.some((item) => !!item.toolStop); - - if (hasStopSignal || workflowInteractiveResponseItem) { - // Get interactive tool data - const workflowInteractiveResponse = - workflowInteractiveResponseItem?.workflowInteractiveResponse; - const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = - workflowInteractiveResponse - ? { - ...workflowInteractiveResponse, - toolParams: { - entryNodeIds: workflowInteractiveResponse.entryNodeIds, - toolCallId: '', - memoryMessages: [lastMessage] - } - } - : undefined; - - return { - dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - completeMessages: filterMessages, - assistantResponses: toolNodeAssistants, - runTimes, - toolWorkflowInteractiveResponse - }; - } - - return runToolWithPromptCall( - { - ...props, - messages - }, - { - dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - assistantResponses: toolNodeAssistants, - runTimes, - finish_reason - } - ); -}; - -async function streamResponse({ - res, - stream, - workflowStreamResponse, - aiChatReasoning, - retainDatasetCite -}: { - res: NextApiResponse; - toolNodes: ToolNodeItemType[]; - stream: StreamChatType; - workflowStreamResponse?: WorkflowResponseType; - aiChatReasoning?: boolean; - retainDatasetCite?: boolean; -}) { - const write = responseWriteController({ - res, - readStream: stream - }); - - let startResponseWrite = false; - let answer = ''; - - const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse(); - - for await (const part of stream) { - if (res.closed) { - stream.controller?.abort(); - updateFinishReason('close'); - break; - } - - const { reasoningContent, content, responseContent } = parsePart({ - part, - parseThinkTag: aiChatReasoning, - retainDatasetCite - }); - answer += content; - - // Reasoning response - if (aiChatReasoning && reasoningContent) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - reasoning_content: reasoningContent - }) - }); - } - - if (content) { - if (startResponseWrite) { - if (responseContent) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: responseContent - }) - }); - } - } else if (answer.length >= 3) { - answer = answer.trimStart(); - if (/0(:|:)/.test(answer)) { - startResponseWrite = true; - - // find first : index - const firstIndex = - answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0:'); - answer = answer.substring(firstIndex + 2).trim(); - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: answer - }) - }); - } - } - } - } - - const { reasoningContent, content, finish_reason, usage } = getResponseData(); - - return { answer: content, reasoning: reasoningContent, finish_reason, usage }; -} - -const parseAnswer = ( - str: string -): { - answer: string; - toolJson?: FunctionCallCompletion; -} => { - str = str.trim(); - // 首先,使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS - const prefixReg = /1(:|:)/; - - if (prefixReg.test(str)) { - const toolString = sliceJsonStr(str); - - try { - const toolCall = json5.parse(toolString); - return { - answer: `1: ${toolString}`, - toolJson: { - id: getNanoid(), - name: toolCall.toolId, - arguments: JSON.stringify(toolCall.arguments || toolCall.parameters) - } - }; - } catch (error) { - if (/^1(:|:)/.test(str)) { - return { - answer: ERROR_TEXT - }; - } else { - return { - answer: str - }; - } - } - } else { - const firstIndex = str.indexOf('0:') !== -1 ? str.indexOf('0:') : str.indexOf('0:'); - const answer = str.substring(firstIndex + 2).trim(); - return { - answer - }; - } -}; diff --git a/packages/service/core/workflow/dispatch/ai/agent/toolChoice.ts b/packages/service/core/workflow/dispatch/ai/agent/toolCall.ts similarity index 53% rename from packages/service/core/workflow/dispatch/ai/agent/toolChoice.ts rename to packages/service/core/workflow/dispatch/ai/agent/toolCall.ts index c36a4e2fe029..5dd264bbf899 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/toolCall.ts @@ -1,38 +1,27 @@ -import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; -import { - type ChatCompletion, - type ChatCompletionMessageToolCall, - type StreamChatType, - type ChatCompletionToolMessageParam, - type ChatCompletionMessageParam, - type ChatCompletionTool, - type CompletionFinishReason +import { filterGPTMessageByMaxContext } from '../../../../ai/llm/utils'; +import type { + ChatCompletionToolMessageParam, + ChatCompletionMessageParam, + ChatCompletionTool } from '@fastgpt/global/core/ai/type'; -import { type NextApiResponse } from 'next'; import { responseWriteController } from '../../../../../common/response'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { dispatchWorkFlow } from '../../index'; -import { type DispatchToolModuleProps, type RunToolResponse, type ToolNodeItemType } from './type'; +import type { DispatchToolModuleProps, RunToolResponse, ToolNodeItemType } from './type'; import json5 from 'json5'; -import { type DispatchFlowResponse, type WorkflowResponseType } from '../../type'; -import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; +import type { DispatchFlowResponse } from '../../type'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; -import { type AIChatItemType } from '@fastgpt/global/core/chat/type'; +import type { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; -import { - computedMaxToken, - llmCompletionsBodyFormat, - removeDatasetCiteText, - parseLLMStreamResponse -} from '../../../../ai/utils'; -import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; -import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants'; -import { type WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import { computedMaxToken } from '../../../../ai/utils'; +import { sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; +import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { getErrText } from '@fastgpt/global/common/error/utils'; +import { createLLMResponse } from '../../../../ai/llm/request'; +import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants'; type ToolRunResponseType = { toolRunResponse?: DispatchFlowResponse; @@ -76,7 +65,7 @@ type ToolRunResponseType = { 3. messages:本次递归中,assistants responses 和 tool responses */ -export const runToolWithToolChoice = async ( +export const runToolCall = async ( props: DispatchToolModuleProps & { maxRunToolTimes: number; }, @@ -110,7 +99,6 @@ export const runToolWithToolChoice = async ( aiChatReasoning } } = workflowProps; - aiChatReasoning = !!aiChatReasoning && !!toolModel.reasoning; if (maxRunToolTimes <= 0 && response) { return response; @@ -175,8 +163,8 @@ export const runToolWithToolChoice = async ( return { dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, + toolCallInputTokens: 0, + toolCallOutputTokens: 0, completeMessages: requestMessages, assistantResponses: toolRunResponse.assistantResponses, runTimes: toolRunResponse.runTimes, @@ -184,7 +172,7 @@ export const runToolWithToolChoice = async ( }; } - return runToolWithToolChoice( + return runToolCall( { ...props, interactiveEntryToolParams: undefined, @@ -194,8 +182,8 @@ export const runToolWithToolChoice = async ( }, { dispatchFlowResponse: [toolRunResponse], - toolNodeInputTokens: 0, - toolNodeOutputTokens: 0, + toolCallInputTokens: 0, + toolCallOutputTokens: 0, assistantResponses: toolRunResponse.assistantResponses, runTimes: toolRunResponse.runTimes } @@ -206,7 +194,9 @@ export const runToolWithToolChoice = async ( const assistantResponses = response?.assistantResponses || []; + const toolNodesMap = new Map(); const tools: ChatCompletionTool[] = toolNodes.map((item) => { + toolNodesMap.set(item.nodeId, item); if (item.jsonSchema) { return { type: 'function', @@ -282,20 +272,26 @@ export const runToolWithToolChoice = async ( return item; }); - const [requestMessages] = await Promise.all([ - loadRequestMessages({ - messages: filterMessages, - useVision: toolModel.vision && aiChatVision, - origin: requestOrigin - }) - ]); - const requestBody = llmCompletionsBodyFormat( - { + const write = res ? responseWriteController({ res, readStream: stream }) : undefined; + + let { + reasoningText: reasoningContent, + answerText: answer, + toolCalls = [], + finish_reason, + usage, + getEmptyResponseTip, + assistantMessage, + completeMessages + } = await createLLMResponse({ + body: { model: toolModel.model, stream, - messages: requestMessages, - tools, + reasoning: aiChatReasoning, + messages: filterMessages, tool_choice: 'auto', + toolCallMode: toolModel.toolChoice ? 'toolChoice' : 'prompt', + tools, parallel_tool_calls: true, temperature, max_tokens, @@ -304,124 +300,67 @@ export const runToolWithToolChoice = async ( response_format: { type: aiChatResponseFormat as any, json_schema: aiChatJsonSchema - } + }, + retainDatasetCite, + useVision: aiChatVision, + requestOrigin }, - toolModel - ); - // console.log(JSON.stringify(requestBody, null, 2), '==requestMessages'); - /* Run llm */ - const { - response: aiResponse, - isStreamResponse, - getEmptyResponseTip - } = await createChatCompletion({ - body: requestBody, + isAborted: () => res?.closed, userKey: externalProvider.openaiAccount, - options: { - headers: { - Accept: 'application/json, text/plain, */*' - } - } - }); - - let { reasoningContent, answer, toolCalls, finish_reason, inputTokens, outputTokens } = - await (async () => { - if (isStreamResponse) { - if (!res || res.closed) { - return { - reasoningContent: '', - answer: '', - toolCalls: [], - finish_reason: 'close' as const, - inputTokens: 0, - outputTokens: 0 - }; - } - - const result = await streamResponse({ - res, - workflowStreamResponse, - toolNodes, - stream: aiResponse, - aiChatReasoning, - retainDatasetCite - }); - - return { - reasoningContent: result.reasoningContent, - answer: result.answer, - toolCalls: result.toolCalls, - finish_reason: result.finish_reason, - inputTokens: result.usage.prompt_tokens, - outputTokens: result.usage.completion_tokens - }; - } else { - const result = aiResponse as ChatCompletion; - const finish_reason = result.choices?.[0]?.finish_reason as CompletionFinishReason; - const calls = result.choices?.[0]?.message?.tool_calls || []; - const answer = result.choices?.[0]?.message?.content || ''; - // @ts-ignore - const reasoningContent = result.choices?.[0]?.message?.reasoning_content || ''; - const usage = result.usage; - - const formatReasoningContent = removeDatasetCiteText(reasoningContent, retainDatasetCite); - const formatAnswer = removeDatasetCiteText(answer, retainDatasetCite); - - if (aiChatReasoning && reasoningContent) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - reasoning_content: formatReasoningContent - }) - }); - } - - // 格式化 toolCalls - const toolCalls = calls.map((tool) => { - const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); - - // 不支持 stream 模式的模型的这里需要补一个响应给客户端 - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: tool.id, - toolName: toolNode?.name || '', - toolAvatar: toolNode?.avatar || '', - functionName: tool.function.name, - params: tool.function?.arguments ?? '', - response: '' - } + onReasoning({ text }) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + reasoning_content: text + }) + }); + }, + onStreaming({ text }) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text + }) + }); + }, + onToolCall({ call }) { + const toolNode = toolNodesMap.get(call.function.name); + if (toolNode) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: call.id, + toolName: toolNode.name, + toolAvatar: toolNode.avatar, + functionName: call.function.name, + params: call.function.arguments ?? '', + response: '' } - }); - - return { - ...tool, - toolName: toolNode?.name || '', - toolAvatar: toolNode?.avatar || '' - }; + } }); - - if (answer) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: formatAnswer - }) - }); + } + }, + onToolParam({ tool, params }) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.toolParams, + data: { + tool: { + id: tool.id, + toolName: '', + toolAvatar: '', + params, + response: '' + } } + }); + } + }); - return { - reasoningContent: formatReasoningContent, - answer: formatAnswer, - toolCalls: toolCalls, - finish_reason, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens - }; - } - })(); - if (!answer && !reasoningContent && toolCalls.length === 0) { + if (!answer && !reasoningContent && !toolCalls.length) { return Promise.reject(getEmptyResponseTip()); } @@ -431,7 +370,7 @@ export const runToolWithToolChoice = async ( const toolsRunResponse: ToolRunResponseType = []; for await (const tool of toolCalls) { try { - const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); + const toolNode = toolNodesMap.get(tool.function?.name); if (!toolNode) continue; @@ -511,64 +450,46 @@ export const runToolWithToolChoice = async ( ? response.dispatchFlowResponse.concat(flatToolsResponseData) : flatToolsResponseData; - if (toolCalls.length > 0) { - // Run the tool, combine its results, and perform another round of AI calls - const assistantToolMsgParams: ChatCompletionMessageParam[] = [ - ...(answer || reasoningContent - ? [ - { - role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant', - content: answer, - reasoning_text: reasoningContent - } - ] - : []), - { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - tool_calls: toolCalls - } - ]; - - /* - ... - user - assistant: tool data - */ - const concatToolMessages = [ - ...requestMessages, - ...assistantToolMsgParams - ] as ChatCompletionMessageParam[]; - - // Only toolCall tokens are counted here, Tool response tokens count towards the next reply - inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages, tools)); - outputTokens = outputTokens || (await countGptMessagesTokens(assistantToolMsgParams)); + const inputTokens = response + ? response.toolCallInputTokens + usage.inputTokens + : usage.inputTokens; + const outputTokens = response + ? response.toolCallOutputTokens + usage.outputTokens + : usage.outputTokens; + if (toolCalls.length > 0) { /* ... user assistant: tool data tool: tool response */ - const completeMessages = [ - ...concatToolMessages, + const nextRequestMessages: ChatCompletionMessageParam[] = [ + ...completeMessages, ...toolsRunResponse.map((item) => item?.toolMsgParams) ]; /* Get tool node assistant response - history assistant - current tool assistant - tool child assistant + - history assistant + - current tool assistant + - tool child assistant */ - const toolNodeAssistant = GPTMessages2Chats([ - ...assistantToolMsgParams, - ...toolsRunResponse.map((item) => item?.toolMsgParams) - ])[0] as AIChatItemType; + const toolNodeAssistant = GPTMessages2Chats({ + messages: [...assistantMessage, ...toolsRunResponse.map((item) => item?.toolMsgParams)], + getToolInfo: (id) => { + const toolNode = toolNodesMap.get(id); + return { + name: toolNode?.name || '', + avatar: toolNode?.avatar || '' + }; + } + })[0] as AIChatItemType; const toolChildAssistants = flatToolsResponseData .map((item) => item.assistantResponses) .flat() .filter((item) => item.type !== ChatItemValueTypeEnum.interactive); // 交互节点留着下次记录 - const toolNodeAssistants = [ + const concatAssistantResponses = [ ...assistantResponses, ...toolNodeAssistant.value, ...toolChildAssistants @@ -577,10 +498,6 @@ export const runToolWithToolChoice = async ( const runTimes = (response?.runTimes || 0) + flatToolsResponseData.reduce((sum, item) => sum + item.runTimes, 0); - const toolNodeInputTokens = response ? response.toolNodeInputTokens + inputTokens : inputTokens; - const toolNodeOutputTokens = response - ? response.toolNodeOutputTokens + outputTokens - : outputTokens; // Check stop signal const hasStopSignal = flatToolsResponseData.some( @@ -596,8 +513,8 @@ export const runToolWithToolChoice = async ( workflowInteractiveResponseItem?.toolRunResponse?.workflowInteractiveResponse; // Flashback traverses completeMessages, intercepting messages that know the first user - const firstUserIndex = completeMessages.findLastIndex((item) => item.role === 'user'); - const newMessages = completeMessages.slice(firstUserIndex + 1); + const firstUserIndex = nextRequestMessages.findLastIndex((item) => item.role === 'user'); + const newMessages = nextRequestMessages.slice(firstUserIndex + 1); const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = workflowInteractiveResponse @@ -613,49 +530,41 @@ export const runToolWithToolChoice = async ( return { dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - completeMessages, - assistantResponses: toolNodeAssistants, + toolCallInputTokens: inputTokens, + toolCallOutputTokens: outputTokens, + completeMessages: nextRequestMessages, + assistantResponses: concatAssistantResponses, toolWorkflowInteractiveResponse, runTimes, finish_reason }; } - return runToolWithToolChoice( + return runToolCall( { ...props, maxRunToolTimes: maxRunToolTimes - 1, - messages: completeMessages + messages: nextRequestMessages }, { dispatchFlowResponse, - toolNodeInputTokens, - toolNodeOutputTokens, - assistantResponses: toolNodeAssistants, + toolCallInputTokens: inputTokens, + toolCallOutputTokens: outputTokens, + assistantResponses: concatAssistantResponses, runTimes, finish_reason } ); } else { - // No tool is invoked, indicating that the process is over - const gptAssistantResponse: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: answer, - reasoning_text: reasoningContent - }; - const completeMessages = filterMessages.concat(gptAssistantResponse); - inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages, tools)); - outputTokens = outputTokens || (await countGptMessagesTokens([gptAssistantResponse])); - // concat tool assistant - const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType; + const toolNodeAssistant = GPTMessages2Chats({ + messages: assistantMessage + })[0] as AIChatItemType; return { dispatchFlowResponse: response?.dispatchFlowResponse || [], - toolNodeInputTokens: response ? response.toolNodeInputTokens + inputTokens : inputTokens, - toolNodeOutputTokens: response ? response.toolNodeOutputTokens + outputTokens : outputTokens, + toolCallInputTokens: inputTokens, + toolCallOutputTokens: outputTokens, completeMessages, assistantResponses: [...assistantResponses, ...toolNodeAssistant.value], @@ -664,152 +573,3 @@ export const runToolWithToolChoice = async ( }; } }; - -async function streamResponse({ - res, - toolNodes, - stream, - workflowStreamResponse, - aiChatReasoning, - retainDatasetCite -}: { - res: NextApiResponse; - toolNodes: ToolNodeItemType[]; - stream: StreamChatType; - workflowStreamResponse?: WorkflowResponseType; - aiChatReasoning: boolean; - retainDatasetCite?: boolean; -}) { - const write = responseWriteController({ - res, - readStream: stream - }); - - let callingTool: { name: string; arguments: string } | null = null; - let toolCalls: ChatCompletionMessageToolCall[] = []; - - const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse(); - - for await (const part of stream) { - if (res.closed) { - stream.controller?.abort(); - updateFinishReason('close'); - break; - } - - const { reasoningContent, responseContent } = parsePart({ - part, - parseThinkTag: true, - retainDatasetCite - }); - - const responseChoice = part.choices?.[0]?.delta; - - // Reasoning response - if (aiChatReasoning && reasoningContent) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - reasoning_content: reasoningContent - }) - }); - } - if (responseContent) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: responseContent - }) - }); - } - // Parse tool calls - if (responseChoice?.tool_calls?.length) { - responseChoice.tool_calls.forEach((toolCall, i) => { - const index = toolCall.index ?? i; - - // Call new tool - const hasNewTool = toolCall?.function?.name || callingTool; - if (hasNewTool) { - // 有 function name,代表新 call 工具 - if (toolCall?.function?.name) { - callingTool = { - name: toolCall.function?.name || '', - arguments: toolCall.function?.arguments || '' - }; - } else if (callingTool) { - // Continue call(Perhaps the name of the previous function was incomplete) - callingTool.name += toolCall.function?.name || ''; - callingTool.arguments += toolCall.function?.arguments || ''; - } - - if (!callingTool) { - return; - } - - const toolNode = toolNodes.find((item) => item.nodeId === callingTool!.name); - - if (toolNode) { - // New tool, add to list. - const toolId = getNanoid(); - toolCalls[index] = { - ...toolCall, - id: toolId, - type: 'function', - function: callingTool, - toolName: toolNode.name, - toolAvatar: toolNode.avatar - }; - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: toolId, - toolName: toolNode.name, - toolAvatar: toolNode.avatar, - functionName: callingTool.name, - params: callingTool?.arguments ?? '', - response: '' - } - } - }); - callingTool = null; - } - } else { - /* arg 追加到当前工具的参数里 */ - const arg: string = toolCall?.function?.arguments ?? ''; - const currentTool = toolCalls[index]; - if (currentTool && arg) { - currentTool.function.arguments += arg; - - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.toolParams, - data: { - tool: { - id: currentTool.id, - toolName: '', - toolAvatar: '', - params: arg, - response: '' - } - } - }); - } - } - }); - } - } - - const { reasoningContent, content, finish_reason, usage } = getResponseData(); - - return { - reasoningContent, - answer: content, - toolCalls: toolCalls.filter(Boolean), - finish_reason, - usage - }; -} diff --git a/packages/service/core/workflow/dispatch/ai/agent/type.d.ts b/packages/service/core/workflow/dispatch/ai/agent/type.d.ts index 68c452d93483..944dba7cef90 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/type.d.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/type.d.ts @@ -42,8 +42,8 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{ export type RunToolResponse = { dispatchFlowResponse: DispatchFlowResponse[]; - toolNodeInputTokens: number; - toolNodeOutputTokens: number; + toolCallInputTokens: number; + toolCallOutputTokens: number; completeMessages?: ChatCompletionMessageParam[]; assistantResponses?: AIChatItemValueItemType[]; toolWorkflowInteractiveResponse?: WorkflowInteractiveResponseType; diff --git a/packages/service/core/workflow/dispatch/ai/chat.ts b/packages/service/core/workflow/dispatch/ai/chat.ts index b3d02fe91021..bf93f93a3cdf 100644 --- a/packages/service/core/workflow/dispatch/ai/chat.ts +++ b/packages/service/core/workflow/dispatch/ai/chat.ts @@ -1,28 +1,13 @@ -import type { NextApiResponse } from 'next'; -import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../chat/utils'; +import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; -import { - removeDatasetCiteText, - parseReasoningContent, - parseLLMStreamResponse -} from '../../../ai/utils'; -import { createChatCompletion } from '../../../ai/config'; -import type { - ChatCompletionMessageParam, - CompletionFinishReason, - StreamChatType -} from '@fastgpt/global/core/ai/type.d'; -import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; -import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import type { ChatDispatchProps, DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { countGptMessagesTokens } from '../../../../common/string/tiktoken/index'; import { chats2GPTMessages, chatValue2RuntimePrompt, @@ -47,16 +32,15 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti import { checkQuoteQAValue, getNodeErrResponse, getHistories } from '../utils'; import { filterSearchResultsByMaxChars } from '../../utils'; import { getHistoryPreview } from '@fastgpt/global/core/chat/utils'; -import { computedMaxToken, llmCompletionsBodyFormat } from '../../../ai/utils'; -import { type WorkflowResponseType } from '../type'; +import { computedMaxToken } from '../../../ai/utils'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; -import { type AiChatQuoteRoleType } from '@fastgpt/global/core/workflow/template/system/aiChat/type'; +import type { AiChatQuoteRoleType } from '@fastgpt/global/core/workflow/template/system/aiChat/type'; import { getFileContentFromLinks, getHistoryFileLinks } from '../tools/readFiles'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; import { i18nT } from '../../../../../web/i18n/utils'; -import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { postTextCensor } from '../../../chat/postTextCensor'; -import { getErrText } from '@fastgpt/global/common/error/utils'; +import { createLLMResponse } from '../../../ai/llm/request'; +import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; export type ChatProps = ModuleDispatchProps< AIChatNodeProps & { @@ -124,7 +108,6 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise item.key === NodeInputKeyEnum.fileUrlList); if (!fileUrlInput || !fileUrlInput.value || fileUrlInput.value.length === 0) { @@ -188,165 +171,82 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise res?.closed, + onReasoning({ text }) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + reasoning_content: text + }) + }); + }, + onStreaming({ text }) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text + }) + }); } }); - let { answerText, reasoningText, finish_reason, inputTokens, outputTokens } = - await (async () => { - if (isStreamResponse) { - if (!res || res.closed) { - return { - answerText: '', - reasoningText: '', - finish_reason: 'close' as const, - inputTokens: 0, - outputTokens: 0 - }; - } - // sse response - const { answer, reasoning, finish_reason, usage } = await streamResponse({ - res, - stream: response, - aiChatReasoning, - parseThinkTag: modelConstantsData.reasoning, - isResponseAnswerText, - workflowStreamResponse, - retainDatasetCite - }); - - return { - answerText: answer, - reasoningText: reasoning, - finish_reason, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens - }; - } else { - const finish_reason = response.choices?.[0]?.finish_reason as CompletionFinishReason; - const usage = response.usage; - - const { content, reasoningContent } = (() => { - const content = response.choices?.[0]?.message?.content || ''; - const reasoningContent: string = - // @ts-ignore - response.choices?.[0]?.message?.reasoning_content || ''; - - // API already parse reasoning content - if (reasoningContent || !aiChatReasoning) { - return { - content, - reasoningContent - }; - } - - const [think, answer] = parseReasoningContent(content); - return { - content: answer, - reasoningContent: think - }; - })(); - - const formatReasonContent = removeDatasetCiteText(reasoningContent, retainDatasetCite); - const formatContent = removeDatasetCiteText(content, retainDatasetCite); - - // Some models do not support streaming - if (aiChatReasoning && reasoningContent) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - reasoning_content: formatReasonContent - }) - }); - } - if (isResponseAnswerText && content) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: formatContent - }) - }); - } - - return { - reasoningText: formatReasonContent, - answerText: formatContent, - finish_reason, - inputTokens: usage?.prompt_tokens, - outputTokens: usage?.completion_tokens - }; - } - })(); - if (!answerText && !reasoningText) { return getNodeErrResponse({ error: getEmptyResponseTip() }); } - const AIMessages: ChatCompletionMessageParam[] = [ - { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: answerText, - reasoning_text: reasoningText // reasoning_text is only recorded for response, but not for request - } - ]; - - const completeMessages = [...requestMessages, ...AIMessages]; - const chatCompleteMessages = GPTMessages2Chats(completeMessages); - - inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages)); - outputTokens = outputTokens || (await countGptMessagesTokens(AIMessages)); - const { totalPoints, modelName } = formatModelChars2Points({ - model, - inputTokens, - outputTokens, - modelType: ModelTypeEnum.llm + model: modelConstantsData.model, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens }); + const points = externalProvider.openaiAccount?.key ? 0 : totalPoints; + + const chatCompleteMessages = GPTMessages2Chats({ messages: completeMessages }); - const trimAnswer = answerText.trim(); return { data: { - answerText: trimAnswer, + answerText: answerText, reasoningText, history: chatCompleteMessages }, - [DispatchNodeResponseKeyEnum.answerText]: isResponseAnswerText ? trimAnswer : undefined, + [DispatchNodeResponseKeyEnum.answerText]: isResponseAnswerText ? answerText : undefined, [DispatchNodeResponseKeyEnum.reasoningText]: aiChatReasoning ? reasoningText : undefined, [DispatchNodeResponseKeyEnum.nodeResponse]: { - totalPoints: externalProvider.openaiAccount?.key ? 0 : totalPoints, + totalPoints: points, model: modelName, - inputTokens: inputTokens, - outputTokens: outputTokens, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens, query: `${userChatInput}`, maxToken: max_tokens, reasoningText, @@ -357,10 +257,10 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { const { totalPoints, modelName } = formatModelChars2Points({ model: extractModel.model, inputTokens: inputTokens, - outputTokens: outputTokens, - modelType: ModelTypeEnum.llm + outputTokens: outputTokens }); return { @@ -231,10 +224,6 @@ const toolChoice = async (props: ActionProps) => { messages: adaptMessages, maxContext: extractModel.maxContext }); - const requestMessages = await loadRequestMessages({ - messages: filterMessages, - useVision: false - }); const schema = getJsonSchema(props); @@ -253,23 +242,22 @@ const toolChoice = async (props: ActionProps) => { } ]; - const body = llmCompletionsBodyFormat( - { - stream: true, - model: extractModel.model, - temperature: 0.01, - messages: requestMessages, - tools, - tool_choice: { type: 'function', function: { name: agentFunName } } - }, - extractModel - ); - - const { response } = await createChatCompletion({ + const body = { + stream: true, + model: extractModel.model, + temperature: 0.01, + messages: filterMessages, + tools, + tool_choice: { type: 'function', function: { name: agentFunName } } + } as const; + const { + answerText: text, + toolCalls, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ body, userKey: externalProvider.openaiAccount }); - const { text, toolCalls, usage } = await formatLLMResponse(response); const arg: Record = (() => { try { @@ -289,8 +277,6 @@ const toolChoice = async (props: ActionProps) => { } ]; - const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(filterMessages, tools)); - const outputTokens = usage?.completion_tokens || (await countGptMessagesTokens(AIMessages)); return { inputTokens, outputTokens, @@ -336,26 +322,19 @@ const completions = async (props: ActionProps) => { ] } ]; - const requestMessages = await loadRequestMessages({ - messages: chats2GPTMessages({ messages, reserveId: false }), - useVision: false - }); - const { response } = await createChatCompletion({ - body: llmCompletionsBodyFormat( - { - model: extractModel.model, - temperature: 0.01, - messages: requestMessages, - stream: true - }, - extractModel - ), + const { + answerText: answer, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model: extractModel.model, + temperature: 0.01, + messages: chats2GPTMessages({ messages, reserveId: false }), + stream: true + }, userKey: externalProvider.openaiAccount }); - const { text: answer, usage } = await formatLLMResponse(response); - const inputTokens = usage?.prompt_tokens || (await countMessagesTokens(messages)); - const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); // parse response const jsonStr = sliceJsonStr(answer); diff --git a/packages/service/core/workflow/dispatch/constants.ts b/packages/service/core/workflow/dispatch/constants.ts new file mode 100644 index 000000000000..0bb6eca59070 --- /dev/null +++ b/packages/service/core/workflow/dispatch/constants.ts @@ -0,0 +1,75 @@ +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { dispatchAppRequest } from './abandoned/runApp'; +import { dispatchClassifyQuestion } from './ai/classifyQuestion'; +import { dispatchContentExtract } from './ai/extract'; +import { dispatchRunTools } from './ai/agent/index'; +import { dispatchStopToolCall } from './ai/agent/stopTool'; +import { dispatchToolParams } from './ai/agent/toolParams'; +import { dispatchChatCompletion } from './ai/chat'; +import { dispatchCodeSandbox } from './tools/codeSandbox'; +import { dispatchDatasetConcat } from './dataset/concat'; +import { dispatchDatasetSearch } from './dataset/search'; +import { dispatchSystemConfig } from './init/systemConfig'; +import { dispatchWorkflowStart } from './init/workflowStart'; +import { dispatchFormInput } from './interactive/formInput'; +import { dispatchUserSelect } from './interactive/userSelect'; +import { dispatchLoop } from './loop/runLoop'; +import { dispatchLoopEnd } from './loop/runLoopEnd'; +import { dispatchLoopStart } from './loop/runLoopStart'; +import { dispatchRunPlugin } from './plugin/run'; +import { dispatchRunAppNode } from './child/runApp'; +import { dispatchPluginInput } from './plugin/runInput'; +import { dispatchPluginOutput } from './plugin/runOutput'; +import { dispatchRunTool } from './child/runTool'; +import { dispatchAnswer } from './tools/answer'; +import { dispatchCustomFeedback } from './tools/customFeedback'; +import { dispatchHttp468Request } from './tools/http468'; +import { dispatchQueryExtension } from './tools/queryExternsion'; +import { dispatchReadFiles } from './tools/readFiles'; +import { dispatchIfElse } from './tools/runIfElse'; +import { dispatchLafRequest } from './tools/runLaf'; +import { dispatchUpdateVariable } from './tools/runUpdateVar'; +import { dispatchTextEditor } from './tools/textEditor'; + +export const callbackMap: Record = { + [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, + [FlowNodeTypeEnum.answerNode]: dispatchAnswer, + [FlowNodeTypeEnum.chatNode]: dispatchChatCompletion, + [FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch, + [FlowNodeTypeEnum.datasetConcatNode]: dispatchDatasetConcat, + [FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion, + [FlowNodeTypeEnum.contentExtract]: dispatchContentExtract, + [FlowNodeTypeEnum.httpRequest468]: dispatchHttp468Request, + [FlowNodeTypeEnum.appModule]: dispatchRunAppNode, + [FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin, + [FlowNodeTypeEnum.pluginInput]: dispatchPluginInput, + [FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput, + [FlowNodeTypeEnum.queryExtension]: dispatchQueryExtension, + [FlowNodeTypeEnum.agent]: dispatchRunTools, + [FlowNodeTypeEnum.stopTool]: dispatchStopToolCall, + [FlowNodeTypeEnum.toolParams]: dispatchToolParams, + [FlowNodeTypeEnum.lafModule]: dispatchLafRequest, + [FlowNodeTypeEnum.ifElseNode]: dispatchIfElse, + [FlowNodeTypeEnum.variableUpdate]: dispatchUpdateVariable, + [FlowNodeTypeEnum.code]: dispatchCodeSandbox, + [FlowNodeTypeEnum.textEditor]: dispatchTextEditor, + [FlowNodeTypeEnum.customFeedback]: dispatchCustomFeedback, + [FlowNodeTypeEnum.readFiles]: dispatchReadFiles, + [FlowNodeTypeEnum.userSelect]: dispatchUserSelect, + [FlowNodeTypeEnum.loop]: dispatchLoop, + [FlowNodeTypeEnum.loopStart]: dispatchLoopStart, + [FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd, + [FlowNodeTypeEnum.formInput]: dispatchFormInput, + [FlowNodeTypeEnum.tool]: dispatchRunTool, + + // none + [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, + [FlowNodeTypeEnum.pluginConfig]: () => Promise.resolve(), + [FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(), + [FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(), + [FlowNodeTypeEnum.comment]: () => Promise.resolve(), + [FlowNodeTypeEnum.toolSet]: () => Promise.resolve(), + + // @deprecated + [FlowNodeTypeEnum.runApp]: dispatchAppRequest +}; diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index dd68b38c0329..3fc5c36e34e3 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -169,8 +169,7 @@ export async function dispatchDatasetSearch( const { totalPoints: embeddingTotalPoints, modelName: embeddingModelName } = formatModelChars2Points({ model: vectorModel.model, - inputTokens: embeddingTokens, - modelType: ModelTypeEnum.embedding + inputTokens: embeddingTokens }); nodeDispatchUsages.push({ totalPoints: embeddingTotalPoints, @@ -181,8 +180,7 @@ export async function dispatchDatasetSearch( // Rerank const { totalPoints: reRankTotalPoints, modelName: reRankModelName } = formatModelChars2Points({ model: rerankModelData?.model, - inputTokens: reRankInputTokens, - modelType: ModelTypeEnum.rerank + inputTokens: reRankInputTokens }); if (usingReRank) { nodeDispatchUsages.push({ @@ -198,8 +196,7 @@ export async function dispatchDatasetSearch( const { totalPoints, modelName } = formatModelChars2Points({ model: queryExtensionResult.model, inputTokens: queryExtensionResult.inputTokens, - outputTokens: queryExtensionResult.outputTokens, - modelType: ModelTypeEnum.llm + outputTokens: queryExtensionResult.outputTokens }); nodeDispatchUsages.push({ totalPoints, @@ -222,8 +219,7 @@ export async function dispatchDatasetSearch( const { totalPoints, modelName } = formatModelChars2Points({ model: deepSearchResult.model, inputTokens: deepSearchResult.inputTokens, - outputTokens: deepSearchResult.outputTokens, - modelType: ModelTypeEnum.llm + outputTokens: deepSearchResult.outputTokens }); nodeDispatchUsages.push({ totalPoints, diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index fe85a45dffbf..97484f8344d8 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -42,83 +42,10 @@ import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edg import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { addLog } from '../../../common/system/log'; import { surrenderProcess } from '../../../common/system/tools'; -import { dispatchAppRequest } from './abandoned/runApp'; -import { dispatchClassifyQuestion } from './ai/classifyQuestion'; -import { dispatchContentExtract } from './ai/extract'; -import { dispatchRunTools } from './ai/agent/index'; -import { dispatchStopToolCall } from './ai/agent/stopTool'; -import { dispatchToolParams } from './ai/agent/toolParams'; -import { dispatchChatCompletion } from './ai/chat'; -import { dispatchCodeSandbox } from './tools/codeSandbox'; -import { dispatchDatasetConcat } from './dataset/concat'; -import { dispatchDatasetSearch } from './dataset/search'; -import { dispatchSystemConfig } from './init/systemConfig'; -import { dispatchWorkflowStart } from './init/workflowStart'; -import { dispatchFormInput } from './interactive/formInput'; -import { dispatchUserSelect } from './interactive/userSelect'; -import { dispatchLoop } from './loop/runLoop'; -import { dispatchLoopEnd } from './loop/runLoopEnd'; -import { dispatchLoopStart } from './loop/runLoopStart'; -import { dispatchRunPlugin } from './plugin/run'; -import { dispatchRunAppNode } from './child/runApp'; -import { dispatchPluginInput } from './plugin/runInput'; -import { dispatchPluginOutput } from './plugin/runOutput'; -import { dispatchRunTool } from './child/runTool'; -import { dispatchAnswer } from './tools/answer'; -import { dispatchCustomFeedback } from './tools/customFeedback'; -import { dispatchHttp468Request } from './tools/http468'; -import { dispatchQueryExtension } from './tools/queryExternsion'; -import { dispatchReadFiles } from './tools/readFiles'; -import { dispatchIfElse } from './tools/runIfElse'; -import { dispatchLafRequest } from './tools/runLaf'; -import { dispatchUpdateVariable } from './tools/runUpdateVar'; -import { dispatchTextEditor } from './tools/textEditor'; import type { DispatchFlowResponse } from './type'; import { removeSystemVariable, rewriteRuntimeWorkFlow } from './utils'; import { getHandleId } from '@fastgpt/global/core/workflow/utils'; - -const callbackMap: Record = { - [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, - [FlowNodeTypeEnum.answerNode]: dispatchAnswer, - [FlowNodeTypeEnum.chatNode]: dispatchChatCompletion, - [FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch, - [FlowNodeTypeEnum.datasetConcatNode]: dispatchDatasetConcat, - [FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion, - [FlowNodeTypeEnum.contentExtract]: dispatchContentExtract, - [FlowNodeTypeEnum.httpRequest468]: dispatchHttp468Request, - [FlowNodeTypeEnum.appModule]: dispatchRunAppNode, - [FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin, - [FlowNodeTypeEnum.pluginInput]: dispatchPluginInput, - [FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput, - [FlowNodeTypeEnum.queryExtension]: dispatchQueryExtension, - [FlowNodeTypeEnum.agent]: dispatchRunTools, - [FlowNodeTypeEnum.stopTool]: dispatchStopToolCall, - [FlowNodeTypeEnum.toolParams]: dispatchToolParams, - [FlowNodeTypeEnum.lafModule]: dispatchLafRequest, - [FlowNodeTypeEnum.ifElseNode]: dispatchIfElse, - [FlowNodeTypeEnum.variableUpdate]: dispatchUpdateVariable, - [FlowNodeTypeEnum.code]: dispatchCodeSandbox, - [FlowNodeTypeEnum.textEditor]: dispatchTextEditor, - [FlowNodeTypeEnum.customFeedback]: dispatchCustomFeedback, - [FlowNodeTypeEnum.readFiles]: dispatchReadFiles, - [FlowNodeTypeEnum.userSelect]: dispatchUserSelect, - [FlowNodeTypeEnum.loop]: dispatchLoop, - [FlowNodeTypeEnum.loopStart]: dispatchLoopStart, - [FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd, - [FlowNodeTypeEnum.formInput]: dispatchFormInput, - [FlowNodeTypeEnum.tool]: dispatchRunTool, - - // none - [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, - [FlowNodeTypeEnum.pluginConfig]: () => Promise.resolve(), - [FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(), - [FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(), - [FlowNodeTypeEnum.comment]: () => Promise.resolve(), - [FlowNodeTypeEnum.toolSet]: () => Promise.resolve(), - - // @deprecated - [FlowNodeTypeEnum.runApp]: dispatchAppRequest -}; +import { callbackMap } from './constants'; type Props = ChatDispatchProps & { runtimeNodes: RuntimeNodeItemType[]; @@ -139,28 +66,30 @@ export async function dispatchWorkFlow(data: Props): Promise 20) { + // 初始化 runtimeNodesMap + const runtimeNodesMap = new Map(runtimeNodes.map((item) => [item.nodeId, item])); + + // Over max depth + if (data.workflowDispatchDeep > 20) { return { flowResponses: [], flowUsages: [], @@ -198,7 +127,7 @@ export async function dispatchWorkFlow(data: Props): Promise { - props?.workflowStreamResponse?.({ + data?.workflowStreamResponse?.({ event: SseResponseEventEnum.answer, data: textAdaptGptResponse({ text: '' @@ -214,624 +143,698 @@ export async function dispatchWorkFlow(data: Props): Promise = {}; // Workflow node memories - - /* Store special response field */ - function pushStore( - { inputs = [] }: RuntimeNodeItemType, - { - answerText, - reasoningText, - responseData, - nodeDispatchUsages, - toolResponses, - assistantResponses, - rewriteHistories, - runTimes = 1, - system_memories: newMemories - }: NodeResponseCompleteType - ) { - // Add run times - workflowRunTimes += runTimes; - props.maxRunTimes -= runTimes; - - if (newMemories) { - system_memories = { - ...system_memories, - ...newMemories - }; + /* + 工作流队列控制 + 特点: + 1. 可以控制一个 team 下,并发 run 的节点数量。 + 2. 每个节点,同时只会执行一个。一个节点不可能同时运行多次。 + 3. 都会返回 resolve,不存在 reject 状态。 + 方案: + - 采用回调的方式,避免深度递归。 + - 使用 activeRunQueue 记录待运行检查的节点(可能可以运行),并控制并发数量。 + - 每次添加新节点,以及节点运行结束后,均会执行一次 processNextNode 方法。 processNextNode 方法,如果没触发跳出条件,则必定会取一个 activeRunQueue 继续检查处理。 + - checkNodeCanRun 会检查该节点状态 + - 没满足运行条件:跳出函数 + - 运行:执行节点逻辑,并返回结果,将 target node 加入到 activeRunQueue 中,等待队列处理。 + - 跳过:执行跳过逻辑,并将其后续的 target node 也进行一次检查。 + */ + class WorkflowQueue { + // Workflow variables + chatResponses: ChatHistoryItemResType[] = []; // response request and save to database + chatAssistantResponse: AIChatItemValueItemType[] = []; // The value will be returned to the user + chatNodeUsages: ChatNodeUsageType[] = []; + toolRunResponse: ToolRunResponseItemType; // Run with tool mode. Result will response to tool node. + debugNextStepRunNodes: RuntimeNodeItemType[] = []; // 记录 Debug 模式下,下一个阶段需要执行的节点。 + // 记录交互节点,交互节点需要在工作流完全结束后再进行计算 + nodeInteractiveResponse: + | { + entryNodeIds: string[]; + interactiveResponse: InteractiveNodeResponseType; + } + | undefined; + system_memories: Record = {}; // Workflow node memories + + // Queue variables + private activeRunQueue = new Set(); + private runningNodeCount = 0; + private maxConcurrency: number; + private resolve: (e: WorkflowQueue) => void; + + constructor({ + maxConcurrency = 10, + resolve + }: { + maxConcurrency?: number; + resolve: (e: WorkflowQueue) => void; + }) { + this.maxConcurrency = maxConcurrency; + this.resolve = resolve; } - if (responseData) { - chatResponses.push(responseData); - } + // Add active node to queue (if already in the queue, it will not be added again) + addActiveNode(nodeId: string) { + if (this.activeRunQueue.has(nodeId)) { + return; + } + this.activeRunQueue.add(nodeId); - if (nodeDispatchUsages) { - chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages); + this.processNextNode(); } - - if (toolResponses !== undefined && toolResponses !== null) { - if (Array.isArray(toolResponses) && toolResponses.length === 0) return; - if ( - !Array.isArray(toolResponses) && - typeof toolResponses === 'object' && - Object.keys(toolResponses).length === 0 - ) + // Process next active node + private processNextNode() { + // Finish + if (this.activeRunQueue.size === 0 && this.runningNodeCount === 0) { + this.resolve(this); return; - toolRunResponse = toolResponses; - } - - // Histories store - if (assistantResponses) { - chatAssistantResponse = chatAssistantResponse.concat(assistantResponses); - } else { - if (reasoningText) { - chatAssistantResponse.push({ - type: ChatItemValueTypeEnum.reasoning, - reasoning: { - content: reasoningText - } - }); } - if (answerText) { - chatAssistantResponse.push({ - type: ChatItemValueTypeEnum.text, - text: { - content: answerText - } - }); - } - } - if (rewriteHistories) { - histories = rewriteHistories; - } - } - /* Pass the output of the node, to get next nodes and update edge status */ - function nodeOutput( - node: RuntimeNodeItemType, - result: NodeResponseCompleteType - ): { - nextStepActiveNodes: RuntimeNodeItemType[]; - nextStepSkipNodes: RuntimeNodeItemType[]; - } { - pushStore(node, result); - - const concatData: Record = { - ...(result.data ?? {}), - ...(result.error ?? {}) - }; + // Over max concurrency(如果 this.activeRunQueue.size === 0 条件触发,代表肯定有节点在运行) + if (this.activeRunQueue.size === 0 || this.runningNodeCount >= this.maxConcurrency) { + return; + } - // Assign the output value to the next node - node.outputs.forEach((outputItem) => { - if (concatData[outputItem.key] === undefined) return; - /* update output value */ - outputItem.value = concatData[outputItem.key]; - }); + const nodeId = this.activeRunQueue.keys().next().value; + const node = nodeId ? runtimeNodesMap.get(nodeId) : undefined; - // Get next source edges and update status - const skipHandleId = result[DispatchNodeResponseKeyEnum.skipHandleId] || []; - const targetEdges = filterWorkflowEdges(runtimeEdges).filter( - (item) => item.source === node.nodeId - ); - - // update edge status - targetEdges.forEach((edge) => { - if (skipHandleId.includes(edge.sourceHandle)) { - edge.status = 'skipped'; - } else { - edge.status = 'active'; + if (nodeId) { + this.activeRunQueue.delete(nodeId); } - }); + if (node) { + this.runningNodeCount++; - const nextStepActiveNodes: RuntimeNodeItemType[] = []; - const nextStepSkipNodes: RuntimeNodeItemType[] = []; - runtimeNodes.forEach((node) => { - if (targetEdges.some((item) => item.target === node.nodeId && item.status === 'active')) { - nextStepActiveNodes.push(node); + this.checkNodeCanRun(node).finally(() => { + this.runningNodeCount--; + this.processNextNode(); + }); } - if (targetEdges.some((item) => item.target === node.nodeId && item.status === 'skipped')) { - nextStepSkipNodes.push(node); + // 兜底,除非极端情况,否则不可能触发 + else { + this.processNextNode(); } - }); - - if (props.mode === 'debug') { - debugNextStepRunNodes = debugNextStepRunNodes.concat( - props.lastInteractive ? nextStepActiveNodes : [...nextStepActiveNodes, ...nextStepSkipNodes] - ); - return { - nextStepActiveNodes: [], - nextStepSkipNodes: [] - }; } - return { - nextStepActiveNodes, - nextStepSkipNodes - }; - } - - /* Have interactive result, computed edges and node outputs */ - function handleInteractiveResult({ - entryNodeIds, - interactiveResponse - }: { - entryNodeIds: string[]; - interactiveResponse: InteractiveNodeResponseType; - }): AIChatItemValueItemType { - // Get node outputs - const nodeOutputs: NodeOutputItemType[] = []; - runtimeNodes.forEach((node) => { - node.outputs.forEach((output) => { - if (output.value) { - nodeOutputs.push({ - nodeId: node.nodeId, - key: output.key as NodeOutputKeyEnum, - value: output.value - }); + async nodeRunWithActive(node: RuntimeNodeItemType): Promise<{ + node: RuntimeNodeItemType; + runStatus: 'run'; + result: NodeResponseCompleteType; + }> { + /* Inject data into module input */ + function getNodeRunParams(node: RuntimeNodeItemType) { + if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) { + // Format plugin input to object + return node.inputs.reduce>((acc, item) => { + acc[item.key] = valueTypeFormat(item.value, item.valueType); + return acc; + }, {}); } - }); - }); - const interactiveResult: WorkflowInteractiveResponseType = { - ...interactiveResponse, - entryNodeIds, - memoryEdges: runtimeEdges.map((edge) => ({ - ...edge, - status: entryNodeIds.includes(edge.target) ? 'active' : edge.status - })), - nodeOutputs - }; + // Dynamic input need to store a key. + const dynamicInput = node.inputs.find( + (item) => item.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam + ); + const params: Record = dynamicInput + ? { + [dynamicInput.key]: {} + } + : {}; + + node.inputs.forEach((input) => { + // Special input, not format + if (input.key === dynamicInput?.key) return; + + // Skip some special key + if ( + [NodeInputKeyEnum.childrenNodeIdList, NodeInputKeyEnum.httpJsonBody].includes( + input.key as NodeInputKeyEnum + ) + ) { + params[input.key] = input.value; + return; + } - // Tool call, not need interactive response - if (!props.isToolCall && isRootRuntime) { - props.workflowStreamResponse?.({ - event: SseResponseEventEnum.interactive, - data: { interactive: interactiveResult } - }); - } + // replace {{$xx.xx$}} and {{xx}} variables + let value = replaceEditorVariable({ + text: input.value, + nodes: runtimeNodes, + variables + }); - return { - type: ChatItemValueTypeEnum.interactive, - interactive: interactiveResult - }; - } + // replace reference variables + value = getReferenceVariableValue({ + value, + nodes: runtimeNodes, + variables + }); - // 每个节点确定 运行/跳过 前,初始化边的状态 - function nodeRunBeforeHook(node: RuntimeNodeItemType) { - runtimeEdges.forEach((item) => { - if (item.target === node.nodeId) { - item.status = 'waiting'; - } - }); - } - /* Check node run/skip or wait */ - async function checkNodeCanRun( - node: RuntimeNodeItemType, - skippedNodeIdList = new Set() - ): Promise { - if (res?.closed || props.maxRunTimes <= 0) return []; - // Thread avoidance - await surrenderProcess(); - - addLog.debug(`Run node`, { maxRunTimes: props.maxRunTimes, appId: props.runningAppInfo.id }); - - // Get node run status by edges - const status = checkNodeRunStatus({ - node, - runtimeEdges - }); + // Dynamic input is stored in the dynamic key + if (input.canEdit && dynamicInput && params[dynamicInput.key]) { + params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType); + } + params[input.key] = valueTypeFormat(value, input.valueType); + }); - const nodeRunResult = await (() => { - if (status === 'run') { - nodeRunBeforeHook(node); - addLog.debug(`[dispatchWorkFlow] nodeRunWithActive: ${node.name}`); - return nodeRunWithActive(node); + return params; } - if (status === 'skip' && !skippedNodeIdList.has(node.nodeId)) { - nodeRunBeforeHook(node); - props.maxRunTimes -= 0.1; - skippedNodeIdList.add(node.nodeId); - addLog.debug(`[dispatchWorkFlow] nodeRunWithSkip: ${node.name}`); - return nodeRunWithSkip(node); - } - })(); - - if (!nodeRunResult) return []; - - /* - 特殊情况: - 通过 skipEdges 可以判断是运行了分支节点。 - 由于分支节点,可能会实现递归调用(skip 连线往前递归) - 需要把分支节点也加入到已跳过的记录里,可以保证递归 skip 运行时,至多只会传递到当前分支节点,不会影响分支后的内容。 - */ - const skipEdges = (nodeRunResult.result[DispatchNodeResponseKeyEnum.skipHandleId] || - []) as string[]; - if (skipEdges && skipEdges?.length > 0) { - skippedNodeIdList.add(node.nodeId); - } - - // In the current version, only one interactive node is allowed at the same time - const interactiveResponse = nodeRunResult.result?.[DispatchNodeResponseKeyEnum.interactive]; - if (interactiveResponse) { - pushStore(nodeRunResult.node, nodeRunResult.result); - if (props.mode === 'debug') { - debugNextStepRunNodes = debugNextStepRunNodes.concat([nodeRunResult.node]); + // push run status messages + if (node.showStatus && !data.isToolCall) { + data.workflowStreamResponse?.({ + event: SseResponseEventEnum.flowNodeStatus, + data: { + status: 'running', + name: node.name + } + }); } + const startTime = Date.now(); + + // get node running params + const params = getNodeRunParams(node); - nodeInteractiveResponse = { - entryNodeIds: [nodeRunResult.node.nodeId], - interactiveResponse + const dispatchData: ModuleDispatchProps> = { + ...data, + variables, + histories, + retainDatasetCite, + node, + runtimeNodes, + runtimeEdges, + params, + mode: data.mode === 'debug' ? 'test' : data.mode }; - return []; - } - // Update the node output at the end of the run and get the next nodes - let { nextStepActiveNodes, nextStepSkipNodes } = nodeOutput( - nodeRunResult.node, - nodeRunResult.result - ); - // Remove repeat nodes(Make sure that the node is only executed once) - nextStepActiveNodes = nextStepActiveNodes.filter( - (node, index, self) => self.findIndex((t) => t.nodeId === node.nodeId) === index - ); - nextStepSkipNodes = nextStepSkipNodes.filter( - (node, index, self) => self.findIndex((t) => t.nodeId === node.nodeId) === index - ); - - // Run next nodes(先运行 run 的,再运行 skip 的) - const nextStepActiveNodesResults = ( - await Promise.all(nextStepActiveNodes.map((node) => checkNodeCanRun(node))) - ).flat(); - - // 如果已经 active 运行过,不再执行 skip(active 中有闭环) - nextStepSkipNodes = nextStepSkipNodes.filter( - (node) => !nextStepActiveNodesResults.some((item) => item.nodeId === node.nodeId) - ); - - const nextStepSkipNodesResults = ( - await Promise.all(nextStepSkipNodes.map((node) => checkNodeCanRun(node, skippedNodeIdList))) - ).flat(); - - if (res?.closed) { - addLog.warn('Request is closed', { - appId: props.runningAppInfo.id, - nodeId: node.nodeId, - nodeName: node.name - }); - return []; - } + // run module + const dispatchRes: NodeResponseType = await (async () => { + if (callbackMap[node.flowNodeType]) { + const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId); + + try { + const result = (await callbackMap[node.flowNodeType](dispatchData)) as NodeResponseType; + const errorHandleId = getHandleId(node.nodeId, 'source_catch', 'right'); + + if (result.error) { + // Run error and not catch error, skip all edges + if (!node.catchError) { + return { + ...result, + [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map( + (item) => item.sourceHandle + ) + }; + } - return [ - ...nextStepActiveNodes, - ...nextStepSkipNodes, - ...nextStepActiveNodesResults, - ...nextStepSkipNodesResults - ]; - } - /* Inject data into module input */ - function getNodeRunParams(node: RuntimeNodeItemType) { - if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) { - // Format plugin input to object - return node.inputs.reduce>((acc, item) => { - acc[item.key] = valueTypeFormat(item.value, item.valueType); - return acc; - }, {}); - } + // Catch error, skip unError handle + const skipHandleIds = targetEdges + .filter((item) => item.sourceHandle !== errorHandleId) + .map((item) => item.sourceHandle); + + return { + ...result, + [DispatchNodeResponseKeyEnum.skipHandleId]: result[ + DispatchNodeResponseKeyEnum.skipHandleId + ] + ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], ...skipHandleIds].filter( + Boolean + ) + : skipHandleIds + }; + } + + // Not error + const errorHandle = + targetEdges.find((item) => item.sourceHandle === errorHandleId)?.sourceHandle || ''; - // Dynamic input need to store a key. - const dynamicInput = node.inputs.find( - (item) => item.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam - ); - const params: Record = dynamicInput - ? { - [dynamicInput.key]: {} + return { + ...result, + [DispatchNodeResponseKeyEnum.skipHandleId]: (result[ + DispatchNodeResponseKeyEnum.skipHandleId + ] + ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], errorHandle] + : [errorHandle] + ).filter(Boolean) + }; + } catch (error) { + // Skip all edges and return error + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + error: getErrText(error) + }, + [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map( + (item) => item.sourceHandle + ) + }; + } } - : {}; - - node.inputs.forEach((input) => { - // Special input, not format - if (input.key === dynamicInput?.key) return; - - // Skip some special key - if ( - [NodeInputKeyEnum.childrenNodeIdList, NodeInputKeyEnum.httpJsonBody].includes( - input.key as NodeInputKeyEnum - ) - ) { - params[input.key] = input.value; - return; + return {}; + })(); + + // format response data. Add modulename and module type + const formatResponseData: NodeResponseCompleteType['responseData'] = (() => { + if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined; + + return { + ...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse], + id: getNanoid(), + nodeId: node.nodeId, + moduleName: node.name, + moduleType: node.flowNodeType, + runningTime: +((Date.now() - startTime) / 1000).toFixed(2) + }; + })(); + + // Response node response + if (version === 'v2' && !data.isToolCall && isRootRuntime && formatResponseData) { + data.workflowStreamResponse?.({ + event: SseResponseEventEnum.flowNodeResponse, + data: responseAllData + ? formatResponseData + : filterPublicNodeResponseData({ + flowResponses: [formatResponseData], + responseDetail + })[0] + }); } - // replace {{$xx.xx$}} and {{xx}} variables - let value = replaceEditorVariable({ - text: input.value, - nodes: runtimeNodes, - variables - }); + // Add output default value + if (dispatchRes.data) { + node.outputs.forEach((item) => { + if (!item.required) return; + if (dispatchRes.data?.[item.key] !== undefined) return; + dispatchRes.data![item.key] = valueTypeFormat(item.defaultValue, item.valueType); + }); + } - // replace reference variables - value = getReferenceVariableValue({ - value, - nodes: runtimeNodes, - variables - }); + // Update new variables + if (dispatchRes[DispatchNodeResponseKeyEnum.newVariables]) { + variables = { + ...variables, + ...dispatchRes[DispatchNodeResponseKeyEnum.newVariables] + }; + } - // Dynamic input is stored in the dynamic key - if (input.canEdit && dynamicInput && params[dynamicInput.key]) { - params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType); + // Error + if (dispatchRes?.responseData?.error) { + addLog.warn('workflow error', { error: dispatchRes.responseData.error }); } - params[input.key] = valueTypeFormat(value, input.valueType); - }); - return params; - } - async function nodeRunWithActive(node: RuntimeNodeItemType): Promise<{ - node: RuntimeNodeItemType; - runStatus: 'run'; - result: NodeResponseCompleteType; - }> { - // push run status messages - if (node.showStatus && !props.isToolCall) { - props.workflowStreamResponse?.({ - event: SseResponseEventEnum.flowNodeStatus, - data: { - status: 'running', - name: node.name + return { + node, + runStatus: 'run', + result: { + ...dispatchRes, + [DispatchNodeResponseKeyEnum.nodeResponse]: formatResponseData } - }); + }; } - const startTime = Date.now(); + private nodeRunWithSkip(node: RuntimeNodeItemType): { + node: RuntimeNodeItemType; + runStatus: 'skip'; + result: NodeResponseCompleteType; + } { + // Set target edges status to skipped + const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId); - // get node running params - const params = getNodeRunParams(node); + return { + node, + runStatus: 'skip', + result: { + [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map((item) => item.sourceHandle) + } + }; + } + /* Check node run/skip or wait */ + private async checkNodeCanRun( + node: RuntimeNodeItemType, + skippedNodeIdList = new Set() + ) { + /* Store special response field */ + const pushStore = ({ + answerText, + reasoningText, + responseData, + nodeDispatchUsages, + toolResponses, + assistantResponses, + rewriteHistories, + runTimes = 1, + system_memories: newMemories + }: NodeResponseCompleteType) => { + // Add run times + workflowRunTimes += runTimes; + data.maxRunTimes -= runTimes; + + if (newMemories) { + this.system_memories = { + ...this.system_memories, + ...newMemories + }; + } - const dispatchData: ModuleDispatchProps> = { - ...props, - res, - variables, - histories, - timezone, - externalProvider, - stream, - retainDatasetCite, - node, - runtimeNodes, - runtimeEdges, - params, - mode: props.mode === 'debug' ? 'test' : props.mode - }; + if (responseData) { + this.chatResponses.push(responseData); + } - // run module - const dispatchRes: NodeResponseType = await (async () => { - if (callbackMap[node.flowNodeType]) { - const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId); + if (nodeDispatchUsages) { + this.chatNodeUsages = this.chatNodeUsages.concat(nodeDispatchUsages); + } - try { - const result = (await callbackMap[node.flowNodeType](dispatchData)) as NodeResponseType; - const errorHandleId = getHandleId(node.nodeId, 'source_catch', 'right'); + if (toolResponses !== undefined && toolResponses !== null) { + if (Array.isArray(toolResponses) && toolResponses.length === 0) return; + if ( + !Array.isArray(toolResponses) && + typeof toolResponses === 'object' && + Object.keys(toolResponses).length === 0 + ) + return; + this.toolRunResponse = toolResponses; + } - if (!result.error) { - const skipHandleId = - targetEdges.find((item) => item.sourceHandle === errorHandleId)?.sourceHandle || ''; + // Histories store + if (assistantResponses) { + this.chatAssistantResponse = this.chatAssistantResponse.concat(assistantResponses); + } else { + if (reasoningText) { + this.chatAssistantResponse.push({ + type: ChatItemValueTypeEnum.reasoning, + reasoning: { + content: reasoningText + } + }); + } + if (answerText) { + this.chatAssistantResponse.push({ + type: ChatItemValueTypeEnum.text, + text: { + content: answerText + } + }); + } + } - return { - ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: (result[ - DispatchNodeResponseKeyEnum.skipHandleId - ] - ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], skipHandleId] - : [skipHandleId] - ).filter(Boolean) - }; + if (rewriteHistories) { + histories = rewriteHistories; + } + }; + /* Pass the output of the node, to get next nodes and update edge status */ + const nodeOutput = ( + node: RuntimeNodeItemType, + result: NodeResponseCompleteType + ): { + nextStepActiveNodes: RuntimeNodeItemType[]; + nextStepSkipNodes: RuntimeNodeItemType[]; + } => { + pushStore(result); + + const concatData: Record = { + ...(result.data ?? {}), + ...(result.error ?? {}) + }; + + // Assign the output value to the next node + node.outputs.forEach((outputItem) => { + if (concatData[outputItem.key] === undefined) return; + /* update output value */ + outputItem.value = concatData[outputItem.key]; + }); + + // Get next source edges and update status + const skipHandleId = result[DispatchNodeResponseKeyEnum.skipHandleId] || []; + const targetEdges = filterWorkflowEdges(runtimeEdges).filter( + (item) => item.source === node.nodeId + ); + + // update edge status + targetEdges.forEach((edge) => { + if (skipHandleId.includes(edge.sourceHandle)) { + edge.status = 'skipped'; + } else { + edge.status = 'active'; } + }); - // Run error and not catch error, skip all edges - if (!node.catchError) { - return { - ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map( - (item) => item.sourceHandle - ) - }; + // 同时可以去重 + const nextStepActiveNodesMap = new Map(); + const nextStepSkipNodesMap = new Map(); + runtimeNodes.forEach((node) => { + if (targetEdges.some((item) => item.target === node.nodeId && item.status === 'active')) { + nextStepActiveNodesMap.set(node.nodeId, node); } + if ( + targetEdges.some((item) => item.target === node.nodeId && item.status === 'skipped') + ) { + nextStepSkipNodesMap.set(node.nodeId, node); + } + }); - // Catch error - const skipHandleIds = targetEdges - .filter((item) => { - if (node.catchError) { - return item.sourceHandle !== errorHandleId; - } - return true; - }) - .map((item) => item.sourceHandle); + const nextStepActiveNodes = Array.from(nextStepActiveNodesMap.values()); + const nextStepSkipNodes = Array.from(nextStepSkipNodesMap.values()); + if (data.mode === 'debug') { + this.debugNextStepRunNodes = this.debugNextStepRunNodes.concat( + data.lastInteractive + ? nextStepActiveNodes + : [...nextStepActiveNodes, ...nextStepSkipNodes] + ); return { - ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: result[ - DispatchNodeResponseKeyEnum.skipHandleId - ] - ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], ...skipHandleIds].filter( - Boolean - ) - : skipHandleIds - }; - } catch (error) { - // Skip all edges and return error - return { - [DispatchNodeResponseKeyEnum.nodeResponse]: { - error: getErrText(error) - }, - [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map((item) => item.sourceHandle) + nextStepActiveNodes: [], + nextStepSkipNodes: [] }; } + + return { + nextStepActiveNodes, + nextStepSkipNodes + }; + }; + + if (data.maxRunTimes <= 0) { + addLog.error('Max run times is 0', { + appId: data.runningAppInfo.id + }); + return; + } + if (res?.closed) { + addLog.warn('Request is closed', { + appId: data.runningAppInfo.id, + nodeId: node.nodeId, + nodeName: node.name + }); + return; } - return {}; - })(); - // format response data. Add modulename and module type - const formatResponseData: NodeResponseCompleteType['responseData'] = (() => { - if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined; + // Thread avoidance + await surrenderProcess(); - return { - ...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse], - id: getNanoid(), - nodeId: node.nodeId, - moduleName: node.name, - moduleType: node.flowNodeType, - runningTime: +((Date.now() - startTime) / 1000).toFixed(2) - }; - })(); - - // Response node response - if (version === 'v2' && !props.isToolCall && isRootRuntime && formatResponseData) { - props.workflowStreamResponse?.({ - event: SseResponseEventEnum.flowNodeResponse, - data: responseAllData - ? formatResponseData - : filterPublicNodeResponseData({ - flowResponses: [formatResponseData], - responseDetail - })[0] + addLog.debug(`Run node`, { maxRunTimes: data.maxRunTimes, appId: data.runningAppInfo.id }); + + // Get node run status by edges + const status = checkNodeRunStatus({ + nodesMap: runtimeNodesMap, + node, + runtimeEdges }); - } + const nodeRunResult = await (() => { + if (status === 'run') { + // All source edges status to waiting + runtimeEdges.forEach((item) => { + if (item.target === node.nodeId) { + item.status = 'waiting'; + } + }); + + addLog.debug(`[dispatchWorkFlow] nodeRunWithActive: ${node.name}`); + return this.nodeRunWithActive(node); + } + if (status === 'skip' && !skippedNodeIdList.has(node.nodeId)) { + // All skip source edges status to waiting + runtimeEdges.forEach((item) => { + if (item.target === node.nodeId) { + item.status = 'waiting'; + } + }); + + data.maxRunTimes -= 0.1; + skippedNodeIdList.add(node.nodeId); + addLog.debug(`[dispatchWorkFlow] nodeRunWithSkip: ${node.name}`); + return this.nodeRunWithSkip(node); + } + })(); + if (!nodeRunResult) return; + + /* + 特殊情况: + 通过 skipEdges 可以判断是运行了分支节点。 + 由于分支节点,可能会实现递归调用(skip 连线往前递归) + 需要把分支节点也加入到已跳过的记录里,可以保证递归 skip 运行时,至多只会传递到当前分支节点,不会影响分支后的内容。 + */ + const skipEdges = (nodeRunResult.result[DispatchNodeResponseKeyEnum.skipHandleId] || + []) as string[]; + if (skipEdges && skipEdges?.length > 0) { + skippedNodeIdList.add(node.nodeId); + } + + // In the current version, only one interactive node is allowed at the same time + const interactiveResponse = nodeRunResult.result?.[DispatchNodeResponseKeyEnum.interactive]; + if (interactiveResponse) { + pushStore(nodeRunResult.result); - // Add output default value - if (dispatchRes.data) { - node.outputs.forEach((item) => { - if (!item.required) return; - if (dispatchRes.data?.[item.key] !== undefined) return; - dispatchRes.data![item.key] = valueTypeFormat(item.defaultValue, item.valueType); + if (data.mode === 'debug') { + this.debugNextStepRunNodes = this.debugNextStepRunNodes.concat([nodeRunResult.node]); + } + + this.nodeInteractiveResponse = { + entryNodeIds: [nodeRunResult.node.nodeId], + interactiveResponse + }; + return; + } + + // Update the node output at the end of the run and get the next nodes + const { nextStepActiveNodes, nextStepSkipNodes } = nodeOutput( + nodeRunResult.node, + nodeRunResult.result + ); + + await Promise.all( + nextStepSkipNodes.map((node) => this.checkNodeCanRun(node, skippedNodeIdList)) + ); + + // Run next nodes + nextStepActiveNodes.forEach((node) => { + this.addActiveNode(node.nodeId); }); } - // Update new variables - if (dispatchRes[DispatchNodeResponseKeyEnum.newVariables]) { - variables = { - ...variables, - ...dispatchRes[DispatchNodeResponseKeyEnum.newVariables] - }; - } + /* Have interactive result, computed edges and node outputs */ + handleInteractiveResult({ + entryNodeIds, + interactiveResponse + }: { + entryNodeIds: string[]; + interactiveResponse: InteractiveNodeResponseType; + }): AIChatItemValueItemType { + // Get node outputs + const nodeOutputs: NodeOutputItemType[] = []; + runtimeNodes.forEach((node) => { + node.outputs.forEach((output) => { + if (output.value) { + nodeOutputs.push({ + nodeId: node.nodeId, + key: output.key as NodeOutputKeyEnum, + value: output.value + }); + } + }); + }); - // Error - if (dispatchRes?.responseData?.error) { - addLog.warn('workflow error', { error: dispatchRes.responseData.error }); - } + const interactiveResult: WorkflowInteractiveResponseType = { + ...interactiveResponse, + entryNodeIds, + memoryEdges: runtimeEdges.map((edge) => ({ + ...edge, + status: entryNodeIds.includes(edge.target) ? 'active' : edge.status + })), + nodeOutputs + }; - return { - node, - runStatus: 'run', - result: { - ...dispatchRes, - [DispatchNodeResponseKeyEnum.nodeResponse]: formatResponseData + // Tool call, not need interactive response + if (!data.isToolCall && isRootRuntime) { + data.workflowStreamResponse?.({ + event: SseResponseEventEnum.interactive, + data: { interactive: interactiveResult } + }); } - }; - } - async function nodeRunWithSkip(node: RuntimeNodeItemType): Promise<{ - node: RuntimeNodeItemType; - runStatus: 'skip'; - result: NodeResponseCompleteType; - }> { - // Set target edges status to skipped - const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId); - return { - node, - runStatus: 'skip', - result: { - [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map((item) => item.sourceHandle) - } - }; + return { + type: ChatItemValueTypeEnum.interactive, + interactive: interactiveResult + }; + } } - try { - // start process width initInput - const entryNodes = runtimeNodes.filter((item) => item.isEntry); - // reset entry - runtimeNodes.forEach((item) => { - // Interactively nodes will use the "isEntry", which does not need to be updated - if ( - item.flowNodeType !== FlowNodeTypeEnum.userSelect && - item.flowNodeType !== FlowNodeTypeEnum.formInput && - item.flowNodeType !== FlowNodeTypeEnum.agent - ) { - item.isEntry = false; - } - }); - await Promise.all(entryNodes.map((node) => checkNodeCanRun(node))); - - // focus try to run pluginOutput - const pluginOutputModule = runtimeNodes.find( - (item) => item.flowNodeType === FlowNodeTypeEnum.pluginOutput - ); - if (pluginOutputModule && props.mode !== 'debug') { - await nodeRunWithActive(pluginOutputModule); + // Start process width initInput + const entryNodes = runtimeNodes.filter((item) => item.isEntry); + // Reset entry + runtimeNodes.forEach((item) => { + // Interactively nodes will use the "isEntry", which does not need to be updated + if ( + item.flowNodeType !== FlowNodeTypeEnum.userSelect && + item.flowNodeType !== FlowNodeTypeEnum.formInput && + item.flowNodeType !== FlowNodeTypeEnum.agent + ) { + item.isEntry = false; } + }); - // Interactive node - const interactiveResult = (() => { - if (nodeInteractiveResponse) { - const interactiveAssistant = handleInteractiveResult({ - entryNodeIds: nodeInteractiveResponse.entryNodeIds, - interactiveResponse: nodeInteractiveResponse.interactiveResponse - }); - if (isRootRuntime) { - chatAssistantResponse.push(interactiveAssistant); - } - return interactiveAssistant.interactive; - } - })(); + const workflowQueue = await new Promise((resolve) => { + const workflowQueue = new WorkflowQueue({ + resolve + }); - const durationSeconds = +((Date.now() - startTime) / 1000).toFixed(2); + entryNodes.forEach((node) => { + workflowQueue.addActiveNode(node.nodeId); + }); + }); + + // Focus try to run pluginOutput + const pluginOutputModule = runtimeNodes.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.pluginOutput + ); + if (pluginOutputModule && data.mode !== 'debug') { + workflowQueue.nodeRunWithActive(pluginOutputModule); + } - if (isRootRuntime && stream) { - props.workflowStreamResponse?.({ - event: SseResponseEventEnum.workflowDuration, - data: { durationSeconds } + // Get interactive node response. + const interactiveResult = (() => { + if (workflowQueue.nodeInteractiveResponse) { + const interactiveAssistant = workflowQueue.handleInteractiveResult({ + entryNodeIds: workflowQueue.nodeInteractiveResponse.entryNodeIds, + interactiveResponse: workflowQueue.nodeInteractiveResponse.interactiveResponse }); + if (isRootRuntime) { + workflowQueue.chatAssistantResponse.push(interactiveAssistant); + } + return interactiveAssistant.interactive; } + })(); - return { - flowResponses: chatResponses, - flowUsages: chatNodeUsages, - debugResponse: { - finishedNodes: runtimeNodes, - finishedEdges: runtimeEdges, - nextStepRunNodes: debugNextStepRunNodes - }, - workflowInteractiveResponse: interactiveResult, - [DispatchNodeResponseKeyEnum.runTimes]: workflowRunTimes, - [DispatchNodeResponseKeyEnum.assistantResponses]: - mergeAssistantResponseAnswerText(chatAssistantResponse), - [DispatchNodeResponseKeyEnum.toolResponses]: toolRunResponse, - [DispatchNodeResponseKeyEnum.newVariables]: removeSystemVariable( - variables, - externalProvider.externalWorkflowVariables - ), - [DispatchNodeResponseKeyEnum.memories]: - Object.keys(system_memories).length > 0 ? system_memories : undefined, - durationSeconds - }; - } catch (error) { - return Promise.reject(error); - } finally { - if (streamCheckTimer) { - clearInterval(streamCheckTimer); - } + const durationSeconds = +((Date.now() - startTime) / 1000).toFixed(2); + + if (isRootRuntime) { + data.workflowStreamResponse?.({ + event: SseResponseEventEnum.workflowDuration, + data: { durationSeconds } + }); } + + if (streamCheckTimer) { + clearInterval(streamCheckTimer); + } + + return { + flowResponses: workflowQueue.chatResponses, + flowUsages: workflowQueue.chatNodeUsages, + debugResponse: { + finishedNodes: runtimeNodes, + finishedEdges: runtimeEdges, + nextStepRunNodes: workflowQueue.debugNextStepRunNodes + }, + workflowInteractiveResponse: interactiveResult, + [DispatchNodeResponseKeyEnum.runTimes]: workflowRunTimes, + [DispatchNodeResponseKeyEnum.assistantResponses]: mergeAssistantResponseAnswerText( + workflowQueue.chatAssistantResponse + ), + [DispatchNodeResponseKeyEnum.toolResponses]: workflowQueue.toolRunResponse, + [DispatchNodeResponseKeyEnum.newVariables]: removeSystemVariable( + variables, + externalProvider.externalWorkflowVariables + ), + [DispatchNodeResponseKeyEnum.memories]: + Object.keys(workflowQueue.system_memories).length > 0 + ? workflowQueue.system_memories + : undefined, + durationSeconds + }; } /* get system variable */ diff --git a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts index afe87d6e5eb1..c06a61c6a2b6 100644 --- a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts +++ b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts @@ -45,8 +45,7 @@ export const dispatchQueryExtension = async ({ const { totalPoints, modelName } = formatModelChars2Points({ model: queryExtensionModel.model, inputTokens, - outputTokens, - modelType: ModelTypeEnum.llm + outputTokens }); const set = new Set(); diff --git a/packages/service/package.json b/packages/service/package.json index 120edf9f4f97..f4523040c46f 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "type": "module", "dependencies": { - "@fastgpt-sdk/plugin": "^0.1.9", "@fastgpt/global": "workspace:*", "@modelcontextprotocol/sdk": "^1.12.1", "@node-rs/jieba": "2.0.1", diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index c4027a5e7d64..df683b8b8033 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -88,6 +88,13 @@ export const authAppByTmbId = async ({ }; } + if (app.favourite || app.quick) { + return { + ...app, + permission: new AppPermission({ isOwner: false, role: ReadRoleVal }) + }; + } + const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); const { Per } = await (async () => { diff --git a/packages/service/support/wallet/usage/controller.ts b/packages/service/support/wallet/usage/controller.ts index c8eed35cd59e..1fa22ba3de6e 100644 --- a/packages/service/support/wallet/usage/controller.ts +++ b/packages/service/support/wallet/usage/controller.ts @@ -218,7 +218,6 @@ export const pushLLMTrainingUsage = async ({ // Compute points const { totalPoints } = formatModelChars2Points({ model, - modelType: ModelTypeEnum.llm, inputTokens, outputTokens }); diff --git a/packages/service/support/wallet/usage/utils.ts b/packages/service/support/wallet/usage/utils.ts index 81b01f33bdc9..55366a4280a6 100644 --- a/packages/service/support/wallet/usage/utils.ts +++ b/packages/service/support/wallet/usage/utils.ts @@ -1,17 +1,14 @@ import { findAIModel } from '../../../core/ai/model'; -import type { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; export const formatModelChars2Points = ({ model, inputTokens = 0, outputTokens = 0, - modelType, multiple = 1000 }: { model: string; inputTokens?: number; outputTokens?: number; - modelType: `${ModelTypeEnum}`; multiple?: number; }) => { const modelData = findAIModel(model); diff --git a/packages/service/thirdProvider/doc2x/index.ts b/packages/service/thirdProvider/doc2x/index.ts index b58de5654265..1d0d9c8dab67 100644 --- a/packages/service/thirdProvider/doc2x/index.ts +++ b/packages/service/thirdProvider/doc2x/index.ts @@ -86,11 +86,11 @@ export const useDoc2xServer = ({ apiKey }: { apiKey: string }) => { const uid = preupload_data.uid; // 2. Upload file to pre-signed URL with binary stream - const blob = new Blob([fileBuffer], { type: 'application/pdf' }); const response = await axios - .put(upload_url, blob, { + .put(upload_url, fileBuffer, { headers: { - 'Content-Type': 'application/pdf' + 'Content-Type': 'application/pdf', + 'Content-Length': fileBuffer.length.toString() } }) .catch((error) => { diff --git a/packages/service/thirdProvider/fastgptPlugin/index.ts b/packages/service/thirdProvider/fastgptPlugin/index.ts index 3c6ab5127d7a..e720eaeb9a23 100644 --- a/packages/service/thirdProvider/fastgptPlugin/index.ts +++ b/packages/service/thirdProvider/fastgptPlugin/index.ts @@ -1,4 +1,4 @@ -import createClient from '@fastgpt-sdk/plugin'; +import { createClient } from '@fastgpt/global/sdk/fastgpt-plugin'; export const BASE_URL = process.env.PLUGIN_BASE_URL || ''; export const TOKEN = process.env.PLUGIN_TOKEN || ''; diff --git a/packages/web/components/common/DndDrag/index.tsx b/packages/web/components/common/DndDrag/index.tsx index 3ed4f4fc5602..153d358a929e 100644 --- a/packages/web/components/common/DndDrag/index.tsx +++ b/packages/web/components/common/DndDrag/index.tsx @@ -23,9 +23,17 @@ type Props = { }) => ReactElement; dataList: T[]; zoom?: number; + renderInnerPlaceholder?: boolean; }; -function DndDrag({ children, renderClone, onDragEndCb, dataList, zoom = 1 }: Props) { +function DndDrag({ + children, + renderClone, + onDragEndCb, + dataList, + zoom = 1, + renderInnerPlaceholder = true +}: Props) { const [draggingItemHeight, setDraggingItemHeight] = useState(0); const onDragStart = (start: DragStart) => { @@ -55,7 +63,9 @@ function DndDrag({ children, renderClone, onDragEndCb, dataList, zoom = 1 }: {(provided, snapshot) => ( <> {children({ provided, snapshot })} - {snapshot.isDraggingOver && } + {snapshot.isDraggingOver && renderInnerPlaceholder && ( + + )} )} diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 19a733fd1baf..25a943a179cf 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -214,6 +214,7 @@ export const iconPaths = { 'core/chat/sidebar/home': () => import('./icons/core/chat/sidebar/home.svg'), 'core/chat/sidebar/logout': () => import('./icons/core/chat/sidebar/logout.svg'), 'core/chat/sidebar/menu': () => import('./icons/core/chat/sidebar/menu.svg'), + 'core/chat/sidebar/star': () => import('./icons/core/chat/sidebar/star.svg'), 'core/chat/speaking': () => import('./icons/core/chat/speaking.svg'), 'core/chat/stopSpeech': () => import('./icons/core/chat/stopSpeech.svg'), 'core/chat/think': () => import('./icons/core/chat/think.svg'), @@ -413,44 +414,11 @@ export const iconPaths = { 'modal/selectSource': () => import('./icons/modal/selectSource.svg'), 'modal/setting': () => import('./icons/modal/setting.svg'), 'modal/teamPlans': () => import('./icons/modal/teamPlans.svg'), - 'model/BAAI': () => import('./icons/model/BAAI.svg'), - 'model/ai360': () => import('./icons/model/ai360.svg'), - 'model/alicloud': () => import('./icons/model/alicloud.svg'), 'model/aws': () => import('./icons/model/aws.svg'), 'model/azure': () => import('./icons/model/azure.svg'), - 'model/baichuan': () => import('./icons/model/baichuan.svg'), - 'model/chatglm': () => import('./icons/model/chatglm.svg'), - 'model/claude': () => import('./icons/model/claude.svg'), 'model/cloudflare': () => import('./icons/model/cloudflare.svg'), 'model/cohere': () => import('./icons/model/cohere.svg'), 'model/coze': () => import('./icons/model/coze.svg'), - 'model/deepseek': () => import('./icons/model/deepseek.svg'), - 'model/doubao': () => import('./icons/model/doubao.svg'), - 'model/ernie': () => import('./icons/model/ernie.svg'), - 'model/fishaudio': () => import('./icons/model/fishaudio.svg'), - 'model/gemini': () => import('./icons/model/gemini.svg'), - 'model/grok': () => import('./icons/model/grok.svg'), - 'model/groq': () => import('./icons/model/groq.svg'), - 'model/huggingface': () => import('./icons/model/huggingface.svg'), - 'model/hunyuan': () => import('./icons/model/hunyuan.svg'), - 'model/intern': () => import('./icons/model/intern.svg'), - 'model/jina': () => import('./icons/model/jina.svg'), - 'model/meta': () => import('./icons/model/meta.svg'), - 'model/minimax': () => import('./icons/model/minimax.svg'), - 'model/mistral': () => import('./icons/model/mistral.svg'), - 'model/moka': () => import('./icons/model/moka.svg'), - 'model/moonshot': () => import('./icons/model/moonshot.svg'), - 'model/novita': () => import('./icons/model/novita.svg'), - 'model/ollama': () => import('./icons/model/ollama.svg'), - 'model/openai': () => import('./icons/model/openai.svg'), - 'model/openrouter': () => import('./icons/model/openrouter.svg'), - 'model/ppio': () => import('./icons/model/ppio.svg'), - 'model/qwen': () => import('./icons/model/qwen.svg'), - 'model/siliconflow': () => import('./icons/model/siliconflow.svg'), - 'model/sparkDesk': () => import('./icons/model/sparkDesk.svg'), - 'model/stepfun': () => import('./icons/model/stepfun.svg'), - 'model/vertexai': () => import('./icons/model/vertexai.svg'), - 'model/yi': () => import('./icons/model/yi.svg'), more: () => import('./icons/more.svg'), moreLine: () => import('./icons/moreLine.svg'), optimizer: () => import('./icons/optimizer.svg'), diff --git a/packages/web/components/common/Icon/icons/core/chat/sidebar/star.svg b/packages/web/components/common/Icon/icons/core/chat/sidebar/star.svg new file mode 100644 index 000000000000..fb4b2a1e21eb --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/sidebar/star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/components/common/Icon/icons/model/BAAI.svg b/packages/web/components/common/Icon/icons/model/BAAI.svg deleted file mode 100644 index 938361b679ac..000000000000 --- a/packages/web/components/common/Icon/icons/model/BAAI.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/components/common/Icon/icons/model/ai360.svg b/packages/web/components/common/Icon/icons/model/ai360.svg deleted file mode 100644 index 2a1e861556d5..000000000000 --- a/packages/web/components/common/Icon/icons/model/ai360.svg +++ /dev/null @@ -1 +0,0 @@ -AI360 \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/alicloud.svg b/packages/web/components/common/Icon/icons/model/alicloud.svg deleted file mode 100644 index 4a6104a705f8..000000000000 --- a/packages/web/components/common/Icon/icons/model/alicloud.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/baichuan.svg b/packages/web/components/common/Icon/icons/model/baichuan.svg deleted file mode 100644 index f16848e0acc6..000000000000 --- a/packages/web/components/common/Icon/icons/model/baichuan.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/chatglm.svg b/packages/web/components/common/Icon/icons/model/chatglm.svg deleted file mode 100644 index eee824cb8490..000000000000 --- a/packages/web/components/common/Icon/icons/model/chatglm.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/claude.svg b/packages/web/components/common/Icon/icons/model/claude.svg deleted file mode 100644 index 95e714e81894..000000000000 --- a/packages/web/components/common/Icon/icons/model/claude.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/deepseek.svg b/packages/web/components/common/Icon/icons/model/deepseek.svg deleted file mode 100644 index c424cfc22fc9..000000000000 --- a/packages/web/components/common/Icon/icons/model/deepseek.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/doubao.svg b/packages/web/components/common/Icon/icons/model/doubao.svg deleted file mode 100644 index f964a1a268f2..000000000000 --- a/packages/web/components/common/Icon/icons/model/doubao.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/ernie.svg b/packages/web/components/common/Icon/icons/model/ernie.svg deleted file mode 100644 index ca91863f6442..000000000000 --- a/packages/web/components/common/Icon/icons/model/ernie.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/fishaudio.svg b/packages/web/components/common/Icon/icons/model/fishaudio.svg deleted file mode 100644 index d40f0febdf55..000000000000 --- a/packages/web/components/common/Icon/icons/model/fishaudio.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/web/components/common/Icon/icons/model/gemini.svg b/packages/web/components/common/Icon/icons/model/gemini.svg deleted file mode 100644 index b1943889eaad..000000000000 --- a/packages/web/components/common/Icon/icons/model/gemini.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/grok.svg b/packages/web/components/common/Icon/icons/model/grok.svg deleted file mode 100644 index c32cf6c9f764..000000000000 --- a/packages/web/components/common/Icon/icons/model/grok.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/groq.svg b/packages/web/components/common/Icon/icons/model/groq.svg deleted file mode 100644 index 4b3f1c50dbd8..000000000000 --- a/packages/web/components/common/Icon/icons/model/groq.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/huggingface.svg b/packages/web/components/common/Icon/icons/model/huggingface.svg deleted file mode 100644 index d36954208495..000000000000 --- a/packages/web/components/common/Icon/icons/model/huggingface.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/hunyuan.svg b/packages/web/components/common/Icon/icons/model/hunyuan.svg deleted file mode 100644 index ec2157e285d5..000000000000 --- a/packages/web/components/common/Icon/icons/model/hunyuan.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/intern.svg b/packages/web/components/common/Icon/icons/model/intern.svg deleted file mode 100644 index 20f5b0e66acb..000000000000 --- a/packages/web/components/common/Icon/icons/model/intern.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/web/components/common/Icon/icons/model/jina.svg b/packages/web/components/common/Icon/icons/model/jina.svg deleted file mode 100644 index fc996bf9055f..000000000000 --- a/packages/web/components/common/Icon/icons/model/jina.svg +++ /dev/null @@ -1 +0,0 @@ -Jina \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/meta.svg b/packages/web/components/common/Icon/icons/model/meta.svg deleted file mode 100644 index f9e96bbeb16d..000000000000 --- a/packages/web/components/common/Icon/icons/model/meta.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/web/components/common/Icon/icons/model/minimax.svg b/packages/web/components/common/Icon/icons/model/minimax.svg deleted file mode 100644 index c80236e5e006..000000000000 --- a/packages/web/components/common/Icon/icons/model/minimax.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/mistral.svg b/packages/web/components/common/Icon/icons/model/mistral.svg deleted file mode 100644 index 735981b9146f..000000000000 --- a/packages/web/components/common/Icon/icons/model/mistral.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/moka.svg b/packages/web/components/common/Icon/icons/model/moka.svg deleted file mode 100644 index e3d50bcb7602..000000000000 --- a/packages/web/components/common/Icon/icons/model/moka.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - Group - Created with Sketch. - - Layer 1 - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/moonshot.svg b/packages/web/components/common/Icon/icons/model/moonshot.svg deleted file mode 100644 index aa4e41e20244..000000000000 --- a/packages/web/components/common/Icon/icons/model/moonshot.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/novita.svg b/packages/web/components/common/Icon/icons/model/novita.svg deleted file mode 100644 index 0658ce0f0925..000000000000 --- a/packages/web/components/common/Icon/icons/model/novita.svg +++ /dev/null @@ -1 +0,0 @@ -Novita AI \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/ollama.svg b/packages/web/components/common/Icon/icons/model/ollama.svg deleted file mode 100644 index 74de384a330e..000000000000 --- a/packages/web/components/common/Icon/icons/model/ollama.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/openai.svg b/packages/web/components/common/Icon/icons/model/openai.svg deleted file mode 100644 index eeca4abc23a0..000000000000 --- a/packages/web/components/common/Icon/icons/model/openai.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/openrouter.svg b/packages/web/components/common/Icon/icons/model/openrouter.svg deleted file mode 100644 index c1237c133dbb..000000000000 --- a/packages/web/components/common/Icon/icons/model/openrouter.svg +++ /dev/null @@ -1 +0,0 @@ -OpenRouter \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/ppio.svg b/packages/web/components/common/Icon/icons/model/ppio.svg deleted file mode 100644 index 87703049224a..000000000000 --- a/packages/web/components/common/Icon/icons/model/ppio.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/components/common/Icon/icons/model/qwen.svg b/packages/web/components/common/Icon/icons/model/qwen.svg deleted file mode 100644 index c46b8f052017..000000000000 --- a/packages/web/components/common/Icon/icons/model/qwen.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/siliconflow.svg b/packages/web/components/common/Icon/icons/model/siliconflow.svg deleted file mode 100644 index b11c61059243..000000000000 --- a/packages/web/components/common/Icon/icons/model/siliconflow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/sparkDesk.svg b/packages/web/components/common/Icon/icons/model/sparkDesk.svg deleted file mode 100644 index 3d9a934f1603..000000000000 --- a/packages/web/components/common/Icon/icons/model/sparkDesk.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/stepfun.svg b/packages/web/components/common/Icon/icons/model/stepfun.svg deleted file mode 100644 index 8b7fd0721286..000000000000 --- a/packages/web/components/common/Icon/icons/model/stepfun.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/vertexai.svg b/packages/web/components/common/Icon/icons/model/vertexai.svg deleted file mode 100644 index e721368de19b..000000000000 --- a/packages/web/components/common/Icon/icons/model/vertexai.svg +++ /dev/null @@ -1 +0,0 @@ -VertexAI \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/yi.svg b/packages/web/components/common/Icon/icons/model/yi.svg deleted file mode 100644 index bb030d7b0bc2..000000000000 --- a/packages/web/components/common/Icon/icons/model/yi.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/web/hooks/useSafeTranslation.ts b/packages/web/hooks/useSafeTranslation.ts index a3f90efa7e8a..fa255b6faa8e 100644 --- a/packages/web/hooks/useSafeTranslation.ts +++ b/packages/web/hooks/useSafeTranslation.ts @@ -4,7 +4,9 @@ import { I18N_NAMESPACES_MAP } from '../i18n/constants'; export function useSafeTranslation() { const { t: originalT, ...rest } = useNextTranslation(); - const t = (key: string | undefined, ...args: any[]): string => { + const t = (key: any, ...args: any[]): string => { + if (key === null || key === undefined) return ''; + if (typeof key !== 'string') return String(key); if (!key) return ''; const ns = key.split(':')[0]; diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index 5940a4fb5bb9..e0b5e51b8b81 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -98,16 +98,49 @@ "setting.copyright.tips.square": "Suggested ratio 1:1", "setting.copyright.title": "Copyright", "setting.copyright.upload_fail": "File upload failed", - "setting.data_dashboard.title": "Data board", + "setting.data_dashboard.title": "Home data", "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram_en.png", + "setting.favourite.add_new_app": "Add an app", + "setting.favourite.cancel_button": "Cancel", + "setting.favourite.categories_modal.delete_cancel_button": "Cancel", + "setting.favourite.categories_modal.delete_confirm": "Confirm deletion {{name}}? \nApps under this category will be moved to default", + "setting.favourite.categories_modal.delete_confirm_button": "delete", + "setting.favourite.categories_modal.delete_confirm_title": "Confirm deletion", + "setting.favourite.categories_modal.title": "Total {{num}} categories", + "setting.favourite.category.no_data": "No selected applications available", + "setting.favourite.category_all": "All Categories", + "setting.favourite.category_placeholder": "Select a category", + "setting.favourite.category_tab.all": "All", + "setting.favourite.confirm_button": "Sure", + "setting.favourite.delete_app_cancel_button": "Cancel", + "setting.favourite.delete_app_confirm": "Are you sure you want to remove this featured app?", + "setting.favourite.delete_app_confirm_button": "Sure", + "setting.favourite.delete_app_title": "Delete the app", + "setting.favourite.manage_categories_button": "Manage Category", + "setting.favourite.save_category_for_app_button": "Save", + "setting.favourite.search_placeholder": "Search for apps", + "setting.favourite.selected_list": "Selected: {{num}}", + "setting.favourite.table_column_action": "Action", + "setting.favourite.table_column_category": "Category", + "setting.favourite.table_column_intro": "Introduce", + "setting.favourite.table_column_name": "Name", + "setting.favourite.tag.no_data": "No classification yet", + "setting.favourite.title": "Favourite App", + "setting.home.available_tools": "Available tools", "setting.home.available_tools.add": "Add", + "setting.home.cancel_button": "Cancel", "setting.home.commercial_version": "Commercial version", + "setting.home.confirm_button": "Sure", "setting.home.diagram": "Schematic diagram", "setting.home.dialogue_tips": "Dialog prompt text", "setting.home.dialogue_tips.default": "You can ask me any questions", "setting.home.dialogue_tips_placeholder": "Please enter the prompt text of the dialog box", "setting.home.home_tab_title": "Home Page Title", "setting.home.home_tab_title_placeholder": "Please enter the title of the homepage", + "setting.home.no_selected_app": "No selected App", + "setting.home.quick_apps": "Quick Apps", + "setting.home.quick_apps.add": "Configure quick applications", + "setting.home.quick_apps.placeholder": "Please select an application", "setting.home.slogan": "Slogan", "setting.home.slogan.default": "Hello 👋, I am FastGPT! Is there anything I can help you?", "setting.home.slogan_placeholder": "Please enter Slogan", @@ -115,9 +148,9 @@ "setting.incorrect_plan": "The current plan does not support this feature, please upgrade to the subscription plan", "setting.incorrect_version": "This feature is not supported in the current version", "setting.log_details.title": "Home Log", - "setting.logs.title": "Homepage log", "setting.save": "Save", "setting.save_success": "Save successfully", + "sidebar.favourite_apps": "Favourite Apps", "sidebar.home": "Home", "sidebar.team_apps": "Team Apps", "source_cronJob": "Scheduled execution", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 4a4a4a6db907..cc59a05e9ce1 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -893,23 +893,7 @@ "model.type.reRank": "ReRank", "model.type.stt": "STT", "model.type.tts": "TTS", - "model_alicloud": "Ali Cloud", - "model_baai": "BAAI", - "model_baichuan": "Baichuan", - "model_chatglm": "ChatGLM", - "model_doubao": "Doubao", - "model_ernie": "Ernie", - "model_hunyuan": "Hunyuan", - "model_intern": "Intern", - "model_moka": "Moka-AI", - "model_moonshot": "Moonshot", - "model_other": "Other", - "model_ppio": "PPIO", - "model_qwen": "Qwen", - "model_siliconflow": "Siliconflow", - "model_sparkdesk": "SprkDesk", - "model_stepfun": "StepFun", - "model_yi": "Yi", + "month": "Month", "move.confirm": "Confirm move", "move_success": "Moved Successfully", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index d21a51db7539..8707378f22ff 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -41,6 +41,7 @@ "file_input_tip": "可通过【插件开始】节点的“文件链接”获取对应文件的链接", "history_slider.home.title": "聊天", "home.chat_app": "首页聊天", + "home.chat_id": "会话ID", "home.no_available_tools": "暂无可用工具", "home.select_tools": "选择工具", "home.tools": "工具:{{num}}", @@ -97,8 +98,36 @@ "setting.copyright.tips.square": "建议比例 1:1", "setting.copyright.title": "版权信息", "setting.copyright.upload_fail": "文件上传失败", - "setting.data_dashboard.title": "数据看板", + "setting.data_dashboard.title": "首页数据", "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram.png", + "setting.favourite.add_new_app": "添加应用", + "setting.favourite.all_apps": "所有应用", + "setting.favourite.cancel_button": "取消", + "setting.favourite.categories_modal.delete_cancel_button": "取消", + "setting.favourite.categories_modal.delete_confirm": "确认删除 {{name}} ?该分类下的应用将被移动至默认", + "setting.favourite.categories_modal.delete_confirm_button": "删除", + "setting.favourite.categories_modal.delete_confirm_title": "确认删除", + "setting.favourite.categories_modal.title": "共 {{num}} 个分类", + "setting.favourite.category_all": "全部分类", + "setting.favourite.category_tab.all": "全部", + "setting.favourite.category.no_data": "暂无可用精选应用", + "setting.favourite.tag.no_data": "暂无分类", + "setting.favourite.category_placeholder": "选择分类", + "setting.favourite.confirm_button": "确定", + "setting.favourite.delete_app_cancel_button": "取消", + "setting.favourite.delete_app_confirm": "确定要移除该精选应用吗?", + "setting.favourite.delete_app_confirm_button": "确定", + "setting.favourite.delete_app_title": "删除应用", + "setting.favourite.manage_categories_button": "分类管理", + "setting.favourite.save_category_for_app_button": "保存", + "setting.favourite.search_placeholder": "搜索应用", + "setting.favourite.selected_list": "已选: {{num}}", + "setting.favourite.table_column_action": "操作", + "setting.favourite.table_column_category": "分类", + "setting.favourite.table_column_intro": "介绍", + "setting.favourite.table_column_name": "名称", + "setting.favourite.title": "精选应用", + "setting.favourite.goto_add": "去配置", "setting.home.available_tools": "可用工具", "setting.home.available_tools.add": "添加", "setting.home.commercial_version": "商业版", @@ -108,18 +137,23 @@ "setting.home.dialogue_tips_placeholder": "请输入对话框提示文字", "setting.home.home_tab_title": "首页标题", "setting.home.home_tab_title_placeholder": "请输入首页标题", + "setting.home.quick_apps": "快捷应用", + "setting.home.quick_apps.add": "配置快捷应用", + "setting.home.quick_apps.placeholder": "请选择应用", "setting.home.slogan": "Slogan", "setting.home.slogan.default": "你好👋,我是 FastGPT ! 请问有什么可以帮你?", "setting.home.slogan_placeholder": "请输入 Slogan", "setting.home.title": "首页配置", + "setting.home.cancel_button": "取消", + "setting.home.confirm_button": "确定", + "setting.home.no_selected_app": "未选择应用", "setting.incorrect_plan": "当前套餐不支持该功能,请升级订阅套餐", "setting.incorrect_version": "当前版本不支持该功能", "setting.log_details.title": "首页日志", - "setting.logs.title": "首页日志", "setting.save": "保存", "setting.save_success": "保存成功", "setting.share": "分享", - "home.chat_id": "会话ID", + "sidebar.favourite_apps": "精选应用", "sidebar.home": "首页", "sidebar.team_apps": "团队应用", "source_cronJob": "定时执行", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 1b8789b1e56a..377d303e7973 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -893,23 +893,7 @@ "model.type.reRank": "重排模型", "model.type.stt": "语音识别", "model.type.tts": "语音合成", - "model_alicloud": "阿里云", - "model_baai": "智源", - "model_baichuan": "百川智能", - "model_chatglm": "ChatGLM", - "model_doubao": "豆包", - "model_ernie": "文心一言", - "model_hunyuan": "腾讯混元", - "model_intern": "书生", - "model_moka": "Moka-AI", - "model_moonshot": "月之暗面", - "model_other": "其他", - "model_ppio": "PPIO 派欧云", - "model_qwen": "阿里千问", - "model_siliconflow": "硅基流动", - "model_sparkdesk": "讯飞星火", - "model_stepfun": "阶跃星辰", - "model_yi": "零一万物", + "month": "月", "move.confirm": "确认移动", "move_success": "移动成功", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index 2f2443e463f5..e55292a15baf 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -97,16 +97,49 @@ "setting.copyright.tips.square": "建議比例 1:1", "setting.copyright.title": "版權信息", "setting.copyright.upload_fail": "文件上傳失敗", - "setting.data_dashboard.title": "數據看板", + "setting.data_dashboard.title": "首頁數據", "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram_zh-Hant.png", + "setting.favourite.add_new_app": "添加應用", + "setting.favourite.cancel_button": "取消", + "setting.favourite.categories_modal.delete_cancel_button": "取消", + "setting.favourite.categories_modal.delete_confirm": "確認刪除 {{name}} ?\n該分類下的應用將被移動至默認", + "setting.favourite.categories_modal.delete_confirm_button": "刪除", + "setting.favourite.categories_modal.delete_confirm_title": "確認刪除", + "setting.favourite.categories_modal.title": "共 {{num}} 個分類", + "setting.favourite.category.no_data": "暫無可用精選應用", + "setting.favourite.category_all": "全部分類", + "setting.favourite.category_placeholder": "選擇分類", + "setting.favourite.category_tab.all": "全部", + "setting.favourite.confirm_button": "確定", + "setting.favourite.delete_app_cancel_button": "取消", + "setting.favourite.delete_app_confirm": "確定要移除該精選應用嗎?", + "setting.favourite.delete_app_confirm_button": "確定", + "setting.favourite.delete_app_title": "刪除應用", + "setting.favourite.manage_categories_button": "分類管理", + "setting.favourite.save_category_for_app_button": "保存", + "setting.favourite.search_placeholder": "搜索應用", + "setting.favourite.selected_list": "已選: {{num}}", + "setting.favourite.table_column_action": "操作", + "setting.favourite.table_column_category": "分類", + "setting.favourite.table_column_intro": "介紹", + "setting.favourite.table_column_name": "名稱", + "setting.favourite.tag.no_data": "暫無分類", + "setting.favourite.title": "精選應用", + "setting.home.available_tools": "可用工具", "setting.home.available_tools.add": "添加", + "setting.home.cancel_button": "取消", "setting.home.commercial_version": "商業版", + "setting.home.confirm_button": "確定", "setting.home.diagram": "示意圖", "setting.home.dialogue_tips": "對話框提示文字", "setting.home.dialogue_tips.default": "你可以問我任何問題", "setting.home.dialogue_tips_placeholder": "請輸入對話框提示文字", "setting.home.home_tab_title": "首頁標題", "setting.home.home_tab_title_placeholder": "請輸入首頁標題", + "setting.home.no_selected_app": "未選擇應用", + "setting.home.quick_apps": "快捷應用", + "setting.home.quick_apps.add": "配置快捷應用", + "setting.home.quick_apps.placeholder": "請選擇應用", "setting.home.slogan": "Slogan", "setting.home.slogan.default": "你好👋,我是 FastGPT ! 請問有什麼可以幫你?", "setting.home.slogan_placeholder": "請輸入 Slogan", @@ -117,6 +150,7 @@ "setting.logs.title": "首頁日誌", "setting.save": "保存", "setting.save_success": "保存成功", + "sidebar.favourite_apps": "精選應用", "sidebar.home": "首頁", "sidebar.team_apps": "團隊應用", "source_cronJob": "定時執行", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 6b22437a4ff2..5c90a8f6af26 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -892,23 +892,7 @@ "model.type.reRank": "重排模型", "model.type.stt": "語音辨識", "model.type.tts": "語音合成", - "model_alicloud": "阿里雲", - "model_baai": "智源", - "model_baichuan": "百川智能", - "model_chatglm": "ChatGLM", - "model_doubao": "豆包", - "model_ernie": "文心一言", - "model_hunyuan": "騰訊混元", - "model_intern": "書生", - "model_moka": "Moka-AI", - "model_moonshot": "月之暗面", - "model_other": "其他", - "model_ppio": "PPIO 派歐雲", - "model_qwen": "阿里千問", - "model_siliconflow": "矽基流動", - "model_sparkdesk": "訊飛星火", - "model_stepfun": "階躍星辰", - "model_yi": "零一萬物", + "month": "月", "move.confirm": "確認移動", "move_success": "移動成功", diff --git a/packages/web/package.json b/packages/web/package.json index 4be2dbf6bf4f..4a4864b0dea1 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,7 +18,7 @@ "@lexical/utils": "0.12.6", "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "^4.24.10", - "ahooks": "^3.7.11", + "ahooks": "^3.9.4", "date-fns": "2.30.0", "dayjs": "^1.11.7", "recharts": "^2.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1126459e88f1..b3f0a32c4c04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: react-i18next: specifier: 14.1.2 version: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + typescript: + specifier: ^5.1.3 + version: 5.8.2 vitest: specifier: ^3.0.9 version: 3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) @@ -68,6 +71,9 @@ importers: '@bany/curl-to-json': specifier: ^1.2.8 version: 1.2.8 + '@fastgpt-sdk/plugin': + specifier: ^0.1.12 + version: 0.1.12(@types/node@20.14.0) axios: specifier: ^1.8.2 version: 1.8.4 @@ -120,9 +126,6 @@ importers: packages/service: dependencies: - '@fastgpt-sdk/plugin': - specifier: ^0.1.9 - version: 0.1.9(@types/node@20.17.24) '@fastgpt/global': specifier: workspace:* version: link:../global @@ -365,8 +368,8 @@ importers: specifier: ^4.24.10 version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ahooks: - specifier: ^3.7.11 - version: 3.8.4(react@18.3.1) + specifier: ^3.9.4 + version: 3.9.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: 2.30.0 version: 2.30.0 @@ -1973,8 +1976,8 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastgpt-sdk/plugin@0.1.9': - resolution: {integrity: sha512-9OBflL5W9SSEnhlNR/deL7+AMVkj7DKZcHlHELZFCMX+y0dhHmpHIh5bDon3zaeny40K35iI7md6pqDZ5xp5tw==} + '@fastgpt-sdk/plugin@0.1.12': + resolution: {integrity: sha512-jcgS0FWNxtfdwq+M7VEK4NggkIIXDkSC7jukYjRkEPS1klupkS89O3D0ekAZHq7uzHUbEN18x0xui+nV3nh0Tw==} '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} @@ -3902,6 +3905,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + ahooks@3.9.4: + resolution: {integrity: sha512-NkbX0mamCz4aBX27mZnObbzqcM9S4fzpjVf/6yOvmHh+McBo74xQw5Yz5ry4q2cLMkfNUjhe2q3M5RpjfMVu4g==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -11212,10 +11222,10 @@ snapshots: '@eslint/js@8.57.1': {} - '@fastgpt-sdk/plugin@0.1.9(@types/node@20.17.24)': + '@fastgpt-sdk/plugin@0.1.12(@types/node@20.14.0)': dependencies: '@fortaine/fetch-event-source': 3.0.6 - '@ts-rest/core': 3.52.1(@types/node@20.17.24)(zod@3.25.51) + '@ts-rest/core': 3.52.1(@types/node@20.14.0)(zod@3.25.51) zod: 3.25.51 transitivePeerDependencies: - '@types/node' @@ -12580,9 +12590,9 @@ snapshots: '@trysound/sax@0.2.0': {} - '@ts-rest/core@3.52.1(@types/node@20.17.24)(zod@3.25.51)': + '@ts-rest/core@3.52.1(@types/node@20.14.0)(zod@3.25.51)': optionalDependencies: - '@types/node': 20.17.24 + '@types/node': 20.14.0 zod: 3.25.51 '@tsconfig/node10@1.0.11': {} @@ -13457,6 +13467,20 @@ snapshots: screenfull: 5.2.0 tslib: 2.8.1 + ahooks@3.9.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.10 + dayjs: 1.11.13 + intersection-observer: 0.12.2 + js-cookie: 3.0.5 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + tslib: 2.8.1 + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -15102,7 +15126,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -15113,7 +15137,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -15135,7 +15159,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15164,7 +15188,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/projects/app/public/imgs/modal/add.svg b/projects/app/public/imgs/modal/add.svg new file mode 100644 index 000000000000..49848f612a65 --- /dev/null +++ b/projects/app/public/imgs/modal/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/app/public/imgs/modal/tag.svg b/projects/app/public/imgs/modal/tag.svg new file mode 100644 index 000000000000..b0bf3b5ce7ae --- /dev/null +++ b/projects/app/public/imgs/modal/tag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/projects/app/public/imgs/modal/warn.svg b/projects/app/public/imgs/modal/warn.svg new file mode 100644 index 000000000000..e7bdeb86f540 --- /dev/null +++ b/projects/app/public/imgs/modal/warn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/projects/app/src/components/Select/AIModelSelector.tsx b/projects/app/src/components/Select/AIModelSelector.tsx index f0c22e10c1b9..8e8ec51a79dc 100644 --- a/projects/app/src/components/Select/AIModelSelector.tsx +++ b/projects/app/src/components/Select/AIModelSelector.tsx @@ -7,7 +7,7 @@ import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants'; import { Box, Flex } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import { ModelProviderList } from '@fastgpt/global/core/ai/provider'; +import { getModelProviders } from '@fastgpt/global/core/ai/provider'; import MultipleRowSelect from '@fastgpt/web/components/common/MySelect/MultipleRowSelect'; import { getModelFromList } from '@fastgpt/global/core/ai/model'; import type { ResponsiveValue } from '@chakra-ui/system'; @@ -18,9 +18,10 @@ type Props = SelectProps & { }; const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Props) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList } = useSystemStore(); + const language = i18n.language; const avatarSize = useMemo(() => { const size = { @@ -42,7 +43,7 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro ]; return list .map((item) => { - const modelData = getModelFromList(allModels, item.value)!; + const modelData = getModelFromList(allModels, item.value, language)!; if (!modelData) return; return { @@ -67,13 +68,15 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro label: React.JSX.Element; }[]; }, [ - list, llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList, - avatarSize + list, + language, + avatarSize, + noOfLines ]); return ( @@ -109,9 +112,10 @@ const MultipleRowSelector = ({ noOfLines, ...props }: Props) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList } = useSystemStore(); + const language = i18n.language; const modelList = useMemo(() => { const allModels = [ ...llmModelList, @@ -121,8 +125,16 @@ const MultipleRowSelector = ({ ...reRankModelList ]; - return list.map((item) => getModelFromList(allModels, item.value)!).filter(Boolean); - }, [llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList, list]); + return list.map((item) => getModelFromList(allModels, item.value, language)!).filter(Boolean); + }, [ + llmModelList, + embeddingModelList, + ttsModelList, + sttModelList, + reRankModelList, + list, + language + ]); const [value, setValue] = useState([]); @@ -137,7 +149,7 @@ const MultipleRowSelector = ({ }, [props.size]); const selectorList = useMemo(() => { - const renderList = ModelProviderList.map<{ + const renderList = getModelProviders(language).map<{ label: React.JSX.Element; value: string; children: { label: string | React.ReactNode; value: string }[]; @@ -151,7 +163,7 @@ const MultipleRowSelector = ({ fallbackSrc={HUGGING_FACE_ICON} w={avatarSize} /> - {t(provider.name as any)} + {provider.name} ), value: provider.id, @@ -159,7 +171,7 @@ const MultipleRowSelector = ({ })); for (const item of list) { - const modelData = getModelFromList(modelList, item.value); + const modelData = getModelFromList(modelList, item.value, language); if (!modelData) continue; const provider = renderList.find((item) => item.value === (modelData?.provider || 'Other')) ?? @@ -172,7 +184,7 @@ const MultipleRowSelector = ({ } return renderList.filter((item) => item.children.length > 0); - }, [avatarSize, list, modelList, t]); + }, [avatarSize, list, modelList, t, language]); const onSelect = useCallback( (e: string[]) => { @@ -183,7 +195,7 @@ const MultipleRowSelector = ({ const SelectedLabel = useMemo(() => { if (!props.value) return <>{t('common:not_model_config')}; - const modelData = getModelFromList(modelList, props.value); + const modelData = getModelFromList(modelList, props.value, language); if (!modelData) return <>{t('common:not_model_config')}; @@ -201,7 +213,7 @@ const MultipleRowSelector = ({ {modelData?.name} ); - }, [modelList, props.value, t, avatarSize]); + }, [modelList, props.value, t, avatarSize, language]); return ( import('@fastgpt/web/components/common/MyModal')); const ModelTable = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const [provider, setProvider] = useState(''); const providerList = useRef<{ label: any; value: ModelProviderIdType | '' }[]>([ { label: t('common:All'), value: '' }, - ...ModelProviderList.map((item) => ({ + ...getModelProviders(language).map((item) => ({ label: ( - {t(item.name as any)} + {item.name} ), value: item.id @@ -162,7 +163,7 @@ const ModelTable = () => { ]; })(); const formatList = list.map((item) => { - const provider = getModelProvider(item.provider); + const provider = getModelProvider(item.provider, language); return { name: item.name, avatar: provider.avatar, @@ -195,7 +196,8 @@ const ModelTable = () => { t, modelType, provider, - search + search, + language ]); const filterProviderList = useMemo(() => { diff --git a/projects/app/src/components/core/app/TTSSelect.tsx b/projects/app/src/components/core/app/TTSSelect.tsx index 3e7a93c46340..b0154b42fef2 100644 --- a/projects/app/src/components/core/app/TTSSelect.tsx +++ b/projects/app/src/components/core/app/TTSSelect.tsx @@ -26,7 +26,8 @@ const TTSSelect = ({ value?: AppTTSConfigType; onChange: (e: AppTTSConfigType) => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { ttsModelList } = useSystemStore(); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -37,7 +38,7 @@ const TTSSelect = ({ { label: t('app:tts_close'), value: TTSTypeEnum.none, children: [] }, { label: t('app:tts_browser'), value: TTSTypeEnum.web, children: [] }, ...ttsModelList.map((model) => { - const providerData = getModelProvider(model.provider); + const providerData = getModelProvider(model.provider, language); return { label: ( @@ -71,9 +72,9 @@ const TTSSelect = ({ const provider = selectorList.find((item) => item.value === formatValue[0]) || selectorList[0]; const voice = provider.children.find((item) => item.value === formatValue[1]); return ( - + {voice ? ( - + {provider.label} / {voice.label} diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx index 0b16db697c5f..e8ff0343e186 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx @@ -376,10 +376,6 @@ const ChatInput = ({ return ( e.preventDefault()} onDrop={(e) => { e.preventDefault(); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx index 91c13cba6101..717dc342a084 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx @@ -24,17 +24,23 @@ import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { useCreation } from 'ahooks'; import type { ChatTypeEnum } from './constants'; +import type { QuickAppType } from '@fastgpt/global/core/chat/setting/type'; export type ChatProviderProps = { appId: string; chatId: string; outLinkAuthData?: OutLinkChatAuthProps; - chatType: ChatTypeEnum; InputLeftComponent?: React.ReactNode; + + chatType: ChatTypeEnum; dialogTips?: string; wideLogo?: string; slogan?: string; + + currentQuickAppId?: string; + quickAppList?: QuickAppType[]; + onSwitchQuickApp?: (appId: string) => Promise; }; type useChatStoreType = ChatProviderProps & { diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/ChatHomeVariablesForm.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/ChatHomeVariablesForm.tsx new file mode 100644 index 000000000000..989837c99dd8 --- /dev/null +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/ChatHomeVariablesForm.tsx @@ -0,0 +1,96 @@ +import LabelAndFormRender from '@/components/core/app/formRender/LabelAndForm'; +import { ChatBoxContext } from '@/components/core/chat/ChatContainer/ChatBox/Provider'; +import { Box, Flex, Card, Button } from '@chakra-ui/react'; +import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; +import { useContextSelector } from 'use-context-selector'; +import { useTranslation } from 'react-i18next'; +import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; +import { variableInputTypeToInputType } from '@/components/core/app/formRender/utils'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { UseFormReturn } from 'react-hook-form'; +import type { ChatBoxInputFormType } from '@/components/core/chat/ChatContainer/ChatBox/type'; + +type Props = { + chatForm: UseFormReturn; +}; + +const ChatHomeVariablesForm = ({ chatForm }: Props) => { + const { t } = useTranslation(); + + const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm); + + const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList); + const allVariableList = useContextSelector(ChatBoxContext, (v) => v.allVariableList); + + return ( + + + {/* custom variables */} + {allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && ( + <> + + + {t('chat:variable_invisable_in_share')} + + {allVariableList + .filter((i) => i.type === VariableInputEnum.custom) + .map((item) => ( + + ))} + + )} + {/* normal variables */} + {variableList.length > 0 && ( + <> + {variableList.map((item) => ( + + ))} + + )} + + + + ); +}; + +export default ChatHomeVariablesForm; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/QuickApps.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/QuickApps.tsx new file mode 100644 index 000000000000..e9656a07d55f --- /dev/null +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/QuickApps.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useContextSelector } from 'use-context-selector'; +import { ChatBoxContext } from '../../Provider'; +import { Box, Flex } from '@chakra-ui/react'; +import Avatar from '@fastgpt/web/components/common/Avatar'; + +const QuickApps = () => { + const quickAppList = useContextSelector(ChatBoxContext, (v) => v.quickAppList); + const currentQuickAppId = useContextSelector(ChatBoxContext, (v) => v.currentQuickAppId); + const onSwitchQuickApp = useContextSelector(ChatBoxContext, (v) => v.onSwitchQuickApp); + + return quickAppList && quickAppList.length > 0 ? ( + + {quickAppList.map((q) => ( + onSwitchQuickApp?.(q._id)} + > + + + {q.name} + + + ))} + + ) : null; +}; + +export default QuickApps; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/WelcomeHomeBox.tsx similarity index 96% rename from projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx rename to projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/WelcomeHomeBox.tsx index 005f4ddb74ce..7d190c600a00 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/home/WelcomeHomeBox.tsx @@ -8,7 +8,7 @@ const WelcomeHomeBox = () => { const slogan = useContextSelector(ChatBoxContext, (v) => v.slogan); return ( - + fastgpt logo import('./components/FeedbackModal')); const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal')); @@ -75,6 +70,9 @@ const SelectMarkCollection = dynamic(() => import('./components/SelectMarkCollec const Empty = dynamic(() => import('./components/Empty')); const WelcomeBox = dynamic(() => import('./components/WelcomeBox')); const VariableInputForm = dynamic(() => import('./components/VariableInputForm')); +const ChatHomeVariablesForm = dynamic(() => import('./components/home/ChatHomeVariablesForm')); +const WelcomeHomeBox = dynamic(() => import('./components/home/WelcomeHomeBox')); +const QuickApps = dynamic(() => import('./components/home/QuickApps')); enum FeedbackTypeEnum { user = 'user', @@ -134,6 +132,7 @@ const ChatBox = ({ const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm); const setIsVariableVisible = useContextSelector(ChatItemContext, (v) => v.setIsVariableVisible); + const isLoadingRecords = useContextSelector(ChatRecordContext, (v) => v.isLoadingRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords); const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); @@ -171,10 +170,12 @@ const ChatBox = ({ }); const { setValue, watch } = chatForm; const chatStartedWatch = watch('chatStarted'); + + // 可以进入对话框对话 const chatStarted = chatBoxData?.appId === appId && - (chatStartedWatch || - chatRecords.length > 0 || + (chatRecords.length > 0 || + chatStartedWatch || [...variableList, ...externalVariableList].length === 0); // 滚动到底部 @@ -411,7 +412,7 @@ const ChatBox = ({ /** * user confirm send prompt */ - const sendPrompt: SendPromptFnType = useMemoizedFn( + const sendPrompt = useMemoizedFn( ({ text = '', files = [], @@ -820,11 +821,6 @@ const ChatBox = ({ }; }); - const showHomeWelcome = useMemo( - () => chatRecords.length === 0 && chatType === ChatTypeEnum.home, - [chatRecords.length, chatType] - ); - const showEmpty = useMemo( () => chatType !== ChatTypeEnum.home && @@ -946,8 +942,10 @@ const ChatBox = ({ const containerRect = container.getBoundingClientRect(); const elementRect = variableInput.getBoundingClientRect(); + if (elementRect.height === 0) return; + setIsVariableVisible( - elementRect.bottom > containerRect.top && elementRect.top < containerRect.bottom + containerRect.top < elementRect.bottom && containerRect.bottom > elementRect.top ); }; @@ -960,24 +958,130 @@ const ChatBox = ({ container.removeEventListener('scroll', checkVariableVisibility); }; } - }, [chatType, setIsVariableVisible]); + }, [ScrollContainerRef, setIsVariableVisible]); - const RenderRecords = useMemo(() => { + const isHomeRender = useMemo(() => { + return chatType === ChatTypeEnum.home && chatRecords.length === 0 && !chatStartedWatch; + }, [chatType, chatRecords.length, chatStartedWatch]); + + //chat history + const RecordsBox = useMemo(() => { + return ( + + {chatRecords.map((item, index) => ( + + {/* 并且时间和上一条的time相差超过十分钟 */} + {index !== 0 && + item.time && + chatRecords[index - 1].time !== undefined && + new Date(item.time).getTime() - new Date(chatRecords[index - 1].time!).getTime() > + 10 * 60 * 1000 && } + + + {item.obj === ChatRoleEnum.Human && !item.hideInUI && ( + + )} + {item.obj === ChatRoleEnum.AI && ( + + {/* custom feedback */} + {item.customFeedbacks && item.customFeedbacks.length > 0 && ( + + + {item.customFeedbacks.map((text, i) => ( + + + } + > + {text} + + + + ))} + + )} + {/* admin mark content */} + {showMarkIcon && item.adminFeedback && ( + + + + {item.adminFeedback.q} + {item.adminFeedback.a} + + + )} + + )} + + + ))} + + ); + }, [ + chatRecords, + userAvatar, + retryInput, + delOneMessage, + appAvatar, + showVoiceIcon, + statusBoxData, + questionGuides, + onMark, + onAddUserLike, + onCloseUserLike, + onAddUserDislike, + onReadUserDislike, + t, + showMarkIcon, + onCloseCustomFeedback + ]); + const AppChatRenderBox = useMemo(() => { return ( - {/* chat header */} - {showHomeWelcome && } {showEmpty && } {!!welcomeText && } + {/* variable input */} {(!!variableList?.length || !!externalVariableList?.length) && ( @@ -988,120 +1092,33 @@ const ChatBox = ({ /> )} - {/* chat history */} - - {chatRecords.map((item, index) => ( - - {/* 并且时间和上一条的time相差超过十分钟 */} - {index !== 0 && - item.time && - chatRecords[index - 1].time !== undefined && - new Date(item.time).getTime() - new Date(chatRecords[index - 1].time!).getTime() > - 10 * 60 * 1000 && } - - - {item.obj === ChatRoleEnum.Human && !item.hideInUI && ( - - )} - {item.obj === ChatRoleEnum.AI && ( - - {/* custom feedback */} - {item.customFeedbacks && item.customFeedbacks.length > 0 && ( - - - {item.customFeedbacks.map((text, i) => ( - - - } - > - {text} - - - - ))} - - )} - {/* admin mark content */} - {showMarkIcon && item.adminFeedback && ( - - - - {item.adminFeedback.q} - {item.adminFeedback.a} - - - )} - - )} - - - ))} - + + {RecordsBox} ); }, [ ScrollData, - appAvatar, - chatForm, - chatRecords, + showEmpty, + welcomeText, + variableList, + externalVariableList, chatStarted, + chatForm, chatType, - delOneMessage, - externalVariableList?.length, - onAddUserDislike, - onAddUserLike, - onCloseCustomFeedback, - onCloseUserLike, - onMark, - onReadUserDislike, - questionGuides, - retryInput, - showEmpty, - showHomeWelcome, - showMarkIcon, - showVoiceIcon, - statusBoxData, - t, - userAvatar, - variableList?.length, - welcomeText + RecordsBox ]); + const HomeChatRenderBox = useMemo(() => { + return ( + <> + + + + + + + ); + }, []); return ( {/* chat box container */} - {RenderRecords} - {/* message input */} - {canSendPrompt && ( - chatController.current?.abort('stop')} - TextareaDom={TextareaDom} - resetInputVal={resetInputVal} - chatForm={chatForm} - /> + {isHomeRender ? ( + + + {HomeChatRenderBox} + {allVariableList.length > 0 ? ( + + + + ) : ( + chatController.current?.abort('stop')} + TextareaDom={TextareaDom} + resetInputVal={resetInputVal} + chatForm={chatForm} + /> + )} + + + ) : ( + <> + {AppChatRenderBox} + {canSendPrompt && ( + + chatController.current?.abort('stop')} + TextareaDom={TextareaDom} + resetInputVal={resetInputVal} + chatForm={chatForm} + /> + + )} + )} + {/* user feedback modal */} {!!feedbackId && chatId && ( { - const { t } = useTranslation(); + const { t } = useSafeTranslation(); // Auto scroll to top const ContentRef = useRef(null); @@ -462,7 +462,7 @@ const SideTabItem = ({ value: string; index: number; }) => { - const { t } = useTranslation(); + const { t } = useSafeTranslation(); if (!sideBarItem) return null; @@ -617,7 +617,7 @@ export const ResponseBox = React.memo(function ResponseBox({ hideTabs?: boolean; useMobile?: boolean; }) { - const { t } = useTranslation(); + const { t } = useSafeTranslation(); const { isPc } = useSystem(); const flattedResponse = useMemo(() => { @@ -825,7 +825,7 @@ const WholeResponseModal = ({ dataId: string; chatTime: Date; }) => { - const { t } = useTranslation(); + const { t } = useSafeTranslation(); const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v); const { loading: isLoading, data: response } = useRequest2( diff --git a/projects/app/src/global/aiproxy/constants.ts b/projects/app/src/global/aiproxy/constants.ts index a34382432796..9ab4a7f38696 100644 --- a/projects/app/src/global/aiproxy/constants.ts +++ b/projects/app/src/global/aiproxy/constants.ts @@ -1,4 +1,3 @@ -import { type ModelProviderIdType } from '@fastgpt/global/core/ai/provider'; import { type ChannelInfoType } from './type'; import { i18nT } from '@fastgpt/web/i18n/utils'; @@ -39,156 +38,3 @@ export const defaultChannel: ChannelInfoType = { base_url: '', priority: 0 }; - -export const aiproxyIdMap: Record< - number, - { label: string; provider: ModelProviderIdType; avatar?: string } -> = { - 1: { - label: 'OpenAI', - provider: 'OpenAI' - }, - 3: { - avatar: 'model/azure', - label: i18nT('account_model:azure'), - provider: 'OpenAI' - }, - 4: { - avatar: 'model/azure', - label: `azure (model name support contain '.')`, - provider: 'Other' - }, - 14: { - label: 'Anthropic', - provider: 'Claude' - }, - 12: { - label: 'Google Gemini(OpenAI)', - provider: 'Gemini' - }, - 24: { - label: 'Google Gemini', - provider: 'Gemini' - }, - 28: { - label: 'Mistral AI', - provider: 'MistralAI' - }, - 29: { - label: 'Groq', - provider: 'Groq' - }, - 17: { - label: '阿里云', - provider: 'Qwen' - }, - 40: { - label: '豆包', - provider: 'Doubao' - }, - 36: { - label: 'DeepSeek AI', - provider: 'DeepSeek' - }, - 13: { - label: '百度智能云 V2', - provider: 'Ernie' - }, - 15: { - label: '百度智能云', - provider: 'Ernie' - }, - 16: { - label: '智谱 AI', - provider: 'ChatGLM' - }, - 18: { - label: '讯飞星火', - provider: 'SparkDesk' - }, - 25: { - label: '月之暗面', - provider: 'Moonshot' - }, - 26: { - label: '百川智能', - provider: 'Baichuan' - }, - 27: { - label: 'MiniMax', - provider: 'MiniMax' - }, - 31: { - label: '零一万物', - provider: 'Yi' - }, - 32: { - label: '阶跃星辰', - provider: 'StepFun' - }, - 43: { - label: 'SiliconFlow', - provider: 'Siliconflow' - }, - 30: { - label: 'Ollama', - provider: 'Ollama' - }, - 23: { - label: i18nT('account_model:Hunyuan'), - provider: 'Hunyuan' - }, - 44: { - label: 'doubao audio', - provider: 'Doubao' - }, - 33: { - label: 'AWS', - provider: 'Other', - avatar: 'model/aws' - }, - 35: { - label: 'Cohere', - provider: 'Other', - avatar: 'model/cohere' - }, - 37: { - label: 'Cloudflare', - provider: 'Other', - avatar: 'model/cloudflare' - }, - 20: { - label: 'OpenRouter', - provider: 'OpenRouter' - }, - 47: { - label: 'JinaAI', - provider: 'Jina' - }, - 19: { - label: 'ai360', - provider: 'ai360' - }, - 42: { - label: 'vertexai', - provider: 'vertexai' - }, - 41: { - label: 'novita', - provider: 'novita' - }, - 45: { - label: 'Grok', - provider: 'Grok' - }, - 46: { - label: 'Doc2x', - provider: 'Other', - avatar: 'plugins/doc2x' - }, - 34: { - label: 'Coze', - provider: 'Other', - avatar: 'model/coze' - } -}; diff --git a/projects/app/src/pageComponents/account/model/AddModelBox.tsx b/projects/app/src/pageComponents/account/model/AddModelBox.tsx index 8edc66f10b8d..e0049d04d121 100644 --- a/projects/app/src/pageComponents/account/model/AddModelBox.tsx +++ b/projects/app/src/pageComponents/account/model/AddModelBox.tsx @@ -19,7 +19,7 @@ import { import { useTranslation } from 'next-i18next'; import React, { useMemo, useRef, useState } from 'react'; import { - ModelProviderList, + getModelProviders, type ModelProviderIdType, getModelProvider } from '@fastgpt/global/core/ai/provider'; @@ -95,7 +95,8 @@ export const ModelEditModal = ({ onSuccess: () => void; onClose: () => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { feConfigs } = useSystemStore(); const { register, getValues, setValue, handleSubmit, watch, reset } = @@ -111,14 +112,13 @@ export const ModelEditModal = ({ const isRerankModel = modelData?.type === ModelTypeEnum.rerank; const provider = watch('provider'); - const providerData = useMemo(() => getModelProvider(provider), [provider]); - const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>( - ModelProviderList.map((item) => ({ + const providerList = useRef<{ label: React.ReactNode; value: ModelProviderIdType }[]>( + getModelProviders(language).map((item) => ({ label: ( - {t(item.name as any)} + {item.name} ), value: item.id diff --git a/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx b/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx index 7fcf28ac63cc..c2cd0c1e3c50 100644 --- a/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx +++ b/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx @@ -1,4 +1,4 @@ -import { aiproxyIdMap } from '@/global/aiproxy/constants'; +import { aiproxyIdMap } from '@fastgpt/global/sdk/fastgpt-plugin'; import { type ChannelInfoType } from '@/global/aiproxy/type'; import { Box, @@ -39,6 +39,8 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; import { getChannelProviders, postCreateChannel, putChannel } from '@/web/core/ai/channel'; import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import type { localeType } from '@fastgpt/global/common/i18n/type'; const ModelEditModal = dynamic(() => import('../AddModelBox').then((mod) => mod.ModelEditModal)); @@ -56,7 +58,9 @@ const EditChannelModal = ({ onClose: () => void; onSuccess: () => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language as localeType; + const { defaultModels } = useSystemStore(); const isEdit = defaultConfig.id !== 0; @@ -71,16 +75,17 @@ const EditChannelModal = ({ return Object.entries(res) .map(([key, value]) => { const mapData = aiproxyIdMap[key as any] ?? { - label: value.name, + name: value.name, provider: 'Other' }; - const provider = getModelProvider(mapData.provider); + const provider = getModelProvider(mapData.provider, language); + return { order: provider.order, defaultBaseUrl: value.defaultBaseUrl, keyHelp: value.keyHelp, icon: mapData?.avatar ?? provider.avatar, - label: t(mapData.label as any), + label: parseI18nString(mapData.name, language), value: Number(key) }; }) @@ -127,11 +132,11 @@ const EditChannelModal = ({ const currentProvider = aiproxyIdMap[providerType]?.provider; return systemModelList .map((item) => { - const provider = getModelProvider(item.provider); + const provider = getModelProvider(item.provider, language); return { provider: item.provider, - icon: provider.avatar, + icon: provider?.avatar, label: item.model, value: item.model }; @@ -142,7 +147,7 @@ const EditChannelModal = ({ if (a.provider !== currentProvider && b.provider === currentProvider) return 1; return 0; }); - }, [providerType, systemModelList]); + }, [language, providerType, systemModelList]); const modelMapping = watch('model_mapping'); diff --git a/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx b/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx index 8e18fd69461c..5d1d17b86af8 100644 --- a/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx +++ b/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx @@ -45,7 +45,8 @@ const ModelTest = ({ models: string[]; onClose: () => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { toast } = useToast(); const [testModelList, setTestModelList] = useState([]); @@ -76,7 +77,7 @@ const ModelTest = ({ .map((model) => { const modelData = res.find((item) => item.model === model); if (!modelData) return null; - const provider = getModelProvider(modelData.provider); + const provider = getModelProvider(modelData.provider, language); return { label: ( diff --git a/projects/app/src/pageComponents/account/model/Channel/index.tsx b/projects/app/src/pageComponents/account/model/Channel/index.tsx index e97dc48fa06f..9351d66942a6 100644 --- a/projects/app/src/pageComponents/account/model/Channel/index.tsx +++ b/projects/app/src/pageComponents/account/model/Channel/index.tsx @@ -26,12 +26,8 @@ import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import { useUserStore } from '@/web/support/user/useUserStore'; import { type ChannelInfoType } from '@/global/aiproxy/type'; import MyTag from '@fastgpt/web/components/common/Tag/index'; -import { - aiproxyIdMap, - ChannelStatusEnum, - ChannelStautsMap, - defaultChannel -} from '@/global/aiproxy/constants'; +import { aiproxyIdMap } from '@fastgpt/global/sdk/fastgpt-plugin'; +import { ChannelStatusEnum, ChannelStautsMap, defaultChannel } from '@/global/aiproxy/constants'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import dynamic from 'next/dynamic'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; @@ -39,12 +35,16 @@ import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; import { getModelProvider } from '@fastgpt/global/core/ai/provider'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import type { localeType } from '@fastgpt/global/common/i18n/type'; +import Avatar from '@fastgpt/web/components/common/Avatar'; const EditChannelModal = dynamic(() => import('./EditChannelModal'), { ssr: false }); const ModelTest = dynamic(() => import('./ModelTest'), { ssr: false }); const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language as localeType; const { userInfo } = useUserStore(); const isRoot = userInfo?.username === 'root'; @@ -126,10 +126,10 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => { {channelList.map((item) => { const providerData = aiproxyIdMap[item.type] || { - label: channelProviders[item.type]?.name || 'Invalid provider', + name: channelProviders[item.type]?.name || 'Invalid provider', provider: 'Other' }; - const provider = getModelProvider(providerData?.provider); + const provider = getModelProvider(providerData?.provider, language); return ( @@ -137,11 +137,8 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => { {item.name} - - {t(providerData?.label as any)} + + {parseI18nString(providerData.name, language)} diff --git a/projects/app/src/pageComponents/account/model/Log/index.tsx b/projects/app/src/pageComponents/account/model/Log/index.tsx index fe72b63dd75a..75e2e6aab88a 100644 --- a/projects/app/src/pageComponents/account/model/Log/index.tsx +++ b/projects/app/src/pageComponents/account/model/Log/index.tsx @@ -49,7 +49,8 @@ type LogDetailType = Omit & { response_body?: string; }; const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { userInfo } = useUserStore(); const isRoot = userInfo?.username === 'root'; @@ -103,7 +104,7 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => { const modelList = useMemo(() => { const res = systemModelList .map((item) => { - const provider = getModelProvider(item.provider); + const provider = getModelProvider(item.provider, language); return { order: provider.order, diff --git a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx index 1ae132ab7e92..2716c17fc8df 100644 --- a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx +++ b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx @@ -19,7 +19,7 @@ import { import { useTranslation } from 'next-i18next'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { - ModelProviderList, + getModelProviders, type ModelProviderIdType, getModelProvider } from '@fastgpt/global/core/ai/provider'; @@ -59,20 +59,21 @@ const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal')); const ModelEditModal = dynamic(() => import('./AddModelBox').then((mod) => mod.ModelEditModal)); const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { userInfo } = useUserStore(); const { defaultModels, feConfigs } = useSystemStore(); const isRoot = userInfo?.username === 'root'; const [provider, setProvider] = useState(''); - const providerList = useRef<{ label: any; value: ModelProviderIdType | '' }[]>([ + const providerList = useRef<{ label: React.ReactNode; value: ModelProviderIdType | '' }[]>([ { label: t('common:All'), value: '' }, - ...ModelProviderList.map((item) => ({ + ...getModelProviders(language).map((item) => ({ label: ( - {t(item.name as any)} + {item.name} ), value: item.id @@ -216,7 +217,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { })(); const formatList = list.map((item) => { - const provider = getModelProvider(item.provider); + const provider = getModelProvider(item.provider, language); return { ...item, avatar: provider.avatar, @@ -239,7 +240,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { }); return filterList; - }, [systemModelList, t, modelType, provider, search, showActive]); + }, [systemModelList, t, modelType, language, provider, search, showActive]); const activeModelLength = useMemo(() => { return modelList.filter((item) => item.isActive).length; }, [modelList]); diff --git a/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx b/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx index 88fb7610abe4..8a312620f6d4 100644 --- a/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx +++ b/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx @@ -57,7 +57,8 @@ const getDefaultDateRange = (): DateRangeType => { }; const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const theme = useTheme(); const { feConfigs } = useSystemStore(); @@ -113,7 +114,7 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => { const modelList = useMemo(() => { const res = systemModelList .map((item) => { - const provider = getModelProvider(item.provider); + const provider = getModelProvider(item.provider, language); return { order: provider.order, icon: provider.avatar, diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx index a979d2fd78fd..0edad7986852 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx @@ -53,6 +53,7 @@ export type HeaderControlProps = { setIsSelectAllSource: React.Dispatch>; dateRange: DateRangeType; setDateRange: (value: DateRangeType) => void; + px?: [number, number]; }; const chartBoxStyles = { @@ -138,7 +139,8 @@ const LogChart = ({ setIsSelectAllSource, dateRange, setDateRange, - showSourceSelector = true + showSourceSelector = true, + px = [4, 8] }: HeaderControlProps) => { const { t } = useTranslation(); @@ -334,6 +336,7 @@ const LogChart = ({ return ( - + @@ -765,7 +768,8 @@ const HeaderControl = ({ setIsSelectAllSource, dateRange, setDateRange, - showSourceSelector = true + showSourceSelector = true, + px = [4, 8] }: HeaderControlProps) => { const { t } = useTranslation(); @@ -780,13 +784,7 @@ const HeaderControl = ({ console.log(showSourceSelector); return ( - + {showSourceSelector && ( diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogKeysConfigPopover.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogKeysConfigPopover.tsx index e317fbbe1489..2f4faf4cd957 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/LogKeysConfigPopover.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/LogKeysConfigPopover.tsx @@ -10,13 +10,14 @@ import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; import MyIcon from '@fastgpt/web/components/common/Icon'; import React from 'react'; import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; +import type { SetState } from 'ahooks/lib/createUseStorageState'; const LogKeysConfigPopover = ({ logKeysList, setLogKeysList }: { logKeysList: AppLogKeysType[]; - setLogKeysList: (logKeysList: AppLogKeysType[] | undefined) => void; + setLogKeysList: (value: SetState) => void; }) => { const { t } = useTranslation(); return ( diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx index e81f07a9c5d1..a029e16dc484 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx @@ -61,7 +61,8 @@ const LogTable = ({ setIsSelectAllSource, dateRange, setDateRange, - showSourceSelector = true + showSourceSelector = true, + px = [4, 8] }: HeaderControlProps) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); @@ -340,7 +341,7 @@ const LogTable = ({ }); return ( - + {showSourceSelector && ( diff --git a/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx b/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx index b16a85cf0483..cf0733ccf07f 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx @@ -10,6 +10,7 @@ import { useContextSelector } from 'use-context-selector'; import { AppContext } from '../context'; import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; import type { getLogKeysResponse } from '@/pages/api/core/app/logs/getLogKeys'; +import type { SetState } from 'ahooks/lib/createUseStorageState'; const SyncLogKeysPopover = ({ logKeys, @@ -18,7 +19,7 @@ const SyncLogKeysPopover = ({ fetchLogKeys }: { logKeys: AppLogKeysType[]; - setLogKeys: (logKeys: AppLogKeysType[]) => void; + setLogKeys: (value: SetState) => void; teamLogKeys: AppLogKeysType[]; fetchLogKeys: () => Promise; }) => { diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx index 7781eec74d5f..11cbc8a991d6 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx @@ -697,6 +697,7 @@ const WorkflowContextProvider = ({ .map((node) => { const status = checkNodeRunStatus({ node, + nodesMap: new Map(runtimeNodes.map((item) => [item.nodeId, item])), runtimeEdges: debugData?.runtimeEdges || [] }); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowEventContext.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowEventContext.tsx index 3c917a8eb253..1405de5c2f52 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowEventContext.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowEventContext.tsx @@ -17,7 +17,7 @@ type WorkflowEventContextType = { hoverEdgeId?: string; setHoverEdgeId: React.Dispatch>; workflowControlMode?: 'drag' | 'select'; - setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void; + setWorkflowControlMode: (value: SetState<'drag' | 'select'>) => void; menu: { top: number; left: number; @@ -40,7 +40,7 @@ export const WorkflowEventContext = createContext({ throw new Error('Function not implemented.'); }, workflowControlMode: 'drag', - setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void { + setWorkflowControlMode: function (value: SetState<'drag' | 'select'>): void { throw new Error('Function not implemented.'); }, menu: null, @@ -96,7 +96,7 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) => /* Version histories */ const [showHistoryModal, setShowHistoryModal] = useState(false); - const contextValue = useMemo( + const contextValue = useMemo( () => ({ mouseInCanvas, reactFlowWrapper, diff --git a/projects/app/src/pageComponents/app/evaluation/DetailModal.tsx b/projects/app/src/pageComponents/app/evaluation/DetailModal.tsx index ccb91da7ef1d..0117c0f4c6c5 100644 --- a/projects/app/src/pageComponents/app/evaluation/DetailModal.tsx +++ b/projects/app/src/pageComponents/app/evaluation/DetailModal.tsx @@ -83,15 +83,16 @@ const EvaluationDetailModal = ({ onClose: () => void; fetchEvalList: () => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const [selectedIndex, setSelectedIndex] = useState(0); const [editing, setEditing] = useState(false); const [pollingInterval, setPollingInterval] = useState(10000); const { llmModelList } = useSystemStore(); const modelData = useMemo( - () => getModelFromList(llmModelList, evalDetail.evalModel), - [evalDetail.evalModel, llmModelList] + () => getModelFromList(llmModelList, evalDetail.evalModel, language), + [evalDetail.evalModel, llmModelList, language] ); const { diff --git a/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx new file mode 100644 index 000000000000..fef3041ccdda --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx @@ -0,0 +1,298 @@ +import { getFavouriteApps } from '@/web/core/chat/api'; +import { + Box, + Button, + Flex, + Grid, + GridItem, + Input, + InputGroup, + InputLeftElement, + Tab, + TabIndicator, + TabList, + Tabs +} from '@chakra-ui/react'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { useContextSelector } from 'use-context-selector'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { useMemo } from 'react'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; +import MyPopover from '@fastgpt/web/components/common/MyPopover'; +import NextHead from '@/components/common/NextHead'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { ChatContext } from '@/web/core/chat/context/chatContext'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; + +const ChatFavouriteApp = () => { + const { isPc } = useSystem(); + const { t } = useTranslation(); + + const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); + + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const wideLogoUrl = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.wideLogoUrl); + const homeTabTitle = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.homeTabTitle); + + const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); + const tagCache = useMemo(() => { + return tags.reduce( + (acc, tag) => { + acc[tag.id] = tag; + return acc; + }, + {} as Record + ); + }, [tags]); + const tagOptions = useMemo( + () => [ + { label: t('chat:setting.favourite.category_tab.all'), value: '' }, + ...tags.map((tag) => ({ + label: tag.name, + value: tag.id + })) + ], + [tags, t] + ); + + const { register, watch, setValue } = useForm<{ name: string; tag: string }>({ + defaultValues: { + name: '', + tag: '' + } + }); + const searchAppName = watch('name'); + const selectedTag = watch('tag'); + + // load all favourites for checked state and saving + const { loading: isSearching, data: favouriteApps = [] } = useRequest2( + async () => { + return await getFavouriteApps({ + name: searchAppName, + tag: selectedTag + }); + }, + { + manual: false, + throttleWait: 500, + refreshDeps: [searchAppName, selectedTag] + } + ); + + const TagBox = ({ id }: { id: string }) => { + const tag = tagCache[id]; + + if (!tag) return null; + + return ( + e.stopPropagation()} + > + {tag.name} + + ); + }; + + return ( + + + + {!isPc && ( + + + + + + + + + + + + + + + )} + + {/* header */} + + {/* tag tabs */} + + + {tagOptions.map((option) => ( + setValue('tag', option.value)} + > + {option.label} + + ))} + + + + + {/* search input */} + {isPc && ( + + + + + + + )} + + + {/* list */} + {favouriteApps.length > 0 ? ( + + {favouriteApps.map((app) => ( + + handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app.appId)} + > + + + + {app.name} + + + {app.intro} + + + + {app.favouriteTags.slice(0, 3).map((id) => ( + + ))} + + {app.favouriteTags.length > 3 && ( + e.stopPropagation()} + > + +{app.favouriteTags.length - 3} + + } + > + {() => ( + e.stopPropagation()} + > + {app.favouriteTags.slice(3).map((id) => ( + + ))} + + )} + + )} + + + + ))} + + ) : ( + + + + + + )} + + ); +}; + +export default ChatFavouriteApp; diff --git a/projects/app/src/pageComponents/chat/ChatHeader.tsx b/projects/app/src/pageComponents/chat/ChatHeader.tsx index 6f2996e36cfd..a16dc52e5e2b 100644 --- a/projects/app/src/pageComponents/chat/ChatHeader.tsx +++ b/projects/app/src/pageComponents/chat/ChatHeader.tsx @@ -31,7 +31,7 @@ import { } from '@/pageComponents/chat/constants'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { usePathname } from 'next/navigation'; -import type { ChatSettingSchema } from '@fastgpt/global/core/chat/setting/type'; +import type { ChatSettingReturnType } from '@fastgpt/global/core/chat/setting/type'; const ChatHeader = ({ history, @@ -43,7 +43,7 @@ const ChatHeader = ({ chatSettings }: { pane: ChatSidebarPaneEnum; - chatSettings: ChatSettingSchema | undefined; + chatSettings?: ChatSettingReturnType; history: ChatItemType[]; showHistory?: boolean; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/AppTree.tsx b/projects/app/src/pageComponents/chat/ChatSetting/AppTree.tsx new file mode 100644 index 000000000000..cd295c8e1f62 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/AppTree.tsx @@ -0,0 +1,146 @@ +import type { getMyApps } from '@/web/core/app/api'; +import { Box, Checkbox, Flex } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { useMemo, useState, useCallback } from 'react'; + +export type App = Awaited>[number]; + +export const TreeItem = ({ + app, + depth, + folder, + checked, + expanded, + onCheck, + onCollapse +}: { + app: App; + depth: number; + folder: boolean; + checked: boolean; + expanded: boolean; + onCheck: (id: string) => void; + onCollapse: (id: string) => void; +}) => { + return ( + (folder ? onCollapse(app._id) : onCheck(app._id))} + > + {folder ? ( + { + e.stopPropagation(); + if (folder) onCollapse(app._id); + }} + > + + + ) : ( + onCheck(app._id)} size="sm" /> + )} + + + + + {app.name} + + + + ); +}; + +export const Tree = ({ + apps, + checkedIds, + onCheck +}: { + apps: App[]; + checkedIds: string[]; + onCheck: (id: string) => void; +}) => { + const children = useMemo(() => { + const map = new Map(); + apps.forEach((item) => { + const key = item.parentId ? String(item.parentId) : '__root__'; + const list = map.get(key) || []; + list.push(item); + map.set(key, list); + }); + return map; + }, [apps]); + + const [expanded, setExpanded] = useState>({}); + const handleExpand = useCallback((id: string) => { + setExpanded((prev) => ({ ...prev, [id]: !prev[id] })); + }, []); + + const RenderNodes = useCallback( + ({ parent, depth }: { parent: string; depth: number }) => { + const list = children.get(parent) || []; + return ( + <> + {list.map((node) => { + const nodeId = String(node._id); + const isExpanded = !!expanded[nodeId]; + const folder = node.type === AppTypeEnum.folder; + + return ( + + + + {folder && isExpanded && ( + + + + )} + + ); + })} + + ); + }, + [children, checkedIds, expanded, onCheck, handleExpand] + ); + + return ; +}; + +export default Tree; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx index 4f70f74eb40f..d9e4ecca3258 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx @@ -28,18 +28,11 @@ const LogDetails = ({ Header }: Props) => { } = useMultipleSelect(Object.values(ChatSourceEnum), true); return ( - +
void; + onRefresh: () => Promise; +}; + +const AddFavouriteAppModal = ({ onClose, onRefresh }: Props) => { + const { t } = useTranslation(); + + const { watch: watchSearchValue, setValue } = useForm<{ name: string }>({ + defaultValues: { + name: '' + } + }); + const searchAppNameValue = watchSearchValue('name'); + + const [parentId, setParentId] = useState(''); + const { data: appData = { apps: [], paths: [] }, loading: isFetching } = useRequest2( + async () => { + const [apps, paths] = await Promise.all([ + getMyApps({ + parentId, + searchKey: searchAppNameValue, + type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] + }), + searchAppNameValue.trim() + ? Promise.resolve([]) + : getAppFolderPath({ sourceId: parentId, type: 'current' }) + ]); + return { apps, paths }; + }, + { + manual: false, + throttleWait: 500, + refreshDeps: [parentId, searchAppNameValue] + } + ); + const availableApps = appData.apps; + const paths = appData.paths; + + const [selectedApps, setSelectedApps] = useState<{ id: string; name: string; avatar: string }[]>( + [] + ); + + useRequest2(getFavouriteApps, { + manual: false, + onSuccess(res) { + setSelectedApps( + res.map((item) => ({ id: item.appId, name: item.name, avatar: item.avatar })) + ); + } + }); + + const handleCheck = useCallback((app: { id: string; name: string; avatar: string }) => { + setSelectedApps((prev) => { + const exists = prev.some((item) => item.id === app.id); + if (exists) { + return prev.filter((item) => item.id !== app.id); + } + return [{ id: app.id, name: app.name, avatar: app.avatar }, ...prev]; + }); + }, []); + + const { run: updateFavourites, loading: isUpdating } = useRequest2( + async () => { + await updateFavouriteApps( + selectedApps.map((app, index) => ({ appId: app.id, order: index + 1 })) + ); + }, + { + manual: true, + onSuccess: async () => { + await onRefresh(); + onClose(); + } + } + ); + + return ( + + + + + + + + setValue('name', e.target.value)} + size="md" + /> + + + + {searchAppNameValue && ( + + {t('chat:search_results')} + + )} + {!searchAppNameValue && paths.length === 0 && ( + + setParentId('')} + > + {t('common:root_folder')} + + + + )} + {!searchAppNameValue && paths.length > 0 && ( + ({ parentId: p.parentId, parentName: p.parentName }))} + FirstPathDom={t('common:root_folder')} + onClick={(e) => setParentId(e)} + /> + )} + + + + {availableApps.length === 0 && !isFetching && ( + + )} + {availableApps.map((item: App) => ( + + { + if (item.type === AppTypeEnum.folder) { + if (searchAppNameValue) setValue('name', ''); + setParentId(String(item._id)); + } else { + handleCheck({ id: item._id, name: item.name, avatar: item.avatar }); + } + }} + > + e.stopPropagation()}> + {item.type !== AppTypeEnum.folder && ( + app.id === item._id)} + onChange={() => + handleCheck({ id: item._id, name: item.name, avatar: item.avatar }) + } + colorScheme="blue" + size="sm" + /> + )} + + + + + + + {item.name} + + + {item.type === AppTypeEnum.folder ? t('common:Folder') : ''} + + + + {item.type === AppTypeEnum.folder && ( + + + + )} + + + ))} + + + + + + + + {t('chat:setting.favourite.selected_list', { + num: selectedApps.length + })} + + + + {selectedApps.length === 0 && !isFetching && ( + + )} + + {selectedApps.map((app) => { + return ( + + + + {app.name} + + + handleCheck(app)} + /> + + + ); + })} + + + + + + + + + + + + + + ); +}; + +export default React.memo(AddFavouriteAppModal); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx new file mode 100644 index 000000000000..8b3ea8254764 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx @@ -0,0 +1,611 @@ +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { AddIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Checkbox, + Flex, + IconButton, + Input, + InputGroup, + InputLeftElement, + useDisclosure, + VStack +} from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useContextSelector } from 'use-context-selector'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { ChatFavouriteTagType } from '@fastgpt/global/core/chat/setting/type'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { getFavouriteApps, updateChatSetting, updateFavouriteAppTags } from '@/web/core/chat/api'; +import { useForm } from 'react-hook-form'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import type { ChatFavouriteApp } from '@fastgpt/global/core/chat/favouriteApp/type'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; + +type EditableTagItemProps = { + tag: ChatFavouriteTagType; + isEditing: boolean; + onStartEdit: () => void; + onCommit: (updated: ChatFavouriteTagType) => Promise | void; + onCancelNew: (tag: ChatFavouriteTagType) => void; + onExitEdit: (tag: ChatFavouriteTagType) => void; + onConfirmDelete: (tag: ChatFavouriteTagType) => void; + onSaveTagForApp: (tag: ChatFavouriteTagType) => void; + appCount?: number; +}; + +const EditableTagItem = React.memo(function EditableTagItem({ + isEditing, + tag: initialTag, + onCommit, + onCancelNew, + onExitEdit, + onStartEdit, + onConfirmDelete, + onSaveTagForApp, + appCount +}: EditableTagItemProps) { + const { t } = useTranslation(); + + const [tag, setTag] = useState(initialTag); + const [isSelfEditing, setIsSelfEditing] = useState(isEditing); + const inputRef = useRef(null); + + const { ConfirmModal, openConfirm } = useConfirm({ + type: 'delete', + content: t('chat:setting.favourite.categories_modal.delete_confirm', { + name: initialTag.name + }) + }); + + const handleConfirmDelete = useCallback(() => { + openConfirm(() => { + onConfirmDelete(tag); + })(); + }, [openConfirm, onConfirmDelete, tag]); + + const handleFinishEdit = useCallback(async () => { + // 取消或者复原 tag 的名称 + if (tag.name.trim() === '') { + if ((initialTag.name || '').trim() === '') { + onCancelNew(initialTag); + } else { + setTag(initialTag); + setIsSelfEditing(false); + } + onExitEdit(initialTag); + if (inputRef.current) inputRef.current.blur(); + return; + } + setIsSelfEditing(false); + await onCommit(tag); + + if (inputRef.current) inputRef.current.blur(); + }, [tag, onCommit, onCancelNew, onExitEdit, initialTag]); + + useEffect(() => { + setIsSelfEditing(isEditing); + }, [isEditing]); + + useEffect(() => { + if (isSelfEditing) return; + // sync from props when not editing + setTag(initialTag); + }, [initialTag, isSelfEditing]); + + useEffect(() => { + if (!inputRef.current || !isSelfEditing) return; + inputRef.current.focus(); + }, [isSelfEditing]); + + return ( + + + {isSelfEditing ? ( + { + const nextName = e.target.value; + setTag({ ...tag, name: nextName }); + }} + onKeyDown={(e) => { + if (e.key.toLowerCase() !== 'enter') return; + handleFinishEdit(); + }} + /> + ) : ( + + {tag.name} + + )} + ({appCount ?? 0}) + + + {!isSelfEditing && ( + + } + onClick={() => onSaveTagForApp(tag)} + /> + + } + onClick={() => { + onStartEdit(); + setIsSelfEditing(true); + }} + /> + + } + onClick={() => handleConfirmDelete()} + /> + + )} + + + + ); +}); + +const SaveTagForAppSubPanel = ({ + tag, + onClose, + onRefresh +}: { + tag: ChatFavouriteTagType; + onClose: () => void; + onRefresh: () => Promise; +}) => { + const { t } = useTranslation(); + + const { register, watch } = useForm<{ name: string }>({ + defaultValues: { + name: '' + } + }); + const searchAppName = watch('name'); + // search favourite apps for list rendering (only favourites, not all apps) + const { data: visibleFavourites = [], loading: isSearching } = useRequest2( + async () => { + return await getFavouriteApps({ name: searchAppName }); + }, + { + manual: false, + throttleWait: 500, + refreshDeps: [searchAppName] + } + ); + + // load all favourites for checked state and saving + const { data: favouriteApps = [] } = useRequest2( + async () => { + return await getFavouriteApps({ name: '' }); + }, + { + manual: false + } + ); + + const [localAllFavourites, setLocalAllFavourites] = useState([]); + + useEffect(() => { + setLocalAllFavourites(favouriteApps); + }, [favouriteApps]); + + const checkedAppIds = useMemo(() => { + return (localAllFavourites || []) + .filter((fav) => Array.isArray(fav.favouriteTags) && fav.favouriteTags.includes(tag.id)) + .map((fav) => fav.appId); + }, [localAllFavourites, tag.id]); + + const isAppChecked = useCallback( + (appId: string) => { + const f = (localAllFavourites || []).find((f) => f.appId === appId); + return Array.isArray(f?.favouriteTags) && f.favouriteTags.includes(tag.id); + }, + [localAllFavourites, tag.id] + ); + + const toggleAppChecked = useCallback( + (appId: string) => { + setLocalAllFavourites((prev) => + (prev || []).map((item) => { + if (item.appId !== appId) return item; + const tags: string[] = Array.isArray(item.favouriteTags) ? [...item.favouriteTags] : []; + const idx = tags.indexOf(tag.id); + if (idx >= 0) { + tags.splice(idx, 1); + } else { + tags.push(tag.id); + } + return { ...item, favouriteTags: tags }; + }) + ); + }, + [tag.id] + ); + + // save apps (update tags) via updateFavouriteApps + const { loading: isSaving, runAsync: saveApps } = useRequest2( + async () => { + await updateFavouriteAppTags( + localAllFavourites.map((item) => ({ id: item._id, tags: item.favouriteTags })) + ); + }, + { + manual: true, + onSuccess: async () => { + await onRefresh(); + onClose(); + } + } + ); + + return ( + + + + + } + onClick={onClose} + /> + + + + {tag.name} + + ({checkedAppIds.length}) + + + + + + + + + + + + + + + + + {visibleFavourites.length > 0 ? ( + + {visibleFavourites.map((fav: any) => ( + toggleAppChecked(fav.appId)} + > + { + e.stopPropagation(); + toggleAppChecked(fav.appId); + }} + size="sm" + /> + + + + {fav.name || fav.appId} + + + + ))} + + ) : ( + + + + )} + + ); +}; + +type Props = { + onClose: () => void; + onRefresh: () => Promise; +}; + +const TagManageModal = ({ onClose, onRefresh }: Props) => { + const { t } = useTranslation(); + + const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + + // get tags from db + const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); + // local editable tags list + const [localTags, setLocalTags] = useState(tags); + + // control the editable state + const [isEditing, setIsEditing] = useState([]); + + // update tags + const { loading: isUpdating, runAsync: updateTags } = useRequest2( + async (nextTags: ChatFavouriteTagType[]) => { + await updateChatSetting({ favouriteTags: nextTags }); + }, + { + manual: true, + onSuccess: async () => { + await refreshChatSetting(); + // after successful update, exit all editing states + setIsEditing([]); + } + } + ); + + // handle click new tag button + const handleClickNewTag = () => { + const id = getNanoid(8); + const next = [{ id, name: '' }, ...localTags]; + setLocalTags(next as ChatFavouriteTagType[]); + setIsEditing((prev) => [...prev, id]); + }; + + // handle commit updated tag to server + const handleCommitTag = useCallback( + async (updated: ChatFavouriteTagType) => { + // compute next tags deterministically and use it for both state and request + const next = localTags.map((c) => (c.id === updated.id ? updated : c)); + setLocalTags(next); + setIsEditing((prev) => prev.filter((v) => v !== updated.id)); + await updateTags(next); + }, + [localTags, updateTags] + ); + + const handleCancelNewTag = useCallback((target: ChatFavouriteTagType) => { + setLocalTags((prev) => prev.filter((c) => c.id !== target.id)); + setIsEditing((prev) => prev.filter((v) => v !== target.id)); + }, []); + + const handleExitEdit = useCallback((target: ChatFavouriteTagType) => { + setIsEditing((prev) => prev.filter((v) => v !== target.id)); + }, []); + // delete tag + const { loading: isDeleting, runAsync: deleteTag } = useRequest2( + async (target: ChatFavouriteTagType) => { + const next = localTags.filter((c) => c.id !== target.id); + setLocalTags(next); + await updateTags(next); + }, + { + manual: true + } + ); + + const { + isOpen: isSaveTagForAppSubPanelOpen, + onOpen: onOpenSaveTagForAppSubPanel, + onClose: onCloseSaveTagForAppSubPanel + } = useDisclosure(); + + const [currentSaveTagForApp, setCurrentSaveTagForApp] = useState( + null + ); + + const handleOpenSaveTagForAppSubPanel = useCallback( + (tag: ChatFavouriteTagType) => { + setCurrentSaveTagForApp(tag); + onOpenSaveTagForAppSubPanel(); + }, + [onOpenSaveTagForAppSubPanel] + ); + + const isLoading = isUpdating || isDeleting || isEditing.length > 0; + + // counts + const { data: allFavourites = [] } = useRequest2( + async () => { + return await getFavouriteApps({ name: '' }); + }, + { + manual: false, + refreshDeps: [isSaveTagForAppSubPanelOpen] + } + ); + const tagIdToCount = useMemo(() => { + const map = new Map(); + (allFavourites || []).forEach((fav: any) => { + const tags: string[] = Array.isArray(fav?.favouriteTags) ? fav.favouriteTags : []; + tags.forEach((tid) => map.set(tid, (map.get(tid) || 0) + 1)); + }); + return map; + }, [allFavourites]); + + return ( + <> + + {isSaveTagForAppSubPanelOpen ? ( + + ) : ( + + + + + + + {t('chat:setting.favourite.categories_modal.title', { + num: localTags.length + })} + + + + + + + + {localTags.length > 0 ? ( + + + dataList={localTags} + renderInnerPlaceholder={false} + onDragEndCb={(list) => { + setLocalTags(list); + updateTags(list); + }} + > + {({ provided }) => ( + + {localTags.map((tag, index) => ( + + {(provided, snapshot) => ( + + + + + + + setIsEditing((prev) => [...prev, tag.id])} + onConfirmDelete={(c) => deleteTag(c)} + onSaveTagForApp={handleOpenSaveTagForAppSubPanel} + /> + + + + )} + + ))} + {provided.placeholder} + + )} + + + ) : ( + + + + )} + + )} + + + ); +}; + +export default React.memo(TagManageModal); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx new file mode 100644 index 000000000000..8daec2228a0f --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx @@ -0,0 +1,377 @@ +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { + Button, + ButtonGroup, + Flex, + HStack, + IconButton, + Input, + InputGroup, + InputLeftElement, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useDisclosure +} from '@chakra-ui/react'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useContextSelector } from 'use-context-selector'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { AddIcon } from '@chakra-ui/icons'; +import { deleteFavouriteApp, getFavouriteApps, updateFavouriteAppOrder } from '@/web/core/chat/api'; +import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { Box, Wrap } from '@chakra-ui/react'; +import type { ChatFavouriteApp } from '@fastgpt/global/core/chat/favouriteApp/type'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import MyPopover from '@fastgpt/web/components/common/MyPopover'; +import type { ChatFavouriteTagType } from '@fastgpt/global/core/chat/setting/type'; +import dynamic from 'next/dynamic'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; + +const TagManageModal = dynamic( + () => import('@/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal') +); +const AddFavouriteAppModal = dynamic( + () => import('@/pageComponents/chat/ChatSetting/FavouriteAppSetting/AddFavouriteAppModal') +); + +type Props = { + Header: React.FC<{ children?: React.ReactNode }>; +}; + +const FavouriteAppSetting = ({ Header }: Props) => { + const { t } = useTranslation(); + + // search apps input + const { + register, + setValue: setSearchValue, + watch: watchSearchValue + } = useForm<{ search: string; tag: string }>({ + defaultValues: { + search: '', + tag: '' + } + }); + + const searchAppNameValue = watchSearchValue('search'); + + const searchAppTagValue = watchSearchValue('tag'); + // apps' tags options + const tagOptions = useContextSelector(ChatSettingContext, (v) => { + const tags = v.chatSettings?.favouriteTags || []; + return [ + { label: t('chat:setting.favourite.category_all'), value: '' }, + ...tags.map((c) => ({ label: c.name, value: c.id })) + ]; + }); + // app's tags cache map + const tagMap = useContextSelector(ChatSettingContext, (v) => + (v.chatSettings?.favouriteTags || []).reduce>( + (acc, tag) => { + acc[tag.id] = { ...tag }; + return acc; + }, + {} + ) + ); + + const [localFavourites, setLocalFavourites] = useState([]); + + // search favourite apps by apps' name and tag + const { loading: isSearching, runAsync: getApps } = useRequest2( + async () => { + const apps = await getFavouriteApps({ + name: searchAppNameValue, + tag: searchAppTagValue + }); + + setLocalFavourites(apps); + }, + { + manual: false, + throttleWait: 500, + refreshDeps: [searchAppNameValue, searchAppTagValue] + } + ); + + // update app order + const { runAsync: orderApp } = useRequest2( + async (list: ChatFavouriteApp[]) => { + await updateFavouriteAppOrder( + list.map((item, idx) => ({ + id: item._id, + order: idx + })) + ); + getApps(); + }, + { manual: true } + ); + + // delete app + const { runAsync: deleteApp } = useRequest2( + async (id: string) => { + await deleteFavouriteApp(id); + getApps(); + }, + { manual: true } + ); + + // open tag manage modal + const { + isOpen: isOpenTagManageModal, + onOpen: onOpenTagManageModal, + onClose: onCloseTagManageModal + } = useDisclosure(); + + // open add app modal + const { + isOpen: isOpenAddAppModal, + onOpen: onOpenAddAppModal, + onClose: onCloseAddAppModal + } = useDisclosure(); + + const TagBox = ({ id }: { id: string }) => { + const tag = tagMap[id]; + + if (!tag) return null; + + return ( + e.stopPropagation()} + > + {tag.name} + + ); + }; + + return ( + <> + +
+ + + + + + + + + setSearchValue('tag', tag)} + /> + + + + + +
+ + + + + + + + + + + + + + + dataList={localFavourites} + renderInnerPlaceholder={false} + onDragEndCb={(list) => { + const next = list.map((item, idx) => ({ ...item, order: idx })); + setLocalFavourites(next); + orderApp(next); + }} + > + {({ provided }) => ( + + {localFavourites.map((row, index) => ( + + {(provided, snapshot) => ( + + {/* drag handle */} + + + {/* name */} + + + {/* intro */} + + + {/* tags */} + + + {/* action */} + + + )} + + ))} + {provided.placeholder} + + )} + +
{t('chat:setting.favourite.table_column_name')}{t('chat:setting.favourite.table_column_intro')}{t('chat:setting.favourite.table_column_category')} + {t('chat:setting.favourite.table_column_action')} +
+ + + + + + + {row.name || ''} + + + + {row.intro || ''} + + + + {row.favouriteTags.slice(0, 3).map((id) => ( + + ))} + + {row.favouriteTags.length > 3 && ( + e.stopPropagation()} + > + +{row.favouriteTags.length - 3} + + } + > + {() => ( + e.stopPropagation()} + > + {row.favouriteTags.slice(3).map((id) => ( + + ))} + + )} + + )} + + + { + setLocalFavourites((prev) => { + const next = prev.filter((_, i) => i !== index); + // reset order + const ordered = next.map((item, idx) => ({ + ...item, + order: idx + })); + deleteApp(row._id); + return ordered; + }); + }} + Trigger={ + } + /> + } + /> +
+ {localFavourites.length === 0 && } +
+
+ + {isOpenTagManageModal && ( + + )} + + {isOpenAddAppModal && ( + + )} + + ); +}; + +export default FavouriteAppSetting; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/AddQuickAppModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/AddQuickAppModal.tsx new file mode 100644 index 000000000000..acd1ab104d4a --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/AddQuickAppModal.tsx @@ -0,0 +1,400 @@ +import { getMyApps, getAppBasicInfoByIds } from '@/web/core/app/api'; +import { Box, Button, Grid, GridItem, HStack, VStack, Flex, Checkbox } from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { App } from '@/pageComponents/chat/ChatSetting/AppTree'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { QuickAppType } from '@fastgpt/global/core/chat/setting/type'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import FolderPath from '@/components/common/folder/Path'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { getAppFolderPath } from '@/web/core/app/api/app'; +import { ChevronRightIcon } from '@chakra-ui/icons'; + +type Props = { + selectedIds: string[]; + onClose: () => void; + onConfirm: (list: QuickAppType[]) => void; +}; + +const AddQuickAppModal = ({ selectedIds, onClose, onConfirm }: Props) => { + const { t } = useTranslation(); + + const [localSelectedIds, setLocalSelectedIds] = useState(selectedIds); + + const [selectedInfo, setSelectedInfo] = useState>({}); + + const { watch, setValue } = useForm<{ name: string }>({ + defaultValues: { + name: '' + } + }); + const searchAppName = watch('name'); + + const [parentId, setParentId] = useState(''); + + const { + data: appData = { apps: [], paths: [] as { parentId: string; parentName: string }[] }, + loading: isFetching + } = useRequest2( + async () => { + const [apps, paths] = await Promise.all([ + getMyApps({ + parentId, + searchKey: searchAppName, + type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow] + }), + searchAppName.trim() + ? Promise.resolve([]) + : getAppFolderPath({ sourceId: parentId, type: 'current' }) + ]); + return { apps, paths }; + }, + { + manual: false, + throttleWait: 500, + refreshDeps: [parentId, searchAppName] + } + ); + const availableApps = appData.apps; + const paths = appData.paths; + + const availableAppsMap = useMemo(() => { + const map = new Map(); + availableApps.forEach((app) => map.set(app._id, app)); + return map; + }, [availableApps]); + + const handleCheck = useCallback( + (id: string) => { + setLocalSelectedIds((prev) => { + const exists = prev.includes(id); + if (exists) { + // remove id and its cached info + setSelectedInfo((old) => { + const next: Record = { ...old }; + delete next[id]; + return next; + }); + return prev.filter((v) => v !== id); + } + if (prev.length >= 4) return prev; + // add id and cache its info if available from current list + const app = availableAppsMap.get(id); + if (app) { + setSelectedInfo((old) => ({ + ...old, + [id]: { _id: id, name: app.name, avatar: app.avatar } + })); + } + return [...prev, id]; + }); + }, + [availableAppsMap] + ); + + const checkedQuickApps = useMemo(() => { + return localSelectedIds + .map((id) => { + const cached = selectedInfo[id]; + if (cached) return cached; + + const app = availableAppsMap.get(id); + if (app) return { _id: app._id, name: app.name, avatar: app.avatar }; + }) + .filter(Boolean) as QuickAppType[]; + }, [localSelectedIds, selectedInfo, availableAppsMap]); + + useEffect(() => { + const missing = localSelectedIds.filter((id) => !selectedInfo[id]); + if (missing.length === 0) return; + getAppBasicInfoByIds(missing) + .then((list) => { + setSelectedInfo((old) => { + const next: Record = { ...old }; + list.forEach((item) => { + next[item.id] = { _id: item.id, name: item.name, avatar: item.avatar }; + }); + return next; + }); + }) + .catch(() => {}); + }, [localSelectedIds, selectedInfo]); + + const { loading: isUpdating, runAsync: confirmSelect } = useRequest2( + async () => { + onConfirm(checkedQuickApps); + }, + { + refreshDeps: [checkedQuickApps], + manual: true, + onSuccess: onClose + } + ); + + return ( + + + + + + + + { + const v = e.target.value; + setValue('name', v); + }} + size="md" + /> + + + + {searchAppName && ( + + {t('chat:search_results')} + + )} + {!searchAppName && paths.length === 0 && ( + + setParentId('')} + > + {t('common:root_folder')} + + + + )} + {!searchAppName && paths.length > 0 && ( + ({ parentId: p.parentId, parentName: p.parentName }))} + FirstPathDom={t('common:root_folder')} + onClick={(e) => setParentId(e)} + /> + )} + + + + {availableApps.length === 0 && !isFetching && ( + + )} + {availableApps.map((item: App) => ( + + { + if (item.type === AppTypeEnum.folder) { + if (searchAppName) { + setValue('name', ''); + } + setParentId(String(item._id)); + } else { + handleCheck(String(item._id)); + } + }} + > + e.stopPropagation()}> + {item.type !== AppTypeEnum.folder && ( + handleCheck(String(item._id))} + colorScheme="blue" + size="sm" + /> + )} + + + + + + + {item.name} + + + {item.type === AppTypeEnum.folder ? t('common:Folder') : ''} + + + + {item.type === AppTypeEnum.folder && ( + + + + )} + + + ))} + + + + + + + + {t('chat:setting.favourite.selected_list', { + num: `${checkedQuickApps.length} / 4` + })} + + + + {checkedQuickApps.length === 0 && !isFetching && ( + + )} + + dataList={checkedQuickApps} + renderInnerPlaceholder={false} + onDragEndCb={(list) => { + const newOrderIds = list.map((item) => item._id); + setLocalSelectedIds(newOrderIds); + }} + > + {({ provided }) => ( + + {checkedQuickApps.map((q, index) => { + const app = selectedInfo[q._id] || { + _id: q._id, + name: q.name, + avatar: q.avatar + }; + return ( + + {(provided, snapshot) => ( + + + + + + + + {app.name} + + + + handleCheck(q._id)} + /> + + + )} + + ); + })} + {provided.placeholder} + + )} + + + + + + + + + + + + + + ); +}; + +export default AddQuickAppModal; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx similarity index 80% rename from projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx rename to projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx index c643d914e8cf..52fed72e612d 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex, Grid, Input } from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, IconButton, Input, useDisclosure } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import MyInput from '@/components/MyInput'; import { useCallback, useState } from 'react'; @@ -9,9 +9,10 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import ImageUpload from '@/pageComponents/chat/ChatSetting/ImageUpload'; import type { ChatSettingSchema, - ChatSettingUpdateParams + ChatSettingUpdateParams, + QuickAppType, + SelectedToolType } from '@fastgpt/global/core/chat/setting/type'; -import NextHead from '@/components/common/NextHead'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import ToolSelectModal from '@/pageComponents/chat/ChatSetting/ToolSelectModal'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d'; @@ -25,14 +26,21 @@ import { DEFAULT_LOGO_BANNER_URL } from '@/pageComponents/chat/constants'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import dynamic from 'next/dynamic'; +import type { ChatSettingReturnType } from '@fastgpt/global/core/chat/setting/type'; + +const AddQuickAppModal = dynamic( + () => import('@/pageComponents/chat/ChatSetting/HomepageSetting/AddQuickAppModal') +); type Props = { Header: React.FC<{ children?: React.ReactNode }>; onDiagramShow: (show: boolean) => void; }; -type FormValues = Omit & { - selectedTools: ChatSettingSchema['selectedTools']; +type FormValues = Omit & { + selectedTools: SelectedToolType[]; + quickAppList: QuickAppType[]; }; const HomepageSetting = ({ Header, onDiagramShow }: Props) => { @@ -44,14 +52,15 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); const chatSettings2Form = useCallback( - (data?: ChatSettingSchema) => { + (data?: ChatSettingReturnType) => { return { slogan: data?.slogan || t('chat:setting.home.slogan.default'), dialogTips: data?.dialogTips || t('chat:setting.home.dialogue_tips.default'), homeTabTitle: data?.homeTabTitle || 'FastGPT', selectedTools: data?.selectedTools || [], wideLogoUrl: data?.wideLogoUrl, - squareLogoUrl: data?.squareLogoUrl + squareLogoUrl: data?.squareLogoUrl, + quickAppList: data?.quickAppList || [] }; }, [t] @@ -63,6 +72,7 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { const wideLogoUrl = watch('wideLogoUrl'); const squareLogoUrl = watch('squareLogoUrl'); + const formQuickApps = watch('quickAppList'); useMount(async () => { reset(chatSettings2Form(await refreshChatSetting())); @@ -105,8 +115,10 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { const { runAsync: onSubmit, loading: isSaving } = useRequest2( async (values: FormValues) => { + const { quickAppList, ...params } = values; return updateChatSetting({ - ...values, + ...params, + quickAppIds: quickAppList.map((q) => q._id), selectedTools: values.selectedTools.map((tool) => ({ pluginId: tool.pluginId, inputs: tool.inputs @@ -121,19 +133,16 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { } ); + const { + isOpen: isOpenAddQuickApp, + onOpen: onOpenAddQuickApp, + onClose: onCloseAddQuickApp + } = useDisclosure(); + return ( - +
{ > + {/* QUICK APPS */} + + + {t('chat:setting.home.quick_apps')} + + + + + {(formQuickApps || []).length > 0 ? ( + + {formQuickApps.map((q) => ( + + + {q.name} + + ))} + + ) : ( + + {t('chat:setting.home.quick_apps.placeholder')} + + )} + + + } + aria-label="add quick apps" + variant="ghost" + size="sm" + color="primary.700" + onClick={onOpenAddQuickApp} + /> + + + {/* AVAILABLE TOOLS */} { + + {isOpenAddQuickApp && ( + q._id)} + onClose={onCloseAddQuickApp} + onConfirm={(list) => setValue('quickAppList', list)} + /> + )} ); }; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx index b17673ba4dc1..5a7cbe6987c0 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx @@ -28,18 +28,11 @@ const LogDetails = ({ Header }: Props) => { } = useMultipleSelect(Object.values(ChatSourceEnum), true); return ( - +
void; + tab: ChatSettingTabOptionEnum; + onTabChange: (tab: ChatSettingTabOptionEnum) => void; children?: React.ReactNode; }; -const SettingTabs = ({ tab, children, onChange }: Props) => { +const SettingTabs = ({ tab, children, onTabChange }: Props) => { const { t } = useTranslation(); - const tabOptions: Parameters>[0]['list'] = - useMemo( - () => [ - { label: t('chat:setting.home.title'), value: ChatSettingTabOptionEnum.HOME }, - { - label: t('chat:setting.data_dashboard.title'), - value: ChatSettingTabOptionEnum.DATA_DASHBOARD - }, - { label: t('chat:setting.log_details.title'), value: ChatSettingTabOptionEnum.LOG_DETAILS } - ], - [t] - ); + const tabOptions: Parameters>[0]['list'] = useMemo( + () => [ + { + label: t('chat:setting.home.title'), + value: ChatSettingTabOptionEnum.HOME + }, + { + label: t('chat:setting.data_dashboard.title'), + value: ChatSettingTabOptionEnum.DATA_DASHBOARD + }, + { + label: t('chat:setting.log_details.title'), + value: ChatSettingTabOptionEnum.LOG_DETAILS + }, + { + label: t('chat:setting.favourite.title'), + value: ChatSettingTabOptionEnum.FAVOURITE_APPS + } + ], + [t] + ); return ( - - + + {children} diff --git a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx index 9eb33c1b8c26..ca8faaf27117 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx @@ -1,10 +1,10 @@ import DiagramModal from '@/pageComponents/chat/ChatSetting/DiagramModal'; -import { type PropsWithChildren, useCallback, useState } from 'react'; -import { ChatSettingTabOptionEnum } from '@/pageComponents/chat/constants'; +import { type PropsWithChildren, useCallback, useMemo, useState } from 'react'; +import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import dynamic from 'next/dynamic'; import SettingTabs from '@/pageComponents/chat/ChatSetting/SettingTabs'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import { Flex } from '@chakra-ui/react'; +import { Box, Flex, type FlexProps } from '@chakra-ui/react'; import { useContextSelector } from 'use-context-selector'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -12,66 +12,107 @@ import NextHead from '@/components/common/NextHead'; import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import { useTranslation } from 'react-i18next'; +import { useMount } from 'ahooks'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useRouter } from 'next/router'; const HomepageSetting = dynamic(() => import('@/pageComponents/chat/ChatSetting/HomepageSetting')); const LogDetails = dynamic(() => import('@/pageComponents/chat/ChatSetting/LogDetails')); const DataDashboard = dynamic(() => import('@/pageComponents/chat/ChatSetting/DataDashboard')); +const FavouriteAppSetting = dynamic( + () => import('@/pageComponents/chat/ChatSetting/FavouriteAppSetting') +); const ChatSetting = () => { - const { t } = useTranslation(); + const router = useRouter(); const { isPc } = useSystem(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { tab: tabQuery } = router.query as { tab: ChatSettingTabOptionEnum }; const [isOpenDiagram, setIsOpenDiagram] = useState(false); - const [tab, setTab] = useState<`${ChatSettingTabOptionEnum}`>('home'); - + const tab = useMemo( + () => + Object.values(ChatSettingTabOptionEnum).includes(tabQuery) + ? tabQuery + : ChatSettingTabOptionEnum.HOME, + [tabQuery] + ); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + + const handleTabChange = useCallback( + (tab: ChatSettingTabOptionEnum) => { + handlePaneChange(ChatSidebarPaneEnum.SETTING, undefined, tab); + }, + [handlePaneChange] + ); const SettingHeader = useCallback( ({ children }: PropsWithChildren) => ( - + {children} ), - [tab, setTab] + [tab, handleTabChange] ); + useMount(() => { + if (!feConfigs?.isPlus) { + handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); + } + }); + return ( <> - {!isPc && ( - <> - - + {!isPc && ( + <> + + + + + - - - - - )} - - {/* homepage setting */} - {tab === ChatSettingTabOptionEnum.HOME && ( - - )} - - {/* data dashboard */} - {tab === ChatSettingTabOptionEnum.DATA_DASHBOARD && } - - {/* log details */} - {tab === ChatSettingTabOptionEnum.LOG_DETAILS && } + + )} + + {chatSettings && ( + + {/* homepage setting */} + {tab === ChatSettingTabOptionEnum.HOME && ( + + )} + + {/* data dashboard */} + {tab === ChatSettingTabOptionEnum.DATA_DASHBOARD && ( + + )} + + {/* log details */} + {tab === ChatSettingTabOptionEnum.LOG_DETAILS && } + + {/* home chat logs */} + {tab === ChatSettingTabOptionEnum.FAVOURITE_APPS && ( + + )} + + )} + diff --git a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx index a26477eb76db..0ec33549fb26 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx @@ -25,6 +25,10 @@ import { ChatSidebarPaneEnum } from '../constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; +import dynamic from 'next/dynamic'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; + +const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox')); type Props = { myApps: AppListItemType[]; @@ -33,7 +37,6 @@ type Props = { const AppChatWindow = ({ myApps }: Props) => { const { userInfo } = useUserStore(); const { chatId, appId, outLinkAuthData } = useChatStore(); - const { feConfigs } = useSystemStore(); const { t } = useTranslation(); const { isPc } = useSystem(); @@ -41,6 +44,8 @@ const AppChatWindow = ({ myApps }: Props) => { const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat); const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); + const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin); + const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); @@ -103,7 +108,7 @@ const AppChatWindow = ({ myApps }: Props) => { onMessage: generatingMessage }); - const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]); + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats({ messages: histories })[0]); onUpdateHistoryTitle({ chatId, newTitle }); setChatBoxData((state) => ({ @@ -153,16 +158,26 @@ const AppChatWindow = ({ myApps }: Props) => { /> - + {isPlugin ? ( + onChangeChatId(getNanoid())} + onStartChat={onStartChat} + /> + ) : ( + + )} diff --git a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx index 75bfd6d05f2a..bbbadc45ad1f 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx @@ -16,7 +16,7 @@ import { ChatContext } from '@/web/core/chat/context/chatContext'; import { useContextSelector } from 'use-context-selector'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; -import React, { useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect, useRef, useState } from 'react'; import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type'; import { streamFetch } from '@/web/common/api/fetch'; import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils'; @@ -47,6 +47,7 @@ import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatSidebarPaneEnum } from '../constants'; import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; +import type { QuickAppType } from '@fastgpt/global/core/chat/setting/type'; type Props = { myApps: AppListItemType[]; @@ -65,7 +66,8 @@ const defaultWhisperConfig: AppWhisperConfigType = { }; const HomeChatWindow = ({ myApps }: Props) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + const language = i18n.language; const { isPc } = useSystem(); const { userInfo } = useUserStore(); @@ -74,6 +76,7 @@ const HomeChatWindow = ({ myApps }: Props) => { const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat); const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); + const onChangeGlobalAppId = useContextSelector(ChatContext, (v) => v.onChangeAppId); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); @@ -83,10 +86,16 @@ const HomeChatWindow = ({ myApps }: Props) => { const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const homeAppId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); + const isQuickApp = useMemo( + () => chatSettings?.quickAppList.some((app) => app._id === appId), + [chatSettings?.quickAppList, appId] + ); + const availableModels = useMemo( () => llmModelList.map((model) => ({ value: model.model, label: model.name })), [llmModelList] @@ -126,23 +135,26 @@ const HomeChatWindow = ({ myApps }: Props) => { const modelData = getWebLLMModel(selectedModel); const res = await getInitChatInfo({ appId, chatId }); res.userAvatar = userInfo?.avatar; - if (!res.app.chatConfig) { - res.app.chatConfig = { - fileSelectConfig: { + + if (!isQuickApp) { + if (!res.app.chatConfig) { + res.app.chatConfig = { + fileSelectConfig: { + ...defaultFileSelectConfig, + canSelectImg: !!modelData.vision + }, + whisperConfig: defaultWhisperConfig + }; + } else { + res.app.chatConfig.fileSelectConfig = { ...defaultFileSelectConfig, canSelectImg: !!modelData.vision - }, - whisperConfig: defaultWhisperConfig - }; - } else { - res.app.chatConfig.fileSelectConfig = { - ...defaultFileSelectConfig, - canSelectImg: !!modelData.vision - }; - res.app.chatConfig.whisperConfig = { - ...defaultWhisperConfig, - open: true - }; + }; + res.app.chatConfig.whisperConfig = { + ...defaultWhisperConfig, + open: true + }; + } } setChatBoxData(res); @@ -169,13 +181,21 @@ const HomeChatWindow = ({ myApps }: Props) => { } ); + const handleSwitchQuickApp = async (id: string) => { + if (isQuickApp && appId === id) { + onChangeGlobalAppId(homeAppId); + return; + } + onChangeGlobalAppId(id); + }; + useMount(() => { if (!feConfigs?.isPlus) { handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); } }); - // 使用类似AppChatWindow的对话逻辑 + // 使用类似 AppChatWindow 的对话逻辑 const onStartChat = useMemoizedFn( async ({ messages, @@ -184,13 +204,38 @@ const HomeChatWindow = ({ myApps }: Props) => { responseChatItemId, generatingMessage }: StartChatFnProps) => { + const histories = messages.slice(-1); + + // using original workflow of quick app + if (isQuickApp && appId) { + const { responseText } = await streamFetch({ + data: { + messages: histories, + variables, + responseChatItemId, + appId, + chatId + }, + abortCtrl: controller, + onMessage: generatingMessage + }); + + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats({ messages: histories })[0]); + + onUpdateHistoryTitle({ chatId, newTitle }); + setChatBoxData((state) => ({ + ...state, + title: newTitle + })); + + return { responseText, isNewChat: forbidLoadChat.current }; + } + + // not quick app, using model and tools selected on home page if (!selectedModel) { return Promise.reject('No model selected'); } - const histories = messages.slice(-1); - - // 根据所选工具 ID 动态拉取节点,并填充默认输入 const tools: FlowNodeTemplateType[] = await Promise.all( selectedToolIds.map(async (toolId) => { const node = await getPreviewPluginNode({ appId: toolId }); @@ -223,7 +268,7 @@ const HomeChatWindow = ({ myApps }: Props) => { abortCtrl: controller }); - const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]); + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats({ messages: histories })[0]); onUpdateHistoryTitle({ chatId, newTitle }); setChatBoxData((state) => ({ @@ -237,104 +282,104 @@ const HomeChatWindow = ({ myApps }: Props) => { // 自定义按钮组(模型选择和工具选择) const InputLeftComponent = useMemo( - () => ( - <> - {/* 模型选择 */} - {availableModels.length > 0 && ( - - { - setChatBoxData((state) => ({ - ...state, - app: { - ...state.app, - chatConfig: { - ...state.app.chatConfig, - fileSelectConfig: { - ...defaultFileSelectConfig, - canSelectImg: !!getWebLLMModel(model).vision + () => + isQuickApp ? undefined : ( + <> + {/* 模型选择 */} + {availableModels.length > 0 && ( + + { + setChatBoxData((state) => ({ + ...state, + app: { + ...state.app, + chatConfig: { + ...state.app.chatConfig, + fileSelectConfig: { + ...defaultFileSelectConfig, + canSelectImg: !!getWebLLMModel(model).vision + } } } - } - })); - setSelectedModel(model); - }} - /> - - )} - - {/* 工具选择下拉框 */} - {availableTools.length > 0 && ( - - } - flexShrink={0} - _active={{ - transform: 'none' - }} - {...(selectedTools.length > 0 && { - color: 'primary.600', - bg: 'primary.50', - borderColor: 'primary.200' - })} - > - {isPc - ? selectedTools.length > 0 - ? t('chat:home.tools', { num: selectedTools.length }) - : t('chat:home.select_tools') - : `:${selectedTools.length}`} - - - {availableTools.map((tool) => { - const toolId = tool.pluginId || ''; - const isSelected = selectedToolIds.includes(toolId); - - return ( - { - e.stopPropagation(); - e.preventDefault(); - setSelectedToolIds( - selectedToolIds.includes(toolId) - ? selectedToolIds.filter((id) => id !== toolId) - : [...selectedToolIds, toolId] - ); - }} - closeOnSelect={false} - _hover={{ - bg: 'primary.50' - }} - _notLast={{ mb: 1 }} - borderRadius={'md'} - > - - - - {tool.name} - - - ); - })} - - - )} - - ), + })); + setSelectedModel(model); + }} + /> + + )} + + {/* 工具选择下拉框 */} + {availableTools.length > 0 && ( + + } + flexShrink={0} + _active={{ + transform: 'none' + }} + {...(selectedTools.length > 0 && { + color: 'primary.600', + bg: 'primary.50', + borderColor: 'primary.200' + })} + > + {isPc + ? selectedTools.length > 0 + ? t('chat:home.tools', { num: selectedTools.length }) + : t('chat:home.select_tools') + : `:${selectedTools.length}`} + + + {availableTools.map((tool) => { + const toolId = tool.pluginId || ''; + const isSelected = selectedToolIds.includes(toolId); + + return ( + { + e.stopPropagation(); + e.preventDefault(); + setSelectedToolIds( + selectedToolIds.includes(toolId) + ? selectedToolIds.filter((id) => id !== toolId) + : [...selectedToolIds, toolId] + ); + }} + closeOnSelect={false} + _hover={{ + bg: 'primary.50' + }} + _notLast={{ mb: 1 }} + borderRadius={'md'} + > + + + + {tool.name} + + + ); + })} + + + )} + + ), [ availableModels, selectedModel, @@ -345,7 +390,8 @@ const HomeChatWindow = ({ myApps }: Props) => { selectedToolIds, setSelectedToolIds, setChatBoxData, - isPc + isPc, + isQuickApp ] ); @@ -358,7 +404,7 @@ const HomeChatWindow = ({ myApps }: Props) => { {isPc ? ( @@ -409,12 +455,15 @@ const HomeChatWindow = ({ myApps }: Props) => { isReady={!loading} feedbackType={'user'} chatType={ChatTypeEnum.home} + slogan={chatSettings?.slogan} outLinkAuthData={outLinkAuthData} - onStartChat={onStartChat} - InputLeftComponent={InputLeftComponent} - dialogTips={chatSettings?.dialogTips} wideLogo={chatSettings?.wideLogoUrl} - slogan={chatSettings?.slogan} + dialogTips={chatSettings?.dialogTips} + InputLeftComponent={InputLeftComponent} + onStartChat={onStartChat} + quickAppList={chatSettings?.quickAppList || []} + currentQuickAppId={isQuickApp ? appId : undefined} + onSwitchQuickApp={handleSwitchQuickApp} /> diff --git a/projects/app/src/pageComponents/chat/constants.ts b/projects/app/src/pageComponents/chat/constants.ts index 7a01ca0ce1f0..20835ac70ac1 100644 --- a/projects/app/src/pageComponents/chat/constants.ts +++ b/projects/app/src/pageComponents/chat/constants.ts @@ -16,10 +16,10 @@ export type CollapseStatusType = 0 | 1; export const defaultCollapseStatus: CollapseStatusType = 0; // default expanded export enum ChatSettingTabOptionEnum { - HOME = 'home', - FAVORITE_APPS = 'favorite_apps', - DATA_DASHBOARD = 'data_dashboard', - LOG_DETAILS = 'log_details' + HOME = 'h', + DATA_DASHBOARD = 'd', + LOG_DETAILS = 'l', + FAVOURITE_APPS = 'f' } export const DEFAULT_LOGO_BANNER_URL = '/imgs/chat/fastgpt_banner.svg'; diff --git a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx index e71fb50d4d31..c468d84c266f 100644 --- a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx +++ b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx @@ -34,6 +34,7 @@ const ChatSliderHeader = ({ title, banner }: Props) => { const isHomePane = pane === ChatSidebarPaneEnum.HOME; const isTeamAppsPane = pane === ChatSidebarPaneEnum.TEAM_APPS; + const isFavouriteAppPane = pane === ChatSidebarPaneEnum.FAVORITE_APPS; return isPc ? ( @@ -88,6 +89,34 @@ const ChatSliderHeader = ({ title, banner }: Props) => { + { + handlePaneChange(ChatSidebarPaneEnum.FAVORITE_APPS); + onCloseSlider(); + setChatId(); + }} + > + + + + {t('chat:sidebar.favourite_apps')} + + + + { handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); diff --git a/projects/app/src/pageComponents/chat/SliderApps.tsx b/projects/app/src/pageComponents/chat/slider/index.tsx similarity index 83% rename from projects/app/src/pageComponents/chat/SliderApps.tsx rename to projects/app/src/pageComponents/chat/slider/index.tsx index 592a2cc0c20f..596d2cd00223 100644 --- a/projects/app/src/pageComponents/chat/SliderApps.tsx +++ b/projects/app/src/pageComponents/chat/slider/index.tsx @@ -2,7 +2,6 @@ import React, { useCallback } from 'react'; import type { BoxProps } from '@chakra-ui/react'; import { Flex, Box, HStack, Image } from '@chakra-ui/react'; import { motion, AnimatePresence } from 'framer-motion'; -import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { type AppListItemType } from '@fastgpt/global/core/app/type'; @@ -11,12 +10,6 @@ import { useUserStore } from '@/web/support/user/useUserStore'; import UserAvatarPopover from '@/pageComponents/chat/UserAvatarPopover'; import MyBox from '@fastgpt/web/components/common/MyBox'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import type { - GetResourceFolderListProps, - GetResourceListItemResponse -} from '@fastgpt/global/common/parentFolder/type'; -import { getMyApps } from '@/web/core/app/api'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { ChatSidebarPaneEnum, DEFAULT_LOGO_BANNER_COLLAPSED_URL, @@ -230,12 +223,18 @@ const ActionButton: React.FC<{ borderRadius={'8px'} alignItems={'center'} justifyContent={isCollapsed ? 'center' : 'flex-start'} - bg={isActive ? 'primary.100' : 'transparent'} - color={isActive ? 'primary.600' : 'myGray.500'} - _hover={{ - bg: isCollapsed ? 'myGray.200' : 'primary.100', - color: 'primary.600' - }} + {...(isActive + ? { + bg: 'primary.100', + color: 'primary.600' + } + : { + bg: 'transparent', + color: 'myGray.500', + _hover: { + bg: isCollapsed ? 'myGray.200' : 'primary.100' + } + })} onClick={onClick} > @@ -255,7 +254,6 @@ const ActionButton: React.FC<{ const NavigationSection = () => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); - const isProVersion = !!feConfigs.isPlus; const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); @@ -267,7 +265,11 @@ const NavigationSection = () => { ChatSettingContext, (v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS ); - const onHomeClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const isFavouriteAppsActive = useContextSelector( + ChatSettingContext, + (v) => v.pane === ChatSidebarPaneEnum.FAVORITE_APPS + ); + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); return ( @@ -275,50 +277,67 @@ const NavigationSection = () => { - {isProVersion && ( - - {isCollapsed ? ( - - onHomeClick(ChatSidebarPaneEnum.HOME)} - /> - - ) : ( - - onHomeClick(ChatSidebarPaneEnum.HOME)} - /> - - )} - - )} - {isCollapsed ? ( - onHomeClick(ChatSidebarPaneEnum.TEAM_APPS)} - /> + + {feConfigs.isPlus && ( + <> + handlePaneChange(ChatSidebarPaneEnum.HOME)} + /> + + handlePaneChange(ChatSidebarPaneEnum.FAVORITE_APPS)} + /> + + )} + + handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS)} + /> + ) : ( - onHomeClick(ChatSidebarPaneEnum.TEAM_APPS)} - /> + + {feConfigs.isPlus && ( + <> + handlePaneChange(ChatSidebarPaneEnum.HOME)} + /> + + handlePaneChange(ChatSidebarPaneEnum.FAVORITE_APPS)} + /> + + )} + + handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS)} + /> + )} @@ -459,7 +478,7 @@ const BottomSection = () => { ); }; -const SliderApps = ({ apps, activeAppId }: Props) => { +const ChatSlider = ({ apps, activeAppId }: Props) => { const { t } = useTranslation(); const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); @@ -467,9 +486,6 @@ const SliderApps = ({ apps, activeAppId }: Props) => { const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const isRecentlyUsedAppSelected = (id: string) => - pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && id === activeAppId; - return ( { }} animate={isCollapsed ? 'folded' : 'expanded'} initial={false} + userSelect={'none'} > @@ -517,10 +534,10 @@ const SliderApps = ({ apps, activeAppId }: Props) => { borderRadius={'md'} alignItems={'center'} fontSize={'sm'} - {...(isRecentlyUsedAppSelected(item._id) + {...(pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && item._id === activeAppId ? { bg: 'primary.100', color: 'primary.600' } : { - _hover: { bg: 'primary.100', color: 'primary.600' }, + _hover: { bg: 'primary.100' }, onClick: () => handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item._id) })} @@ -539,4 +556,4 @@ const SliderApps = ({ apps, activeAppId }: Props) => { ); }; -export default React.memo(SliderApps); +export default React.memo(ChatSlider); diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx index 1dde7fe0eaa1..97dbe638aff5 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx @@ -541,7 +541,7 @@ const TrainingStates = ({ } ); - const errorCounts = (Object.values(trainingDetail?.errorCounts || {}) as number[]).reduce( + const errorCounts = Object.values(trainingDetail?.errorCounts || {}).reduce( (acc, count) => acc + count, 0 ); diff --git a/projects/app/src/pages/api/core/ai/model/test.ts b/projects/app/src/pages/api/core/ai/model/test.ts index 974319619f7f..c6efb37fd5f0 100644 --- a/projects/app/src/pages/api/core/ai/model/test.ts +++ b/projects/app/src/pages/api/core/ai/model/test.ts @@ -9,14 +9,14 @@ import { type STTModelType, type TTSModelType } from '@fastgpt/global/core/ai/model.d'; -import { createChatCompletion, getAIApi } from '@fastgpt/service/core/ai/config'; +import { getAIApi } from '@fastgpt/service/core/ai/config'; import { addLog } from '@fastgpt/service/common/system/log'; import { getVectorsByText } from '@fastgpt/service/core/ai/embedding'; import { reRankRecall } from '@fastgpt/service/core/ai/rerank'; import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions'; import { isProduction } from '@fastgpt/global/common/system/constants'; import * as fs from 'fs'; -import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils'; +import { createLLMResponse } from '@fastgpt/service/core/ai/llm/request'; export type testQuery = { model: string; channelId?: number }; @@ -69,29 +69,17 @@ async function handler( export default NextAPI(handler); const testLLMModel = async (model: LLMModelItemType, headers: Record) => { - const requestBody = llmCompletionsBodyFormat( - { + const { answerText } = await createLLMResponse({ + body: { model: model.model, messages: [{ role: 'user', content: 'hi' }], stream: true }, - model - ); - - const { response } = await createChatCompletion({ - modelData: model, - body: requestBody, - options: { - headers: { - Accept: 'application/json, text/plain, */*', - ...headers - } - } + custonHeaders: headers }); - const { text: answer } = await formatLLMResponse(response); - if (answer) { - return answer; + if (answerText) { + return answerText; } return Promise.reject('Model response empty'); diff --git a/projects/app/src/pages/api/core/ai/optimizePrompt.ts b/projects/app/src/pages/api/core/ai/optimizePrompt.ts index b96f14e88b1b..5326c8bdec37 100644 --- a/projects/app/src/pages/api/core/ai/optimizePrompt.ts +++ b/projects/app/src/pages/api/core/ai/optimizePrompt.ts @@ -3,18 +3,15 @@ import { NextAPI } from '@/service/middleware/entry'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { responseWrite } from '@fastgpt/service/common/response'; import { sseErrRes } from '@fastgpt/service/common/response'; -import { createChatCompletion } from '@fastgpt/service/core/ai/config'; import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; -import { loadRequestMessages } from '@fastgpt/service/core/chat/utils'; -import { llmCompletionsBodyFormat, parseLLMStreamResponse } from '@fastgpt/service/core/ai/utils'; -import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index'; import { formatModelChars2Points } from '@fastgpt/service/support/wallet/usage/utils'; import { createUsage } from '@fastgpt/service/support/wallet/usage/controller'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { i18nT } from '@fastgpt/web/i18n/utils'; import { addLog } from '@fastgpt/service/common/system/log'; +import { createLLMResponse } from '@fastgpt/service/core/ai/llm/request'; type OptimizePromptBody = { originalPrompt: string; @@ -100,67 +97,17 @@ async function handler(req: ApiRequestProps, res: ApiRespons } ]; - const requestMessages = await loadRequestMessages({ - messages, - useVision: false - }); - - const { response, isStreamResponse } = await createChatCompletion({ - body: llmCompletionsBodyFormat( - { - model, - messages: requestMessages, - temperature: 0.1, - max_tokens: 2000, - stream: true - }, - model - ) - }); - - const { inputTokens, outputTokens } = await (async () => { - if (isStreamResponse) { - const { parsePart, getResponseData } = parseLLMStreamResponse(); - - let optimizedText = ''; - - for await (const part of response) { - const { responseContent } = parsePart({ - part, - parseThinkTag: true, - retainDatasetCite: false - }); - - if (responseContent) { - optimizedText += responseContent; - responseWrite({ - res, - event: SseResponseEventEnum.answer, - data: JSON.stringify({ - choices: [ - { - delta: { - content: responseContent - } - } - ] - }) - }); - } - } - - const { content: answer, usage } = getResponseData(); - return { - content: answer, - inputTokens: usage?.prompt_tokens || (await countGptMessagesTokens(requestMessages)), - outputTokens: - usage?.completion_tokens || - (await countGptMessagesTokens([{ role: 'assistant', content: optimizedText }])) - }; - } else { - const usage = response.usage; - const content = response.choices?.[0]?.message?.content || ''; - + const { + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model, + messages, + temperature: 0.1, + max_tokens: 2000, + stream: true + }, + onStreaming: ({ text }) => { responseWrite({ res, event: SseResponseEventEnum.answer, @@ -168,22 +115,15 @@ async function handler(req: ApiRequestProps, res: ApiRespons choices: [ { delta: { - content + content: text } } ] }) }); - - return { - content, - inputTokens: usage?.prompt_tokens || (await countGptMessagesTokens(requestMessages)), - outputTokens: - usage?.completion_tokens || - (await countGptMessagesTokens([{ role: 'assistant', content: content }])) - }; } - })(); + }); + responseWrite({ res, event: SseResponseEventEnum.answer, @@ -193,8 +133,7 @@ async function handler(req: ApiRequestProps, res: ApiRespons const { totalPoints, modelName } = formatModelChars2Points({ model, inputTokens, - outputTokens, - modelType: ModelTypeEnum.llm + outputTokens }); createUsage({ diff --git a/projects/app/src/pages/api/core/app/list.ts b/projects/app/src/pages/api/core/app/list.ts index 27811a196f45..b65d8b0cdbf6 100644 --- a/projects/app/src/pages/api/core/app/list.ts +++ b/projects/app/src/pages/api/core/app/list.ts @@ -217,6 +217,7 @@ async function handler(req: ApiRequestProps): Promise, res: NextApiRe ); // update app - await MongoApp.findByIdAndUpdate( - appId, + await MongoApp.updateOne( + { _id: appId }, { modules: nodes, edges, diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index e43e8ca103eb..761f361bcfcd 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -83,7 +83,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (!Array.isArray(edges)) { throw new Error('Edges is not array'); } - const chatMessages = GPTMessages2Chats(messages); + const chatMessages = GPTMessages2Chats({ messages }); // console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length); /* user auth */ diff --git a/projects/app/src/pages/api/core/chat/feedback/updateUserFeedback.ts b/projects/app/src/pages/api/core/chat/feedback/updateUserFeedback.ts index d8d6120f1e59..30ba3bc7c399 100644 --- a/projects/app/src/pages/api/core/chat/feedback/updateUserFeedback.ts +++ b/projects/app/src/pages/api/core/chat/feedback/updateUserFeedback.ts @@ -60,7 +60,7 @@ async function handler(req: ApiRequestProps, res: NextA return 0; })(); - await MongoAppChatLog.updateOne( + await MongoAppChatLog.findOneAndUpdate( { teamId, appId, diff --git a/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts b/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts index d656507c13cc..76b9abfd27d0 100644 --- a/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts +++ b/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts @@ -112,7 +112,7 @@ async function handler( { $match: { ...match, - retryCount: { $lte: 0 }, + // retryCount: { $lte: 0 }, errorMsg: { $exists: true } } }, diff --git a/projects/app/src/pages/api/system/pluginImgs/[...path].ts b/projects/app/src/pages/api/system/plugin/[...path].ts similarity index 95% rename from projects/app/src/pages/api/system/pluginImgs/[...path].ts rename to projects/app/src/pages/api/system/plugin/[...path].ts index 983868ce1ee4..d0744b87a8d6 100644 --- a/projects/app/src/pages/api/system/pluginImgs/[...path].ts +++ b/projects/app/src/pages/api/system/plugin/[...path].ts @@ -6,7 +6,7 @@ import { FastGPTPluginUrl } from '@fastgpt/service/common/system/constants'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { path = [] } = req.query as any; - const requestPath = `/imgs/tools/${path?.join('/')}`; + const requestPath = `/imgs/${path?.join('/')}`; if (!requestPath) { throw new Error('url is empty'); diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 8e8415c3086f..cd210e8a5707 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -132,7 +132,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { API params: chatId + [Human] API params: [histories, Human] */ - const chatMessages = GPTMessages2Chats(messages); + const chatMessages = GPTMessages2Chats({ messages }); // Computed start hook params const startHookText = (() => { diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index d5da272ecdb2..13e309066b12 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -132,7 +132,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { API params: chatId + [Human] API params: [histories, Human] */ - const chatMessages = GPTMessages2Chats(messages); + const chatMessages = GPTMessages2Chats({ messages }); // Computed start hook params const startHookText = (() => { diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index 9a676b484495..b3a70f98465b 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -3,7 +3,7 @@ import NextHead from '@/components/common/NextHead'; import { Box, Flex } from '@chakra-ui/react'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import PageContainer from '@/components/PageContainer'; -import SliderApps from '@/pageComponents/chat/SliderApps'; +import ChatSlider from '@/pageComponents/chat/slider'; import { serviceSideProps } from '@/web/common/i18n/utils'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { GetChatTypeEnum } from '@/global/core/chat/constants'; @@ -26,6 +26,7 @@ import { ChatSettingContextProvider } from '@/web/core/chat/context/chatSettingContext'; import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp'; +import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp'; const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { const { isPc } = useSystem(); @@ -49,7 +50,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { overflow={'hidden'} transition={'width 0.1s ease-in-out'} > - + )} @@ -58,12 +59,15 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { {/* home chat window */} {pane === ChatSidebarPaneEnum.HOME && } - {/* recently used apps chat window */} - {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } + {/* favourite apps */} + {pane === ChatSidebarPaneEnum.FAVORITE_APPS && } {/* team apps */} {pane === ChatSidebarPaneEnum.TEAM_APPS && } + {/* recently used apps chat window */} + {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } + {/* setting */} {pane === ChatSidebarPaneEnum.SETTING && } diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index b588d6f319d8..df6dc63d210c 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -173,7 +173,7 @@ const OutLink = (props: Props) => { abortCtrl: controller }); - const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]); + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats({ messages: histories })[0]); // new chat if (completionChatId !== chatId) { diff --git a/projects/app/src/service/core/dataset/queues/generateQA.ts b/projects/app/src/service/core/dataset/queues/generateQA.ts index 8238c31e03a7..26468efca564 100644 --- a/projects/app/src/service/core/dataset/queues/generateQA.ts +++ b/projects/app/src/service/core/dataset/queues/generateQA.ts @@ -1,7 +1,6 @@ import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; import { pushLLMTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller'; import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants'; -import { createChatCompletion } from '@fastgpt/service/core/ai/config'; import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; import { addLog } from '@fastgpt/service/common/system/log'; import { replaceVariable } from '@fastgpt/global/common/string/tools'; @@ -10,12 +9,6 @@ import type { PushDatasetDataChunkProps } from '@fastgpt/global/core/dataset/api import { getLLMModel } from '@fastgpt/service/core/ai/model'; import { checkTeamAiPointsAndLock } from './utils'; import { addMinutes } from 'date-fns'; -import { - countGptMessagesTokens, - countPromptTokens -} from '@fastgpt/service/common/string/tiktoken/index'; -import { loadRequestMessages } from '@fastgpt/service/core/chat/utils'; -import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { chunkAutoChunkSize, @@ -25,6 +18,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils'; import { text2Chunks } from '@fastgpt/service/worker/function'; import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller'; import { delay } from '@fastgpt/service/common/bullmq'; +import { createLLMResponse } from '@fastgpt/service/core/ai/llm/request'; const reduceQueue = () => { global.qaQueueLen = global.qaQueueLen > 0 ? global.qaQueueLen - 1 : 0; @@ -130,20 +124,17 @@ export async function generateQA(): Promise { } ]; - const { response: chatResponse } = await createChatCompletion({ - body: llmCompletionsBodyFormat( - { - model: modelData.model, - temperature: 0.3, - messages: await loadRequestMessages({ messages, useVision: false }), - stream: true - }, - modelData - ) + const { + answerText: answer, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model: modelData.model, + temperature: 0.3, + messages, + stream: true + } }); - const { text: answer, usage } = await formatLLMResponse(chatResponse); - const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(messages)); - const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); const qaArr = await formatSplitText({ answer, rawText: text, llmModel: modelData }); // 格式化后的QA对 @@ -181,7 +172,7 @@ export async function generateQA(): Promise { addLog.info(`[QA Queue] Finish`, { time: Date.now() - startTime, splitLength: qaArr.length, - usage + usage: { inputTokens, outputTokens } }); } catch (err: any) { addLog.error(`[QA Queue] Error`, err); diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts index ec9355693d63..7db003bb8c9e 100644 --- a/projects/app/src/service/support/mcp/utils.ts +++ b/projects/app/src/service/support/mcp/utils.ts @@ -38,7 +38,7 @@ import { saveChat } from '@fastgpt/service/core/chat/saveChat'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; -import { removeDatasetCiteText } from '@fastgpt/service/core/ai/utils'; +import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils'; export const pluginNodes2InputSchema = ( nodes: { flowNodeType: FlowNodeTypeEnum; inputs: FlowNodeInputItemType[] }[] diff --git a/projects/app/src/service/support/wallet/usage/push.ts b/projects/app/src/service/support/wallet/usage/push.ts index d0b627443788..bc5918f1739d 100644 --- a/projects/app/src/service/support/wallet/usage/push.ts +++ b/projects/app/src/service/support/wallet/usage/push.ts @@ -35,7 +35,6 @@ export const pushGenerateVectorUsage = ({ deepSearchOutputTokens?: number; }) => { const { totalPoints: totalVector, modelName: vectorModelName } = formatModelChars2Points({ - modelType: ModelTypeEnum.embedding, model, inputTokens }); @@ -47,7 +46,6 @@ export const pushGenerateVectorUsage = ({ extensionModelName: '' }; const { totalPoints, modelName } = formatModelChars2Points({ - modelType: ModelTypeEnum.llm, model: extensionModel, inputTokens: extensionInputTokens, outputTokens: extensionOutputTokens @@ -64,7 +62,6 @@ export const pushGenerateVectorUsage = ({ deepSearchModelName: '' }; const { totalPoints, modelName } = formatModelChars2Points({ - modelType: ModelTypeEnum.llm, model: deepSearchModel, inputTokens: deepSearchInputTokens, outputTokens: deepSearchOutputTokens @@ -145,8 +142,7 @@ export const pushQuestionGuideUsage = ({ const { totalPoints, modelName } = formatModelChars2Points({ inputTokens, outputTokens, - model, - modelType: ModelTypeEnum.llm + model }); createUsage({ @@ -184,8 +180,7 @@ export const pushAudioSpeechUsage = ({ }) => { const { totalPoints, modelName } = formatModelChars2Points({ model, - inputTokens: charsLength, - modelType: ModelTypeEnum.tts + inputTokens: charsLength }); createUsage({ @@ -221,7 +216,6 @@ export const pushWhisperUsage = ({ const { totalPoints, modelName } = formatModelChars2Points({ model: whisperModel.model, inputTokens: duration, - modelType: ModelTypeEnum.stt, multiple: 60 }); @@ -259,8 +253,7 @@ export const pushRerankUsage = ({ }) => { const { totalPoints, modelName } = formatModelChars2Points({ model, - inputTokens, - modelType: ModelTypeEnum.rerank + inputTokens }); createUsage({ diff --git a/projects/app/src/web/common/utils/voice.ts b/projects/app/src/web/common/utils/voice.ts index bbc0cce2f5ce..2791bc761d26 100644 --- a/projects/app/src/web/common/utils/voice.ts +++ b/projects/app/src/web/common/utils/voice.ts @@ -111,9 +111,9 @@ export const useAudioPlay = ( /* Perform a voice playback */ const playAudioByText = useCallback( async ({ text, buffer }: { text: string; buffer?: Uint8Array }) => { - const playAudioBuffer = (buffer: Uint8Array) => { + const playAudioBuffer = (audioBuffer: Uint8Array) => { if (!audioRef.current) return; - const audioUrl = URL.createObjectURL(new Blob([buffer], { type: contentType })); + const audioUrl = URL.createObjectURL(new Blob([audioBuffer], { type: contentType })); audioRef.current.src = audioUrl; audioRef.current.play(); }; diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index 7d2a795e31bd..4ada63bff1f7 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -1,5 +1,5 @@ import { GET, POST, DELETE, PUT } from '@/web/common/api/request'; -import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d'; +import type { AppDetailType, AppListItemType, AppSchema } from '@fastgpt/global/core/app/type.d'; import type { AppUpdateParams, AppChangeOwnerBody } from '@/global/core/app/api'; import type { CreateAppBody } from '@/pages/api/core/app/create'; import type { ListAppBody } from '@/pages/api/core/app/list'; diff --git a/projects/app/src/web/core/chat/api.ts b/projects/app/src/web/core/chat/api.ts index 76fe33af4299..8ea36e92fc15 100644 --- a/projects/app/src/web/core/chat/api.ts +++ b/projects/app/src/web/core/chat/api.ts @@ -1,8 +1,12 @@ import { GET, POST, DELETE, PUT } from '@/web/common/api/request'; import type { ChatHistoryItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d'; import type { + ChatFavouriteTagType, ChatSettingSchema, - ChatSettingUpdateParams + ChatSettingUpdateParams, + QuickAppType, + SelectedToolType, + ChatSettingReturnType } from '@fastgpt/global/core/chat/setting/type'; import type { getResDataQuery } from '@/pages/api/core/chat/getResData'; import type { @@ -34,6 +38,11 @@ import type { GetCollectionQuoteProps, GetCollectionQuoteRes } from '@/pages/api/core/chat/quote/getCollectionQuote'; +import type { + ChatFavouriteAppUpdateParams, + ChatFavouriteAppSchema, + ChatFavouriteApp +} from '@fastgpt/global/core/chat/favouriteApp/type'; /** * 获取初始化聊天内容 @@ -113,9 +122,29 @@ export const getCollectionQuote = (data: GetCollectionQuoteProps) => /*---------- chat setting ------------*/ export const getChatSetting = () => { - return GET('/proApi/core/chat/setting/detail'); + return GET('/proApi/core/chat/setting/detail'); }; export const updateChatSetting = (data: ChatSettingUpdateParams) => { return POST('/proApi/core/chat/setting/update', data); }; + +export const getFavouriteApps = (data?: { name?: string; tag?: string }) => { + return GET('/proApi/core/chat/setting/favourite/list', data); +}; + +export const updateFavouriteApps = (data: ChatFavouriteAppUpdateParams[]) => { + return POST('/proApi/core/chat/setting/favourite/update', data); +}; + +export const updateFavouriteAppOrder = (data: { id: string; order: number }[]) => { + return PUT('/proApi/core/chat/setting/favourite/order', data); +}; + +export const updateFavouriteAppTags = (data: { id: string; tags: string[] }[]) => { + return PUT('/proApi/core/chat/setting/favourite/tags', data); +}; + +export const deleteFavouriteApp = (id: string) => { + return DELETE(`/proApi/core/chat/setting/favourite/delete?id=${id}`); +}; diff --git a/projects/app/src/web/core/chat/context/chatRecordContext.tsx b/projects/app/src/web/core/chat/context/chatRecordContext.tsx index 1778899f1aa6..fc932e72059e 100644 --- a/projects/app/src/web/core/chat/context/chatRecordContext.tsx +++ b/projects/app/src/web/core/chat/context/chatRecordContext.tsx @@ -11,6 +11,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; import { type BoxProps } from '@chakra-ui/react'; type ChatRecordContextType = { + isLoadingRecords: boolean; chatRecords: ChatSiteItemType[]; setChatRecords: React.Dispatch>; isChatRecordsLoaded: boolean; @@ -25,6 +26,7 @@ type ChatRecordContextType = { }; export const ChatRecordContext = createContext({ + isLoadingRecords: false, chatRecords: [], setChatRecords: function (value: React.SetStateAction): void { throw new Error('Function not implemented.'); @@ -60,7 +62,8 @@ const ChatRecordContextProvider = ({ data: chatRecords, ScrollData, setData: setChatRecords, - total: totalRecordsCount + total: totalRecordsCount, + isLoading } = useScrollPagination( async (data: getPaginationRecordsBody): Promise> => { setIsChatRecordsLoaded(false); @@ -100,13 +103,14 @@ const ChatRecordContextProvider = ({ const contextValue = useMemo(() => { return { + isLoadingRecords: isLoading, chatRecords, setChatRecords, totalRecordsCount, ScrollData, isChatRecordsLoaded }; - }, [ScrollData, chatRecords, setChatRecords, totalRecordsCount, isChatRecordsLoaded]); + }, [isLoading, chatRecords, setChatRecords, totalRecordsCount, ScrollData, isChatRecordsLoaded]); return {children}; }; diff --git a/projects/app/src/web/core/chat/context/chatSettingContext.tsx b/projects/app/src/web/core/chat/context/chatSettingContext.tsx index 7b6493c0f537..820210f09a66 100644 --- a/projects/app/src/web/core/chat/context/chatSettingContext.tsx +++ b/projects/app/src/web/core/chat/context/chatSettingContext.tsx @@ -1,4 +1,5 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; +import type { ChatSettingTabOptionEnum } from '@/pageComponents/chat/constants'; import { ChatSidebarPaneEnum, defaultCollapseStatus, @@ -6,21 +7,25 @@ import { } from '@/pageComponents/chat/constants'; import { getChatSetting } from '@/web/core/chat/api'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; -import type { ChatSettingSchema } from '@fastgpt/global/core/chat/setting/type'; +import type { + ChatSettingReturnType, + ChatSettingSchema +} from '@fastgpt/global/core/chat/setting/type'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { createContext } from 'use-context-selector'; -import { usePathname } from 'next/navigation'; - -type ChatSettingReturnType = ChatSettingSchema | undefined; export type ChatSettingContextValue = { pane: ChatSidebarPaneEnum; - handlePaneChange: (pane: ChatSidebarPaneEnum, _id?: string) => void; + handlePaneChange: ( + pane: ChatSidebarPaneEnum, + _id?: string, + _tab?: ChatSettingTabOptionEnum + ) => void; collapse: CollapseStatusType; onTriggerCollapse: () => void; - chatSettings: ChatSettingSchema | undefined; + chatSettings?: ChatSettingReturnType; refreshChatSetting: () => Promise; logos: Pick; }; @@ -42,7 +47,6 @@ export const ChatSettingContext = createContext({ export const ChatSettingContextProvider = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); - const pathname = usePathname(); const { feConfigs } = useSystemStore(); const { appId, setLastPane, setLastChatAppId, lastPane } = useChatStore(); @@ -60,11 +64,14 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React { manual: false, refreshDeps: [feConfigs.isPlus], - onSuccess(data) { + onSuccess: (data) => { if (!data) return; - // Reset home page appId - if (pane === ChatSidebarPaneEnum.HOME && appId !== data.appId) { + if ( + pane === ChatSidebarPaneEnum.HOME && + appId !== data.appId && + data.quickAppList.every((q) => q._id !== appId) + ) { handlePaneChange(ChatSidebarPaneEnum.HOME, data.appId); } } @@ -72,8 +79,8 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React ); const handlePaneChange = useCallback( - async (newPane: ChatSidebarPaneEnum, id?: string) => { - if (newPane === pane && !id) return; + async (newPane: ChatSidebarPaneEnum, id?: string, tab?: ChatSettingTabOptionEnum) => { + if (newPane === pane && !id && !tab) return; const _id = (() => { if (id) return id; @@ -90,7 +97,8 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React query: { ...router.query, appId: _id, - pane: newPane + pane: newPane, + tab } }); diff --git a/projects/app/src/web/core/chat/context/useChatStore.ts b/projects/app/src/web/core/chat/context/useChatStore.ts index dba5e90592ae..de2ed5ce5209 100644 --- a/projects/app/src/web/core/chat/context/useChatStore.ts +++ b/projects/app/src/web/core/chat/context/useChatStore.ts @@ -186,6 +186,7 @@ const createStorageListener = (store: any) => { return () => {}; }; + // 初始化存储事件监听器 if (typeof window !== 'undefined') { createStorageListener(useChatStore); diff --git a/scripts/openapi/package.json b/scripts/openapi/package.json index 92ad992ccc14..4c77b65f4947 100644 --- a/scripts/openapi/package.json +++ b/scripts/openapi/package.json @@ -10,7 +10,7 @@ "@types/babel__traverse": "^7.20.6" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.1.3" }, "dependencies": { "@babel/generator": "^7.25.6", diff --git a/test/cases/service/core/ai/llm/toolCall.test.ts b/test/cases/service/core/ai/llm/toolCall.test.ts new file mode 100644 index 000000000000..60d7bc1a036c --- /dev/null +++ b/test/cases/service/core/ai/llm/toolCall.test.ts @@ -0,0 +1,935 @@ +import { + parsePromptToolCall, + promptToolCallMessageRewrite +} from '@fastgpt/service/core/ai/llm/promptToolCall'; +import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import { describe, expect, it } from 'vitest'; + +describe('parsePromptToolCall function tests', () => { + describe('Basic scenarios', () => { + it('should return answer when input starts with 0:', () => { + const input = '0: This is a regular response'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This is a regular response' + }); + }); + + it('should return answer when input starts with 0:(Chinese colon)', () => { + const input = '0:This is a regular response with Chinese colon'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This is a regular response with Chinese colon' + }); + }); + + it('should return trimmed answer when input starts with 0: and has extra whitespace', () => { + const input = ' 0: This is a response with whitespace '; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This is a response with whitespace' + }); + }); + + it('should handle 0: in the middle of string when within first 6 characters', () => { + const input = 'Pre 0: This is the actual response'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This is the actual response' + }); + }); + + it('should not process 0: when beyond first 6 characters', () => { + const input = 'Long prefix 0: This should not be processed'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Long prefix 0: This should not be processed' + }); + }); + + it('should return original string when no 0: prefix found and no tool call', () => { + const input = 'This is just a regular string without any prefixes'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This is just a regular string without any prefixes' + }); + }); + + it('should parse valid tool call with 1:', () => { + const input = '1: {"name": "get_weather", "arguments": {"location": "Tokyo"}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('get_weather'); + expect(result.toolCalls![0].function.arguments).toBe('{"location":"Tokyo"}'); + expect(result.toolCalls![0].type).toBe('function'); + expect(result.toolCalls![0].id).toBeDefined(); + expect(typeof result.toolCalls![0].id).toBe('string'); + }); + + it('should parse valid tool call with 1:(Chinese colon)', () => { + const input = '1:{"name": "calculate", "arguments": {"expression": "2+2"}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('calculate'); + expect(result.toolCalls![0].function.arguments).toBe('{"expression":"2+2"}'); + }); + }); + + describe('Tool call parsing', () => { + it('should handle tool call with nested object arguments', () => { + const input = + '1: {"name": "complex_tool", "arguments": {"user": {"name": "John", "age": 30}, "settings": {"verbose": true}}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('complex_tool'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + user: { name: 'John', age: 30 }, + settings: { verbose: true } + }); + }); + + it('should handle tool call with array arguments', () => { + const input = + '1: {"name": "process_list", "arguments": {"items": [1, 2, 3], "options": ["sort", "filter"]}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('process_list'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + items: [1, 2, 3], + options: ['sort', 'filter'] + }); + }); + + it('should handle tool call with empty arguments', () => { + const input = '1: {"name": "simple_tool", "arguments": {}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('simple_tool'); + expect(result.toolCalls![0].function.arguments).toBe('{}'); + }); + + it('should handle tool call with extra content before and after JSON', () => { + const input = + 'Some text 1: extra {"name": "test_tool", "arguments": {"param": "value"}} more text'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('test_tool'); + expect(result.toolCalls![0].function.arguments).toBe('{"param":"value"}'); + }); + }); + + describe('Edge cases and error handling', () => { + it('should return error message for malformed JSON with 1:', () => { + const input = '1: {"name": "tool", "arguments": invalid json}'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Tool run error' + }); + }); + + it('should return error message for incomplete JSON with 1:', () => { + const input = '1: {"name": "tool"'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Tool run error' + }); + }); + + it('should handle empty JSON object with 1: (creates tool call with undefined properties)', () => { + const input = '1: {}'; + const result = parsePromptToolCall(input); + + // Empty object {} doesn't have name property, so it parses but creates invalid tool call + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBeUndefined(); + }); + + it('should handle empty string input', () => { + const input = ''; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: '' + }); + }); + + it('should handle whitespace-only input', () => { + const input = ' \n\t '; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: '' + }); + }); + + it('should handle input with only prefix', () => { + const input = '1:'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Tool run error' + }); + }); + + it('should handle input with only prefix and whitespace', () => { + const input = '1: '; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Tool run error' + }); + }); + + it('should handle JSON5 syntax in tool call', () => { + const input = "1: {name: 'test_tool', arguments: {param: 'value', number: 42}}"; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('test_tool'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + param: 'value', + number: 42 + }); + }); + + it('should handle tool call with simple strings (no escaping needed)', () => { + const input = + '1: {"name": "search", "arguments": {"query": "Hello world", "filter": "type:document"}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('search'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + query: 'Hello world', + filter: 'type:document' + }); + }); + + it('should handle input with multiple 0: occurrences - does not process if first one is beyond position 5', () => { + const input = 'First 0: Second part 0: Third part'; + const result = parsePromptToolCall(input); + + // The first '0:' is at position 6, which is > 5, so it's not processed + expect(result).toEqual({ + answer: 'First 0: Second part 0: Third part' + }); + }); + + it('should handle input with multiple 1: occurrences - fails to parse when extra text interferes', () => { + const input = + 'Text 1: {"name": "tool1", "arguments": {"param": "value"}} more text 1: {"name": "tool2", "arguments": {}}'; + const result = parsePromptToolCall(input); + + // The sliceJsonStr function can't properly extract JSON when there's extra text after + expect(result).toEqual({ + answer: 'Tool run error' + }); + }); + + it('should handle tool name with underscores and numbers', () => { + const input = '1: {"name": "get_user_data_v2", "arguments": {"user_id": 123}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('get_user_data_v2'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + user_id: 123 + }); + }); + + it('should handle very long strings', () => { + const longString = 'A'.repeat(10000); + const input = `0: ${longString}`; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: longString + }); + }); + + it('should handle Unicode characters in tool arguments', () => { + const input = + '1: {"name": "translate", "arguments": {"text": "你好世界", "from": "zh", "to": "en"}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('translate'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + text: '你好世界', + from: 'zh', + to: 'en' + }); + }); + + it('should handle mixed Chinese and English colons', () => { + const input1 = '0: Answer with English colon'; + const input2 = '0:Answer with Chinese colon'; + const input3 = '1: {"name": "tool", "arguments": {"key": "value"}}'; + const input4 = '1:{"name": "tool", "arguments": {"key": "value"}}'; + + const result1 = parsePromptToolCall(input1); + const result2 = parsePromptToolCall(input2); + const result3 = parsePromptToolCall(input3); + const result4 = parsePromptToolCall(input4); + + expect(result1.answer).toBe('Answer with English colon'); + expect(result2.answer).toBe('Answer with Chinese colon'); + expect(result3.toolCalls).toHaveLength(1); + expect(result4.toolCalls).toHaveLength(1); + }); + }); + + describe('Boundary conditions', () => { + it('should handle input with only numbers', () => { + const input = '12345'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: '12345' + }); + }); + + it('should handle tool call with null arguments', () => { + const input = '1: {"name": "null_test", "arguments": null}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('null_test'); + expect(result.toolCalls![0].function.arguments).toBe('null'); + }); + + it('should handle tool call with boolean and number values', () => { + const input = + '1: {"name": "mixed_types", "arguments": {"flag": true, "count": 0, "ratio": 3.14}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('mixed_types'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + flag: true, + count: 0, + ratio: 3.14 + }); + }); + + it('should handle newlines in input - 0: beyond position limit', () => { + const input = 'Line 1\n0: Line 2\nLine 3'; + const result = parsePromptToolCall(input); + + // The '0:' appears after position 6, so it's not processed + expect(result).toEqual({ + answer: 'Line 1\n0: Line 2\nLine 3' + }); + }); + + it('should handle tabs and special whitespace', () => { + const input = '\t0:\tThis\thas\ttabs\t'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'This\thas\ttabs' + }); + }); + + it('should not process 0: when it appears after position 5', () => { + const input = 'Longer prefix 0: This should not be processed'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Longer prefix 0: This should not be processed' + }); + }); + + it('should handle 0: at exactly position 5', () => { + const input = '12345 0: Should not be processed'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: '12345 0: Should not be processed' + }); + }); + + it('should handle Chinese colon priority (only when English colon not found)', () => { + const input = '0:Chinese colon without English'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Chinese colon without English' + }); + }); + + it('should prioritize English colon over Chinese colon - but not when beyond position limit', () => { + const input = '0: Chinese 0: English colon'; + const result = parsePromptToolCall(input); + + // The English '0:' is at position 11, beyond the limit, so returns original string + expect(result).toEqual({ + answer: '0: Chinese 0: English colon' + }); + }); + + it('should handle valid 0: within newline constraints', () => { + const input = '0: Line with proper prefix'; + const result = parsePromptToolCall(input); + + expect(result).toEqual({ + answer: 'Line with proper prefix' + }); + }); + + it('should handle simple 1: tool call that works', () => { + const input = '1: {"name": "tool1", "arguments": {"param": "value"}}'; + const result = parsePromptToolCall(input); + + expect(result.answer).toBe(''); + expect(result.toolCalls).toHaveLength(1); + expect(result.toolCalls![0].function.name).toBe('tool1'); + expect(JSON.parse(result.toolCalls![0].function.arguments)).toEqual({ + param: 'value' + }); + }); + }); +}); + +describe('promptToolCallMessageRewrite function tests', () => { + describe('System message handling', () => { + it('should add system message when none exists', () => { + const messages: ChatCompletionMessageParam[] = [{ role: 'user', content: 'Hello' }]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'get_weather', + description: 'Get weather info', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(2); + expect(result[0].role).toBe('system'); + expect(result[0].content).toContain('你是一个智能机器人'); + expect(result[0].content).toContain('get_weather'); + expect(result[1]).toEqual({ role: 'user', content: 'Hello' }); + }); + + it('should update existing string system message', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'system', content: 'You are helpful' }, + { role: 'user', content: 'Hello' } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'calculator', + description: 'Calculate math', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(2); + expect(result[0].role).toBe('system'); + expect(result[0].content).toContain('You are helpful'); + expect(result[0].content).toContain('你是一个智能机器人'); + expect(result[0].content).toContain('calculator'); + }); + + it('should update existing array system message', () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: 'system', + content: [{ type: 'text', text: 'You are helpful' }] + }, + { role: 'user', content: 'Hello' } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'search', + description: 'Search tool', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(2); + expect(result[0].role).toBe('system'); + expect(Array.isArray(result[0].content)).toBe(true); + const content = result[0].content as Array; + expect(content).toHaveLength(2); + expect(content[0]).toEqual({ type: 'text', text: 'You are helpful' }); + expect(content[1].type).toBe('text'); + expect(content[1].text).toContain('你是一个智能机器人'); + expect(content[1].text).toContain('search'); + }); + + it('should throw error for invalid system message content', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'system', content: null as any }, + { role: 'user', content: 'Hello' } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'test_tool', + description: 'Test', + parameters: { type: 'object', properties: {} } + } + } + ]; + + expect(() => promptToolCallMessageRewrite(messages, tools)).toThrow( + 'Prompt call invalid input' + ); + }); + + it('should handle multiple tools in system message', () => { + const messages: ChatCompletionMessageParam[] = [{ role: 'user', content: 'Hello' }]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'tool1', + description: 'First tool', + parameters: { type: 'object', properties: { param1: { type: 'string' } } } + } + }, + { + type: 'function', + function: { + name: 'tool2', + description: 'Second tool', + parameters: { type: 'object', properties: { param2: { type: 'number' } } } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[0].content).toContain('tool1'); + expect(result[0].content).toContain('tool2'); + expect(result[0].content).toContain('First tool'); + expect(result[0].content).toContain('Second tool'); + }); + }); + + describe('Assistant message rewriting', () => { + it('should rewrite assistant message with string content', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('assistant'); + expect(result[2].content).toBe('0: Hi there!'); + }); + + it('should rewrite assistant message with tool calls', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'What is the weather?' }, + { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'call_123', + type: 'function', + function: { + name: 'get_weather', + arguments: '{"location": "Tokyo"}' + } + } + ] + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('assistant'); + expect(result[2].content).toBe( + '1: {"name":"get_weather","arguments":"{\\"location\\": \\"Tokyo\\"}"}' + ); + expect(result[2]).not.toHaveProperty('tool_calls'); + }); + + it('should skip assistant message with no content and no tool calls', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: null } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('assistant'); + expect(result[2].content).toBeNull(); + }); + + it('should handle assistant message with multiple tool calls (only first one used)', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'tool1', arguments: '{"param": "value1"}' } + }, + { + id: 'call_2', + type: 'function', + function: { name: 'tool2', arguments: '{"param": "value2"}' } + } + ] + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].content).toBe( + '1: {"name":"tool1","arguments":"{\\"param\\": \\"value1\\"}"}' + ); + expect(result[2]).not.toHaveProperty('tool_calls'); + }); + }); + + describe('Tool message rewriting', () => { + it('should convert tool message to user message', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'What is the weather?' }, + { + role: 'tool', + tool_call_id: 'call_123', + content: 'The weather is sunny' + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('user'); + expect(result[2].content).toBe('\nThe weather is sunny\n'); + expect(result[2]).not.toHaveProperty('tool_call_id'); + }); + + it('should handle multiple tool messages', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { + role: 'tool', + tool_call_id: 'call_1', + content: 'Result 1' + }, + { + role: 'tool', + tool_call_id: 'call_2', + content: 'Result 2' + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('user'); + expect(result[2].content).toBe('\nResult 1\n'); + expect(result[3].role).toBe('user'); + expect(result[3].content).toBe('\nResult 2\n'); + }); + + it('should handle tool message with complex content', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Test' }, + { + role: 'tool', + tool_call_id: 'call_123', + content: JSON.stringify({ result: 'success', data: [1, 2, 3] }) + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('user'); + expect(result[2].content).toBe( + '\n{"result":"success","data":[1,2,3]}\n' + ); + }); + }); + + describe('Message immutability', () => { + it('should not mutate original messages', () => { + const originalMessages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' }, + { + role: 'tool', + tool_call_id: 'call_123', + content: 'Tool result' + } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'test_tool', + description: 'Test', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const originalMessagesCopy = JSON.parse(JSON.stringify(originalMessages)); + promptToolCallMessageRewrite(originalMessages, tools); + + expect(originalMessages).toEqual(originalMessagesCopy); + }); + + it('should handle deeply nested message content without mutation', () => { + const originalMessages: ChatCompletionMessageParam[] = [ + { + role: 'system', + content: [{ type: 'text', text: 'Original system message' }] + }, + { role: 'user', content: 'Hello' } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'test_tool', + description: 'Test', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const originalMessagesCopy = JSON.parse(JSON.stringify(originalMessages)); + promptToolCallMessageRewrite(originalMessages, tools); + + expect(originalMessages).toEqual(originalMessagesCopy); + }); + }); + + describe('Complex conversation flows', () => { + it('should handle complete conversation with all message types', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'system', content: 'You are helpful' }, + { role: 'user', content: 'What is the weather in Tokyo?' }, + { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'call_123', + type: 'function', + function: { + name: 'get_weather', + arguments: '{"location": "Tokyo"}' + } + } + ] + }, + { + role: 'tool', + tool_call_id: 'call_123', + content: 'The weather in Tokyo is sunny, 25°C' + }, + { role: 'assistant', content: 'The weather in Tokyo is sunny with a temperature of 25°C.' } + ]; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'get_weather', + description: 'Get current weather', + parameters: { + type: 'object', + properties: { + location: { type: 'string', description: 'City name' } + }, + required: ['location'] + } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(5); + + // System message should be updated + expect(result[0].role).toBe('system'); + expect(result[0].content).toContain('You are helpful'); + expect(result[0].content).toContain('get_weather'); + + // User message unchanged + expect(result[1]).toEqual({ role: 'user', content: 'What is the weather in Tokyo?' }); + + // Assistant with tool call should be rewritten + expect(result[2].role).toBe('assistant'); + expect(result[2].content).toBe( + '1: {"name":"get_weather","arguments":"{\\"location\\": \\"Tokyo\\"}"}' + ); + expect(result[2]).not.toHaveProperty('tool_calls'); + + // Tool message should become user message + expect(result[3].role).toBe('user'); + expect(result[3].content).toBe( + '\nThe weather in Tokyo is sunny, 25°C\n' + ); + + // Final assistant message should be prefixed + expect(result[4].role).toBe('assistant'); + expect(result[4].content).toBe( + '0: The weather in Tokyo is sunny with a temperature of 25°C.' + ); + }); + + it('should handle empty messages array', () => { + const messages: ChatCompletionMessageParam[] = []; + const tools: ChatCompletionTool[] = [ + { + type: 'function', + function: { + name: 'test_tool', + description: 'Test', + parameters: { type: 'object', properties: {} } + } + } + ]; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(1); + expect(result[0].role).toBe('system'); + expect(result[0].content).toContain('你是一个智能机器人'); + expect(result[0].content).toContain('test_tool'); + }); + + it('should handle empty tools array', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(3); + expect(result[0].role).toBe('system'); + expect(result[0].content).toContain('你是一个智能机器人'); + expect(result[0].content).toContain('[]'); // Empty tools array in JSON + expect(result[2].content).toBe('0: Hi there!'); + }); + }); + + describe('Edge cases', () => { + it('should handle assistant message with empty string content', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: '' } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('assistant'); + expect(result[2].content).toBe(''); // Empty string is falsy, so not processed + }); + + it('should handle tool message with empty content', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { + role: 'tool', + tool_call_id: 'call_123', + content: '' + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result[2].role).toBe('user'); + expect(result[2].content).toBe('\n\n'); + }); + + it('should handle mixed message types in sequence', () => { + const messages: ChatCompletionMessageParam[] = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi!' }, + { role: 'user', content: 'How are you?' }, + { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'check_status', arguments: '{}' } + } + ] + }, + { + role: 'tool', + tool_call_id: 'call_1', + content: 'Status: OK' + } + ]; + const tools: ChatCompletionTool[] = []; + + const result = promptToolCallMessageRewrite(messages, tools); + + expect(result).toHaveLength(6); // system + 5 original + expect(result[1]).toEqual({ role: 'user', content: 'Hello' }); + expect(result[2].content).toBe('0: Hi!'); + expect(result[3]).toEqual({ role: 'user', content: 'How are you?' }); + expect(result[4].content).toBe('1: {"name":"check_status","arguments":"{}"}'); + expect(result[5].content).toBe('\nStatus: OK\n'); + }); + }); +}); diff --git a/test/cases/service/core/ai/llm/utils.test.ts b/test/cases/service/core/ai/llm/utils.test.ts new file mode 100644 index 000000000000..858f428bc23f --- /dev/null +++ b/test/cases/service/core/ai/llm/utils.test.ts @@ -0,0 +1,862 @@ +import { + loadRequestMessages, + filterGPTMessageByMaxContext +} from '@fastgpt/service/core/ai/llm/utils'; +import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; +import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +// Mock external dependencies +vi.mock('@fastgpt/service/common/string/tiktoken/index', () => ({ + countGptMessagesTokens: vi.fn() +})); + +vi.mock('@fastgpt/service/common/file/image/utils', () => ({ + getImageBase64: vi.fn() +})); + +vi.mock('@fastgpt/web/i18n/utils', () => ({ + i18nT: vi.fn((key: string) => key) +})); + +vi.mock('@fastgpt/service/common/system/log', () => ({ + addLog: { + info: vi.fn(), + warn: vi.fn() + } +})); + +vi.mock('axios', () => ({ + default: { + head: vi.fn() + } +})); + +import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index'; +import { getImageBase64 } from '@fastgpt/service/common/file/image/utils'; +import { addLog } from '@fastgpt/service/common/system/log'; + +// @ts-ignore +import axios from 'axios'; + +const mockCountGptMessagesTokens = vi.mocked(countGptMessagesTokens); +const mockGetImageBase64 = vi.mocked(getImageBase64); +const mockAxiosHead = vi.mocked(axios.head); + +describe('filterGPTMessageByMaxContext function tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockCountGptMessagesTokens.mockResolvedValue(10); + }); + + describe('Basic filtering scenarios', () => { + it('should return empty array for invalid input', async () => { + const result = await filterGPTMessageByMaxContext({ + messages: null as any, + maxContext: 1000 + }); + expect(result).toEqual([]); + + const result2 = await filterGPTMessageByMaxContext({ + messages: undefined as any, + maxContext: 1000 + }); + expect(result2).toEqual([]); + }); + + it('should return messages unchanged when less than 4 messages', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'You are helpful' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Hi there!' } + ]; + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 1000 + }); + + expect(result).toEqual(messages); + }); + + it('should return only system prompts when no chat prompts exist', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System prompt 1' }, + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System prompt 2' } + ]; + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 1000 + }); + + expect(result).toEqual(messages); + }); + }); + + describe('System and chat prompt separation', () => { + it('should correctly separate system prompts from chat prompts', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System 1' }, + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System 2' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 1' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 2' } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(20) // system prompts + .mockResolvedValueOnce(30) // user 2 + .mockResolvedValueOnce(25) // assistant 1 + user 1 + .mockResolvedValueOnce(15); // user 1 + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 1000 + }); + + expect(result).toHaveLength(5); + expect( + result.slice(0, 2).every((msg) => msg.role === ChatCompletionRequestMessageRoleEnum.System) + ).toBe(true); + expect( + result.slice(2).every((msg) => msg.role !== ChatCompletionRequestMessageRoleEnum.System) + ).toBe(true); + }); + }); + + describe('Context limiting behavior', () => { + it('should filter out messages when context limit is exceeded', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 1' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 2' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 2' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 3' } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(50) // system prompts + .mockResolvedValueOnce(60) // user 3 (exceeds remaining context) + .mockResolvedValueOnce(40); // assistant 2 + user 2 + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 100 + }); + + // Should keep system + last complete conversation that fits + expect(result).toHaveLength(2); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); + expect(result[1].content).toBe('User 3'); + }); + + it('should preserve at least one conversation round even if it exceeds context', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Large user message' }, + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: 'Large assistant response' + } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(20) // system prompts + .mockResolvedValueOnce(200); // user + assistant (exceeds remaining context) + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 50 + }); + + // Should still keep the conversation even though it exceeds context + expect(result).toHaveLength(3); + expect(result[1].content).toBe('Large user message'); + expect(result[2].content).toBe('Large assistant response'); + }); + }); + + describe('Complex conversation patterns', () => { + it('should handle user-assistant-tool conversation pattern', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 1' }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call1', + content: 'Tool 1' + }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 2' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 2' }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call2', + content: 'Tool 2' + }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call3', + content: 'Tool 3' + } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(20) // system + .mockResolvedValueOnce(50) // last group: assistant 2 + tool 2 + tool 3 + user 2 + .mockResolvedValueOnce(40); // previous group: assistant 1 + tool 1 + user 1 + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 1000 + }); + + expect(result).toHaveLength(8); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); + }); + + it('should handle multiple assistant messages in sequence', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Assistant 2' }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call1', + content: 'Tool result' + }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User 2' } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(30) // user 2 + .mockResolvedValueOnce(60); // assistant 1 + assistant 2 + tool + user 1 + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 1000 + }); + + expect(result).toHaveLength(5); + }); + }); + + describe('Edge cases', () => { + it('should handle empty messages array', async () => { + const result = await filterGPTMessageByMaxContext({ + messages: [], + maxContext: 1000 + }); + + expect(result).toEqual([]); + }); + + it('should handle zero maxContext', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User' } + ]; + + mockCountGptMessagesTokens + .mockResolvedValueOnce(10) // system + .mockResolvedValueOnce(20); // user + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: 0 + }); + + // Should still preserve at least one conversation + expect(result).toHaveLength(2); + }); + + it('should handle negative maxContext', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'User' } + ]; + + mockCountGptMessagesTokens.mockResolvedValueOnce(20); + + const result = await filterGPTMessageByMaxContext({ + messages, + maxContext: -100 + }); + + expect(result).toHaveLength(1); + }); + }); +}); + +describe('loadRequestMessages function tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetImageBase64.mockResolvedValue({ + completeBase64: 'data:image/png;base64,test', + base64: 'test', + mime: 'image/png' + }); + mockAxiosHead.mockResolvedValue({ status: 200 }); + }); + + describe('Basic message processing', () => { + it('should reject empty messages array', async () => { + await expect( + loadRequestMessages({ + messages: [] + }) + ).rejects.toMatch('common:core.chat.error.Messages empty'); + }); + + it('should process simple conversation', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'You are helpful' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Hi there!' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(3); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); + expect(result[0].content).toBe('You are helpful'); + expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.User); + expect(result[1].content).toBe('Hello'); + expect(result[2].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); + expect(result[2].content).toBe('Hi there!'); + }); + }); + + describe('System message processing', () => { + it('should handle string system content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System prompt' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('System prompt'); + }); + + it('should handle array system content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.System, + content: [ + { type: 'text', text: 'Part 1' }, + { type: 'text', text: 'Part 2' } + ] + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('Part 1\n\nPart 2'); + }); + + it('should filter out empty text in system content array', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.System, + content: [ + { type: 'text', text: 'Valid text' }, + { type: 'text', text: '' }, + { type: 'text', text: 'Another valid text' } + ] + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('Valid text\n\nAnother valid text'); + }); + + it('should skip system message with empty content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: '' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.User); + }); + }); + + describe('User message processing with vision', () => { + it('should process simple text user message', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello world' } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('Hello world'); + }); + + it('should not extract images from short text by default', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: 'https://example.com/image.png' + } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + expect(typeof result[0].content).toBe('string'); + expect(result[0].content).toBe('https://example.com/image.png'); + }); + + it('should not extract images when useVision is false', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: 'Look at https://example.com/image.png' + } + ]; + + const result = await loadRequestMessages({ messages, useVision: false }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('Look at https://example.com/image.png'); + }); + + it('should not extract images from very long text (>500 chars)', async () => { + const longText = 'A'.repeat(600) + ' https://example.com/image.png'; + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: longText } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe(longText); + }); + + it('should limit to 4 images and return text if more found', async () => { + const textWithManyImages = + 'Images: ' + + 'https://example.com/1.png ' + + 'https://example.com/2.jpg ' + + 'https://example.com/3.gif ' + + 'https://example.com/4.webp ' + + 'https://example.com/5.png'; + + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: textWithManyImages } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe(textWithManyImages); + }); + + it('should handle array content with mixed types', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [ + { type: 'text', text: 'Hello' }, + { type: 'image_url', image_url: { url: 'https://example.com/image.png' } } + ] + } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + // When array content has only text items and filtered images, it becomes a string + expect(typeof result[0].content).toBe('string'); + expect(result[0].content).toBe('Hello'); + }); + + it('should filter out empty text items from array content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [ + { type: 'text', text: 'Valid text' }, + { type: 'text', text: '' }, + { type: 'text', text: 'Another text' } + ] + } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + const content = result[0].content as any[]; + expect(content).toHaveLength(2); + }); + }); + + describe('Image processing', () => { + it('should load local image to base64', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [{ type: 'image_url', image_url: { url: '/local/image.png' } }] + } + ]; + + mockGetImageBase64.mockResolvedValue({ + completeBase64: 'data:image/png;base64,localimage', + base64: 'localimage', + mime: 'image/png' + }); + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + const content = result[0].content as any[]; + expect(content[0].image_url.url).toBe('data:image/png;base64,localimage'); + }); + + it('should preserve base64 images as-is', async () => { + const base64Image = 'data:image/png;base64,existingdata'; + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [{ type: 'image_url', image_url: { url: base64Image } }] + } + ]; + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + const content = result[0].content as any[]; + expect(content[0].image_url.url).toBe(base64Image); + expect(mockGetImageBase64).not.toHaveBeenCalled(); + }); + + it('should handle invalid remote images gracefully', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [ + { type: 'text', text: 'Text' }, + { type: 'image_url', image_url: { url: 'https://invalid.com/image.png' } } + ] + } + ]; + + mockAxiosHead.mockRejectedValue(new Error('Network error')); + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + // When image is filtered out and only text remains, it becomes string + expect(typeof result[0].content).toBe('string'); + expect(result[0].content).toBe('Text'); + }); + + it('should handle 405 status as valid image', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [ + { type: 'text', text: 'Check this image:' }, + { type: 'image_url', image_url: { url: 'https://example.com/image.png' } } + ] + } + ]; + + const error = new Error('Method not allowed'); + (error as any).response = { status: 405 }; + mockAxiosHead.mockRejectedValue(error); + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(result).toHaveLength(1); + // The function processes images from array content differently, expects text to remain + expect(typeof result[0].content).toBe('string'); + expect(result[0].content).toBe('Check this image:'); + }); + + it('should remove origin from image URLs when provided', async () => { + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [{ type: 'image_url', image_url: { url: 'https://mysite.com/images/test.png' } }] + } + ]; + + const result = await loadRequestMessages({ + messages, + useVision: true, + origin: 'https://mysite.com' + }); + + // Just verify the function processes without error - axios call verification is complex + expect(result).toHaveLength(1); + }); + }); + + describe('Assistant message processing', () => { + it('should process assistant message with string content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Hi there!' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[1].content).toBe('Hi there!'); + }); + + it('should process assistant message with array content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: [ + { type: 'text', text: 'Part 1' }, + { type: 'text', text: 'Part 2' } + ] + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[1].content).toBe('Part 1\nPart 2'); + }); + + it('should preserve tool_calls and function_call in assistant messages', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: null, + tool_calls: [ + { + id: 'call_123', + type: 'function', + function: { name: 'test_tool', arguments: '{}' } + } + ] + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect((result[1] as any).tool_calls).toHaveLength(1); + expect((result[1] as any).tool_calls![0].function.name).toBe('test_tool'); + }); + + it('should handle assistant message with null content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: null } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[1].content).toBe('null'); + }); + + it('should handle empty assistant content between other assistants', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'First' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: '' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Last' } + ]; + + const result = await loadRequestMessages({ messages }); + + // Adjacent assistant messages get merged, empty content in middle gets filtered during merge + expect(result).toHaveLength(2); + expect(result[1].content).toBe('First\n\nLast'); + }); + }); + + describe('Message merging behavior', () => { + it('should merge consecutive system messages', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System 1' }, + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'System 2' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); + // System messages when merged get converted to concatenated string + expect(typeof result[0].content).toBe('string'); + expect(result[0].content).toBe('System 1\n\nSystem 2'); + }); + + it('should merge consecutive user messages', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Message 1' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Message 2' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Response' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.User); + // User messages get merged - final format may be array or string + expect(result[0].content).toBeDefined(); + }); + + it('should merge consecutive assistant messages with content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Part 1' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Part 2' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); + expect(result[1].content).toBe('Part 1\nPart 2'); + }); + + it('should not merge assistant messages when one has tool calls', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Text response' }, + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: null, + tool_calls: [ + { id: 'call1', type: 'function', function: { name: 'tool', arguments: '{}' } } + ] + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(3); // Should not merge + expect(result[1].content).toBe('Text response'); + expect((result[2] as any).tool_calls).toHaveLength(1); + }); + }); + + describe('Other message types', () => { + it('should pass through tool messages unchanged', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call1', + content: 'Tool result' + } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(2); + expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Tool); + expect(result[1].content).toBe('Tool result'); + }); + + it('should handle user message with empty content as null', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: '' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('null'); + }); + + it('should handle undefined user content', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.User, content: undefined as any } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(1); + expect(result[0].content).toBe('null'); + }); + }); + + describe('Complex scenarios', () => { + it('should handle mixed conversation with all message types', async () => { + const messages: ChatCompletionMessageParam[] = [ + { role: ChatCompletionRequestMessageRoleEnum.System, content: 'You are helpful' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'Hello' }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'Hi!' }, + { role: ChatCompletionRequestMessageRoleEnum.User, content: 'How are you?' }, + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: null, + tool_calls: [ + { id: 'call1', type: 'function', function: { name: 'check_status', arguments: '{}' } } + ] + }, + { + role: ChatCompletionRequestMessageRoleEnum.Tool, + tool_call_id: 'call1', + content: 'Status: OK' + }, + { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: 'I am doing well!' } + ]; + + const result = await loadRequestMessages({ messages }); + + expect(result).toHaveLength(7); + expect(result.map((msg) => msg.role)).toEqual([ + ChatCompletionRequestMessageRoleEnum.System, + ChatCompletionRequestMessageRoleEnum.User, + ChatCompletionRequestMessageRoleEnum.Assistant, + ChatCompletionRequestMessageRoleEnum.User, + ChatCompletionRequestMessageRoleEnum.Assistant, + ChatCompletionRequestMessageRoleEnum.Tool, + ChatCompletionRequestMessageRoleEnum.Assistant + ]); + }); + + it('should handle environment variable MULTIPLE_DATA_TO_BASE64', async () => { + const originalEnv = process.env.MULTIPLE_DATA_TO_BASE64; + process.env.MULTIPLE_DATA_TO_BASE64 = 'true'; + + const messages: ChatCompletionMessageParam[] = [ + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: [{ type: 'image_url', image_url: { url: 'https://example.com/image.png' } }] + } + ]; + + mockGetImageBase64.mockResolvedValue({ + completeBase64: 'data:image/png;base64,converted', + base64: 'converted', + mime: 'image/png' + }); + + const result = await loadRequestMessages({ messages, useVision: true }); + + expect(mockGetImageBase64).toHaveBeenCalledWith('https://example.com/image.png'); + expect(result).toHaveLength(1); + const content = result[0].content as any[]; + expect(content[0].image_url.url).toBe('data:image/png;base64,converted'); + + // Restore original environment + if (originalEnv !== undefined) { + process.env.MULTIPLE_DATA_TO_BASE64 = originalEnv; + } else { + process.env.MULTIPLE_DATA_TO_BASE64 = ''; + } + }); + }); +}); diff --git a/test/cases/service/core/app/workflow/workflowDispatch.test.ts b/test/cases/service/core/app/workflow/workflowDispatch.test.ts index 6a6810acf034..ca4e54b45124 100644 --- a/test/cases/service/core/app/workflow/workflowDispatch.test.ts +++ b/test/cases/service/core/app/workflow/workflowDispatch.test.ts @@ -45,7 +45,11 @@ const testWorkflow = async (path: string) => { }, runningUserInfo: { tmbId: 'test', - teamId: 'test' + teamId: 'test', + username: 'test', + teamName: 'test', + memberName: 'test', + contact: 'test' }, timezone: 'Asia/Shanghai', externalProvider: {},