From c51395b2c8773cce934f3be9e97b58d1f8f50412 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 12 Aug 2025 22:22:18 +0800 Subject: [PATCH] V4.12.0 features (#5435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add logs chart (#5352) * charts * chart data * log chart * delete * rename api * fix * move api * fix * fix * pro config * fix * feat: Repository interaction (#5356) * feat: 1好像功能没问题了,明天再测 * feat: 2 解决了昨天遗留的bug,但全选按钮又bug了 * feat: 3 第三版,解决了全选功能bug * feat: 4 第四版,下面改小细节 * feat: 5 我勒个痘 * feat: 6 * feat: 6 pr * feat: 7 * feat: 8 * feat: 9 * feat: 10 * feat: 11 * feat: 12 * perf: checkbox ui * refactor: tweak login loyout (#5357) Co-authored-by: Archer <545436317@qq.com> * login ui * app chat log chart pro display (#5392) * app chat log chart pro display * add canopen props * perf: pro tag tip * perf: pro tag tip * feat: openrouter provider (#5406) * perf: login ui * feat: openrouter provider * provider * perf: custom error throw * perf: emb batch (#5407) * perf: emb batch * perf: vector retry * doc * doc (#5411) * doc * fix: team folder will add to workflow * fix: generateToc shell * Tool price (#5376) * resolve conflicts for cherry-pick * fix i18n * Enhance system plugin template data structure and update ToolSelectModal to include CostTooltip component * refactor: update systemKeyCost type to support array of objects in plugin and workflow types * refactor: simplify systemKeyCost type across plugin and workflow types to a single number * refactor: streamline systemKeyCost handling in plugin and workflow components * fix * fix * perf: toolset price config;fix: workflow array selector ui (#5419) * fix: workflow array selector ui * update default model tip * perf: toolset price config * doc * fix: test * Refactor/chat (#5418) * refactor: add homepage configuration; add home chat page; add side bar animated collapse and layout * fix: fix lint rules * chore: improve logics and code * chore: more clearer logics * chore: adjust api --------- Co-authored-by: Archer <545436317@qq.com> * perf: chat setting code * del history * logo image * perf: home chat ui * feat: enhance chat response handling with external links and user info (#5427) * feat: enhance chat response handling with external links and user info * fix * cite code * perf: toolset add in workflow * fix: test * fix: search paraentId * Fix/chat (#5434) * wip: rebase了upstream * wip: adapt mobile UI * fix: fix chat page logic and UI * fix: fix UI and improve some logics * fix: model selector missing logo; vision model to retrieve file * perf: role selector * fix: chat ui * optimize export app chat log (#5436) * doc * chore: move components to proper directory; fix the api to get app list (#5437) * chore: improve team app panel display form (#5438) * feat: add home chat log tab * chore: improve team app panel display form * chore: improve log panel * fix: spec * doc * fix: log permission * fix: dataset schema required * add loading status * remove ui weight * manage log * fix: log detail per * doc * fix: log menu * rename permission * bg color * fix: app log per * fix: log key selector * fix: log * doc --------- Co-authored-by: heheer Co-authored-by: colnii <1286949794@qq.com> Co-authored-by: 伍闲犬 <76519998+xqvvu@users.noreply.github.com> Co-authored-by: Ctrlz <143257420+ctrlz526@users.noreply.github.com> Co-authored-by: 伍闲犬 Co-authored-by: heheer --- .../docs/introduction/development/faq.mdx | 24 +- .../development/modelConfig/intro.mdx | 8 +- .../development/openapi/dataset.mdx | 2 +- document/content/docs/toc.mdx | 1 + document/content/docs/upgrading/4-11/4112.mdx | 18 - .../content/docs/upgrading/4-11/meta.json | 4 +- document/content/docs/upgrading/4-12/4120.mdx | 55 + .../content/docs/upgrading/4-12/meta.json | 5 + document/data/doc-last-modified.json | 94 +- document/lib/generateToc.js | 14 +- package.json | 11 +- .../global/common/system/types/index.d.ts | 1 + packages/global/core/ai/model.ts | 4 +- packages/global/core/ai/provider.ts | 41 +- packages/global/core/app/constants.ts | 5 +- packages/global/core/app/logs/api.d.ts | 29 + packages/global/core/app/logs/constants.ts | 287 ++++++ packages/global/core/app/logs/type.d.ts | 53 + packages/global/core/app/logs/utils.ts | 59 ++ packages/global/core/app/plugin/type.d.ts | 3 + packages/global/core/app/utils.ts | 2 +- packages/global/core/chat/constants.ts | 30 +- packages/global/core/chat/setting/type.d.ts | 28 + packages/global/core/chat/type.d.ts | 9 +- .../global/core/dataset/training/utils.ts | 2 +- .../global/core/workflow/runtime/type.d.ts | 6 + packages/global/core/workflow/type/node.d.ts | 2 + .../global/support/permission/app/constant.ts | 13 +- packages/service/common/api/plusRequest.ts | 3 +- .../service/common/file/gridfs/controller.ts | 4 - .../service/common/file/image/controller.ts | 7 +- packages/service/common/file/multer.ts | 3 +- .../service/common/vectorDB/controller.d.ts | 3 +- .../service/common/vectorDB/controller.ts | 50 +- .../service/common/vectorDB/milvus/index.ts | 132 +-- .../common/vectorDB/oceanbase/index.ts | 119 +-- packages/service/common/vectorDB/pg/index.ts | 115 +-- .../service/core/ai/audio/transcriptions.ts | 3 +- packages/service/core/ai/embedding/index.ts | 121 ++- .../service/core/app/logs/chatLogsSchema.ts | 77 ++ .../service/core/app/plugin/controller.ts | 78 +- .../core/app/plugin/systemPluginSchema.ts | 4 + packages/service/core/app/plugin/type.d.ts | 2 + packages/service/core/app/utils.ts | 2 + packages/service/core/chat/chatSchema.ts | 2 +- packages/service/core/chat/controller.ts | 4 +- packages/service/core/chat/saveChat.ts | 57 ++ packages/service/core/chat/setting/schema.ts | 37 + packages/service/core/dataset/controller.ts | 5 +- packages/service/core/dataset/read.ts | 3 +- .../service/core/dataset/search/controller.ts | 488 +++++---- .../core/dataset/training/controller.ts | 2 +- .../core/workflow/dispatch/child/runApp.ts | 2 + .../core/workflow/dispatch/child/runTool.ts | 6 +- .../service/core/workflow/dispatch/index.ts | 2 +- .../service/core/workflow/dispatch/utils.ts | 8 +- packages/service/core/workflow/utils.ts | 16 +- packages/service/package.json | 4 +- .../service/support/permission/app/auth.ts | 26 +- packages/service/support/user/team/utils.ts | 35 + packages/templates/src/CQ/template.json | 4 +- .../templates/src/longTranslate/template.json | 2 +- .../src/simpleDatasetChat/template.json | 2 +- .../web/components/common/Icon/constants.ts | 35 +- .../components/common/Icon/icons/chart.svg | 4 + .../Icon/icons/core/chat/setting/share.svg | 3 + .../Icon/icons/core/chat/sidebar/expand.svg | 4 + .../Icon/icons/core/chat/sidebar/fold.svg | 4 + .../Icon/icons/core/chat/sidebar/home.svg | 6 + .../Icon/icons/core/chat/sidebar/menu.svg | 3 + .../common/Icon/icons/model/ai360.svg | 1 + .../common/Icon/icons/model/coze.svg | 1 + .../common/Icon/icons/model/novita.svg | 1 + .../common/Icon/icons/model/openrouter.svg | 1 + .../common/Icon/icons/model/vertexai.svg | 1 + .../web/components/common/Icon/icons/star.svg | 22 + .../components/common/Icon/icons/upload.svg | 3 + .../common/Input/NumberInput/index.tsx | 2 +- .../web/components/common/MyModal/index.tsx | 4 +- .../web/components/common/MySelect/index.tsx | 8 +- .../common/charts/AreaChartComponent.tsx | 226 +++++ .../common/charts/BarChartComponent.tsx | 152 +++ .../common/charts/LineChartComponent.tsx | 180 ++-- packages/web/context/useSystem.tsx | 3 +- packages/web/i18n/en/app.json | 54 +- packages/web/i18n/en/chat.json | 43 + packages/web/i18n/en/common.json | 21 +- packages/web/i18n/en/file.json | 1 + packages/web/i18n/en/login.json | 2 +- packages/web/i18n/zh-CN/app.json | 63 +- packages/web/i18n/zh-CN/chat.json | 45 + packages/web/i18n/zh-CN/common.json | 13 +- packages/web/i18n/zh-CN/file.json | 1 + packages/web/i18n/zh-CN/login.json | 2 +- packages/web/i18n/zh-Hant/app.json | 54 +- packages/web/i18n/zh-Hant/chat.json | 42 + packages/web/i18n/zh-Hant/common.json | 14 +- packages/web/i18n/zh-Hant/file.json | 1 + packages/web/i18n/zh-Hant/login.json | 2 +- packages/web/styles/theme.ts | 25 +- pnpm-lock.yaml | 20 +- projects/app/.env.template | 4 + projects/app/data/config.local.json | 4 +- projects/app/data/model.json | 4 +- projects/app/package.json | 2 +- projects/app/public/icon/login-bg-phone.svg | 257 +++++ .../app/public/imgs/chat/fastgpt_banner.svg | 42 + .../public/imgs/chat/fastgpt_banner_fold.svg | 41 + .../public/imgs/chat/fastgpt_chat_diagram.png | Bin 0 -> 80206 bytes .../imgs/chat/fastgpt_chat_diagram_en.png | Bin 0 -> 84355 bytes .../chat/fastgpt_chat_diagram_zh-Hant.png | Bin 0 -> 81231 bytes projects/app/public/imgs/fastgpt_slogan.png | Bin 4622 -> 0 bytes projects/app/public/imgs/modal/info.svg | 3 + projects/app/public/imgs/proModalBg.png | Bin 0 -> 47434 bytes projects/app/public/imgs/proTag.svg | 10 + projects/app/public/imgs/proTagEng.svg | 10 + projects/app/src/components/Layout/auth.tsx | 1 - projects/app/src/components/Layout/index.tsx | 3 +- projects/app/src/components/MyInput/index.tsx | 9 +- .../src/components/PageContainer/index.tsx | 1 - .../app/src/components/ProTip/ProModal.tsx | 100 ++ .../app/src/components/ProTip/ProText.tsx | 49 + projects/app/src/components/ProTip/Tag.tsx | 18 + .../src/components/Select/AIModelSelector.tsx | 3 +- .../src/components/Select/FileSelector.tsx | 2 +- .../app/src/components/common/folder/Path.tsx | 4 +- .../core/app/DatasetSelectModal.tsx | 504 +++++++--- .../core/app/plugin/CostTooltip.tsx | 21 +- .../ChatContainer/ChatBox/Input/ChatInput.tsx | 200 ++-- .../chat/ChatContainer/ChatBox/Provider.tsx | 4 + .../ChatBox/components/ResponseTags.tsx | 62 +- .../ChatBox/components/WelcomeHomeBox.tsx | 23 + .../chat/ChatContainer/ChatBox/constants.ts | 3 +- .../core/chat/ChatContainer/ChatBox/index.tsx | 16 +- .../components/core/dataset/SelectModal.tsx | 39 +- .../permission/MemberManager/RoleSelect.tsx | 5 +- projects/app/src/global/aiproxy/constants.ts | 39 + .../app/src/global/core/chat/constants.ts | 3 +- projects/app/src/global/core/chat/utils.ts | 58 +- .../account/info/RedeemCouponModal.tsx | 2 +- .../account/model/ModelDashboard/index.tsx | 20 +- .../app/detail/Logs/LogChart.tsx | 943 ++++++++++++++++++ .../app/detail/Logs/LogTable.tsx | 517 ++++++++++ .../app/detail/Logs/SyncLogKeysPopover.tsx | 3 +- .../pageComponents/app/detail/Logs/index.tsx | 549 ++-------- .../app/detail/MCPTools/AppCard.tsx | 2 +- .../app/detail/MCPTools/ChatTest.tsx | 2 +- .../app/detail/MCPTools/EditForm.tsx | 2 +- .../app/detail/MCPTools/Header.tsx | 2 +- .../pageComponents/app/detail/RouteTab.tsx | 32 +- .../SimpleApp/components/ConfigToolModal.tsx | 4 +- .../SimpleApp/components/ToolSelectModal.tsx | 3 +- .../app/detail/SimpleApp/index.tsx | 2 +- .../Flow/components/NodeTemplates/header.tsx | 4 +- .../Flow/components/NodeTemplates/list.tsx | 23 +- .../Flow/components/ToolParamConfig.tsx | 4 +- .../Flow/nodes/NodeTool.tsx | 2 +- .../Flow/nodes/NodeToolSet.tsx | 3 +- .../Flow/nodes/render/Handle/index.tsx | 2 +- .../Flow/nodes/render/NodeCard.tsx | 5 +- .../RenderInput/templates/Reference.tsx | 24 +- .../src/pageComponents/app/detail/context.tsx | 4 +- .../app/plugin/SecretInputModal.tsx | 96 +- .../src/pageComponents/chat/ChatHeader.tsx | 50 +- .../pageComponents/chat/ChatHistorySlider.tsx | 162 ++- .../chat/ChatSetting/DataDashboard.tsx | 56 ++ .../chat/ChatSetting/DiagramModal.tsx | 32 + .../chat/ChatSetting/HomepageSetting.tsx | 399 ++++++++ .../ImageUpload/hooks/useImageUpload.tsx | 124 +++ .../chat/ChatSetting/ImageUpload/index.tsx | 124 +++ .../chat/ChatSetting/LogDetails.tsx | 56 ++ .../chat/ChatSetting/SettingTabs.tsx | 38 + .../chat/ChatSetting/ToolSelectModal.tsx | 479 +++++++++ .../pageComponents/chat/ChatSetting/index.tsx | 90 ++ .../pageComponents/chat/ChatTeamApp/List.tsx | 159 +++ .../chat/ChatTeamApp/TypeTag.tsx | 66 ++ .../pageComponents/chat/ChatTeamApp/index.tsx | 173 ++++ .../chat/ChatWindow/AppChatWindow.tsx | 176 ++++ .../chat/ChatWindow/HomeChatWindow.tsx | 449 +++++++++ .../src/pageComponents/chat/SliderApps.tsx | 671 ++++++++++--- .../pageComponents/chat/UserAvatarPopover.tsx | 28 +- .../app/src/pageComponents/chat/constants.ts | 26 + .../app/src/pageComponents/chat/useChat.ts | 45 + .../pageComponents/dashboard/Container.tsx | 2 +- .../pageComponents/dashboard/apps/List.tsx | 9 +- .../dashboard/apps/MCPToolsEditModal.tsx | 2 +- .../pageComponents/dashboard/apps/TypeTag.tsx | 3 +- .../detail/CollectionCard/TrainingStates.tsx | 4 +- .../dataset/detail/CollectionCard/index.tsx | 5 +- .../dataset/list/CreateModal.tsx | 2 +- .../login/LoginForm/FormLayout.tsx | 94 +- .../login/LoginForm/LoginForm.tsx | 38 +- .../login/LoginForm/PolicyTip.tsx | 47 + .../login/LoginForm/WechatForm.tsx | 23 +- .../src/pageComponents/login/LoginModal.tsx | 59 +- .../app/src/pageComponents/login/index.tsx | 19 +- projects/app/src/pages/_app.tsx | 8 +- .../api/admin/{initv4112.ts => initv4120.ts} | 2 +- .../src/pages/api/common/file/uploadImage.ts | 12 +- .../src/pages/api/core/app/exportChatLogs.ts | 273 ++--- .../app/src/pages/api/core/app/getChatLogs.ts | 3 +- projects/app/src/pages/api/core/app/list.ts | 21 +- .../src/pages/api/core/app/logs/getLogKeys.ts | 4 +- .../app/src/pages/api/core/chat/chatTest.ts | 8 +- .../core/chat/feedback/updateUserFeedback.ts | 52 +- .../api/core/chat/getPaginationRecords.ts | 3 +- projects/app/src/pages/api/core/chat/init.ts | 2 +- .../pages/api/core/dataset/data/insertData.ts | 6 - .../app/src/pages/api/core/dataset/list.ts | 6 +- .../app/src/pages/api/core/workflow/debug.ts | 8 +- .../app/src/pages/api/v1/chat/completions.ts | 11 +- projects/app/src/pages/api/v1/embeddings.ts | 74 -- .../app/src/pages/api/v2/chat/completions.ts | 11 +- projects/app/src/pages/app/detail/index.tsx | 12 +- projects/app/src/pages/chat/index.tsx | 309 +----- projects/app/src/pages/chat/team.tsx | 341 ------- .../pages/dashboard/[pluginGroupId]/index.tsx | 2 +- .../app/src/pages/dashboard/apps/index.tsx | 5 +- projects/app/src/pages/login/index.tsx | 62 +- .../app/src/service/common/system/index.ts | 1 + projects/app/src/service/core/app/utils.ts | 8 +- .../service/core/dataset/data/controller.ts | 80 +- .../core/dataset/queues/generateVector.ts | 36 +- projects/app/src/service/support/mcp/utils.ts | 6 +- .../service/support/permission/auth/chat.ts | 13 +- projects/app/src/web/common/api/request.ts | 1 - projects/app/src/web/core/app/api.ts | 5 + projects/app/src/web/core/app/api/log.ts | 12 + projects/app/src/web/core/app/templates.ts | 3 +- projects/app/src/web/core/chat/api.ts | 13 + .../core/chat/context/chatSettingContext.tsx | 109 ++ .../src/web/core/chat/context/useChatStore.ts | 15 +- .../training/deleteTrainingData.test.ts | 4 +- .../training/getTrainingDataDetail.test.ts | 4 +- .../dataset/training/getTrainingError.test.ts | 4 +- .../training/updateTrainingData.test.ts | 4 +- projects/sandbox/src/http-exception.filter.ts | 1 - test/cases/service/support/mcp/utils.test.ts | 87 -- test/setupModels.ts | 14 +- 239 files changed, 9336 insertions(+), 3128 deletions(-) delete mode 100644 document/content/docs/upgrading/4-11/4112.mdx create mode 100644 document/content/docs/upgrading/4-12/4120.mdx create mode 100644 document/content/docs/upgrading/4-12/meta.json create mode 100644 packages/global/core/app/logs/api.d.ts create mode 100644 packages/global/core/app/logs/utils.ts create mode 100644 packages/global/core/chat/setting/type.d.ts create mode 100644 packages/service/core/app/logs/chatLogsSchema.ts create mode 100644 packages/service/core/chat/setting/schema.ts create mode 100644 packages/service/support/user/team/utils.ts create mode 100644 packages/web/components/common/Icon/icons/chart.svg create mode 100644 packages/web/components/common/Icon/icons/core/chat/setting/share.svg create mode 100644 packages/web/components/common/Icon/icons/core/chat/sidebar/expand.svg create mode 100644 packages/web/components/common/Icon/icons/core/chat/sidebar/fold.svg create mode 100644 packages/web/components/common/Icon/icons/core/chat/sidebar/home.svg create mode 100644 packages/web/components/common/Icon/icons/core/chat/sidebar/menu.svg create mode 100644 packages/web/components/common/Icon/icons/model/ai360.svg create mode 100644 packages/web/components/common/Icon/icons/model/coze.svg create mode 100644 packages/web/components/common/Icon/icons/model/novita.svg create mode 100644 packages/web/components/common/Icon/icons/model/openrouter.svg create mode 100644 packages/web/components/common/Icon/icons/model/vertexai.svg create mode 100644 packages/web/components/common/Icon/icons/star.svg create mode 100644 packages/web/components/common/Icon/icons/upload.svg create mode 100644 packages/web/components/common/charts/AreaChartComponent.tsx create mode 100644 packages/web/components/common/charts/BarChartComponent.tsx create mode 100644 projects/app/public/icon/login-bg-phone.svg create mode 100644 projects/app/public/imgs/chat/fastgpt_banner.svg create mode 100644 projects/app/public/imgs/chat/fastgpt_banner_fold.svg create mode 100644 projects/app/public/imgs/chat/fastgpt_chat_diagram.png create mode 100644 projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png create mode 100644 projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png delete mode 100644 projects/app/public/imgs/fastgpt_slogan.png create mode 100644 projects/app/public/imgs/modal/info.svg create mode 100644 projects/app/public/imgs/proModalBg.png create mode 100644 projects/app/public/imgs/proTag.svg create mode 100644 projects/app/public/imgs/proTagEng.svg create mode 100644 projects/app/src/components/ProTip/ProModal.tsx create mode 100644 projects/app/src/components/ProTip/ProText.tsx create mode 100644 projects/app/src/components/ProTip/Tag.tsx create mode 100644 projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx create mode 100644 projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx create mode 100644 projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/DiagramModal.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/hooks/useImageUpload.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/index.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/SettingTabs.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatSetting/index.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatTeamApp/TypeTag.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx create mode 100644 projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx create mode 100644 projects/app/src/pageComponents/chat/constants.ts create mode 100644 projects/app/src/pageComponents/chat/useChat.ts create mode 100644 projects/app/src/pageComponents/login/LoginForm/PolicyTip.tsx rename projects/app/src/pages/api/admin/{initv4112.ts => initv4120.ts} (97%) delete mode 100644 projects/app/src/pages/api/v1/embeddings.ts delete mode 100644 projects/app/src/pages/chat/team.tsx create mode 100644 projects/app/src/web/core/chat/context/chatSettingContext.tsx diff --git a/document/content/docs/introduction/development/faq.mdx b/document/content/docs/introduction/development/faq.mdx index ba00cbc6e..37f8e5cbf 100644 --- a/document/content/docs/introduction/development/faq.mdx +++ b/document/content/docs/introduction/development/faq.mdx @@ -272,7 +272,7 @@ curl --location --request POST 'https://oneapi.xxx/v1/chat/completions' \ --header 'Authorization: Bearer sk-xxxx' \ --header 'Content-Type: application/json' \ --data-raw '{ - "model": "gpt-4o-mini", + "model": "gpt-5", "temperature": 0.01, "max_tokens": 8000, "stream": true, @@ -306,19 +306,13 @@ curl --location --request POST 'https://oneapi.xxx/v1/chat/completions' \ ```json { - "id": "chatcmpl-A7kwo1rZ3OHYSeIFgfWYxu8X2koN3", - "object": "chat.completion.chunk", - "created": 1726412126, - "model": "gpt-4o-mini-2024-07-18", - "system_fingerprint": "fp_483d39d857", - "choices": [ - { - "index": 0, - "delta": { - "role": "assistant", - "content": null, - "tool_calls": [ - { + "id": "chatcmpl-A7kwo1rZ3OHYSeIFgfWYxu8X2koN3", + "object": "chat.completion.chunk", + "created": 1726412126, + "model": "gpt-5", + "system_fingerprint": "fp_483d39d857", + "choices": [ + { "index": 0, "id": "call_0n24eiFk8OUyIyrdEbLdirU7", "type": "function", @@ -347,7 +341,7 @@ curl --location --request POST 'https://oneapi.xxxx/v1/chat/completions' \ --header 'Authorization: Bearer sk-xxx' \ --header 'Content-Type: application/json' \ --data-raw '{ - "model": "gpt-4o-mini", + "model": "gpt-5", "temperature": 0.01, "max_tokens": 8000, "stream": true, diff --git a/document/content/docs/introduction/development/modelConfig/intro.mdx b/document/content/docs/introduction/development/modelConfig/intro.mdx index b2a7d2cc3..2e4212e45 100644 --- a/document/content/docs/introduction/development/modelConfig/intro.mdx +++ b/document/content/docs/introduction/development/modelConfig/intro.mdx @@ -94,8 +94,8 @@ import { Alert } from '@/components/docs/Alert'; "isCustom": true, // 是否为自定义模型 "isActive": true, // 是否启用 "provider": "OpenAI", // 模型提供商,主要用于分类展示,目前已经内置提供商包括:https://github.com/labring/FastGPT/blob/main/packages/global/core/ai/provider.ts, 可 pr 提供新的提供商,或直接填写 Other - "model": "gpt-4o-mini", // 模型ID(对应OneAPI中渠道的模型名) - "name": "gpt-4o-mini", // 模型别名 + "model": "gpt-5", // 模型ID(对应OneAPI中渠道的模型名) + "name": "gpt-5", // 模型别名 "maxContext": 125000, // 最大上下文 "maxResponse": 16000, // 最大回复 "quoteMaxToken": 120000, // 最大引用内容 @@ -303,8 +303,8 @@ OneAPI 的语言识别接口,无法正确的识别其他模型(会始终识 "llmModels": [ { "provider": "OpenAI", // 模型提供商,主要用于分类展示,目前已经内置提供商包括:https://github.com/labring/FastGPT/blob/main/packages/global/core/ai/provider.ts, 可 pr 提供新的提供商,或直接填写 Other - "model": "gpt-4o-mini", // 模型名(对应OneAPI中渠道的模型名) - "name": "gpt-4o-mini", // 模型别名 + "model": "gpt-5", // 模型名(对应OneAPI中渠道的模型名) + "name": "gpt-5", // 模型别名 "maxContext": 125000, // 最大上下文 "maxResponse": 16000, // 最大回复 "quoteMaxToken": 120000, // 最大引用内容 diff --git a/document/content/docs/introduction/development/openapi/dataset.mdx b/document/content/docs/introduction/development/openapi/dataset.mdx index c1699cac4..cbcb508a8 100644 --- a/document/content/docs/introduction/development/openapi/dataset.mdx +++ b/document/content/docs/introduction/development/openapi/dataset.mdx @@ -1249,7 +1249,7 @@ curl --location --request POST 'https://api.fastgpt.in/api/core/dataset/searchTe "usingReRank": false, "datasetSearchUsingExtensionQuery": true, - "datasetSearchExtensionModel": "gpt-4o-mini", + "datasetSearchExtensionModel": "gpt-5", "datasetSearchExtensionBg": "" }' ``` diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index 1bb8187c6..3377720ff 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -98,6 +98,7 @@ description: FastGPT 文档目录 - [/docs/upgrading/4-10/4101](/docs/upgrading/4-10/4101) - [/docs/upgrading/4-11/4110](/docs/upgrading/4-11/4110) - [/docs/upgrading/4-11/4111](/docs/upgrading/4-11/4111) +- [/docs/upgrading/4-12/4120](/docs/upgrading/4-12/4120) - [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40) - [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41) - [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42) diff --git a/document/content/docs/upgrading/4-11/4112.mdx b/document/content/docs/upgrading/4-11/4112.mdx deleted file mode 100644 index e7e38e878..000000000 --- a/document/content/docs/upgrading/4-11/4112.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: 'V4.11.2(进行中)' -description: 'FastGPT V4.11.2 更新说明' ---- - -## 🚀 新增内容 - -## ⚙️ 优化 - -1. 优化 3 处存在潜在内存泄露的代码。 -2. 优化工作流部分递归检查,避免无限递归。 -3. 优化文档阅读 Worker,采用 ShareBuffer 避免数据拷贝。 - -## 🐛 修复 - -1. Doc2x API 更新,导致解析失败。 - -## 🔨 工具更新 diff --git a/document/content/docs/upgrading/4-11/meta.json b/document/content/docs/upgrading/4-11/meta.json index b973d73bf..b72fcac9a 100644 --- a/document/content/docs/upgrading/4-11/meta.json +++ b/document/content/docs/upgrading/4-11/meta.json @@ -1,5 +1,5 @@ { - "title": "4.11.x", + "title": "4.12.x", "description": "", - "pages": ["4112", "4111", "4110"] + "pages": ["4120"] } diff --git a/document/content/docs/upgrading/4-12/4120.mdx b/document/content/docs/upgrading/4-12/4120.mdx new file mode 100644 index 000000000..9f038d9aa --- /dev/null +++ b/document/content/docs/upgrading/4-12/4120.mdx @@ -0,0 +1,55 @@ +--- +title: 'V4.12.0(进行中)' +description: 'FastGPT V4.12.0 更新说明' +--- + +## 更新指南 + +### 1. 更新镜像: + +### 2. 执行升级脚本 + +该脚本仅需商业版用户执行。 + +从任意终端,发起 1 个 HTTP 请求。其中 `{{rootkey}}` 替换成环境变量里的 `rootkey`;`{{host}}` 替换成**FastGPT 域名**。 + +```bash +curl --location --request POST 'https://{{host}}/api/admin/initv4120' \ +--header 'rootkey: {{rootkey}}' \ +--header 'Content-Type: application/json' +``` + +**脚本功能** + +1. 初始化团队成员的应用对话日志权限。 + +## 🚀 新增内容 + +1. 商业版支持应用日志数据看板。 +2. 商业版支持简易对话页,可直接选择模型和预设工具进行聊天,无需进行应用搭建。 +3. 对话页,增加团队应用快速切换。 +4. 权限表调整,采用 Role 映射 Permission 模式。 +5. 应用可单独分配对话日志查看权限。 + +## ⚙️ 优化 + +1. 优化 3 处存在潜在内存泄露的代码。 +2. 优化工作流部分递归检查,避免无限递归。 +3. 优化文档阅读 Worker,采用 ShareBuffer 避免数据拷贝。 +4. 批量进行向量生成和入库,减少网络操作。 +5. 知识库搜索,多 query 合并计算,减少数据库操作。 +6. 选择知识库交互优化。 +7. 登录页 UI 调整。 +8. 工作流中,更严格检测工具集是否可被添加。 +9. 对话日志导出,仅导出选中的表头,并修复部分表头无法导出的问题。 + +## 🐛 修复 + +1. Doc2x API 更新,导致解析失败。 +2. 工作流中,团队应用目录也可以被加入工作流。 +3. 工作流,数组选择器 UI 缺陷。 +4. 成员同步存在权限未完成删除问题 + +## 🔨 工具更新 + +1. 系统工具可返回 citeLinks 响应值,从而在对话框实现引用链接展示。 \ No newline at end of file diff --git a/document/content/docs/upgrading/4-12/meta.json b/document/content/docs/upgrading/4-12/meta.json new file mode 100644 index 000000000..b973d73bf --- /dev/null +++ b/document/content/docs/upgrading/4-12/meta.json @@ -0,0 +1,5 @@ +{ + "title": "4.11.x", + "description": "", + "pages": ["4112", "4111", "4110"] +} diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 046f88d07..c44a1909d 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -5,39 +5,39 @@ "document/content/docs/faq/error.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/external_channel_integration.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/index.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/faq/other.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/faq/other.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/faq/points_consumption.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/introduction/cloud.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/introduction/commercial.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/introduction/commercial.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/introduction/development/community.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/introduction/development/configuration.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/development/configuration.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/custom-models/bge-rerank.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/custom-models/chatglm2-m3e.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/development/custom-models/chatglm2-m3e.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/custom-models/chatglm2.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/custom-models/m3e.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/custom-models/marker.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/custom-models/ollama.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/development/custom-models/marker.mdx": "2025-08-04T22:07:52+08:00", + "document/content/docs/introduction/development/custom-models/ollama.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/design/dataset.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-07-24T13:00:27+08:00", - "document/content/docs/introduction/development/docker.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/introduction/development/faq.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/intro.mdx": "2025-07-24T10:39:41+08:00", + "document/content/docs/introduction/development/docker.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/faq.mdx": "2025-08-09T14:20:10+08:00", + "document/content/docs/introduction/development/intro.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/migration/docker_db.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/migration/docker_mongo.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/modelConfig/ai-proxy.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/modelConfig/intro.mdx": "2025-08-01T16:08:20+08:00", + "document/content/docs/introduction/development/modelConfig/ai-proxy.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/modelConfig/intro.mdx": "2025-08-09T14:20:10+08:00", "document/content/docs/introduction/development/modelConfig/one-api.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/openapi/chat.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-04T18:04:39+08:00", + "document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-09T14:20:10+08:00", "document/content/docs/introduction/development/openapi/intro.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/openapi/share.mdx": "2025-08-04T18:09:06+08:00", + "document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/http_proxy.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/nginx.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/sealos.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/introduction/development/sealos.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/DialogBoxes/htmlRendering.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/DialogBoxes/quoteList.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/admin/sso.mdx": "2025-07-24T13:00:27+08:00", @@ -52,7 +52,7 @@ "document/content/docs/introduction/guide/dashboard/intro.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/dashboard/mcp_server.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/dashboard/mcp_tools.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/dashboard/workflow/ai_chat.mdx": "2025-07-24T13:00:27+08:00", + "document/content/docs/introduction/guide/dashboard/workflow/ai_chat.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/dashboard/workflow/content_extract.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/dashboard/workflow/coreferenceResolution.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/dashboard/workflow/custom_feedback.mdx": "2025-07-23T21:35:03+08:00", @@ -78,7 +78,7 @@ "document/content/docs/introduction/guide/knowledge_base/lark_dataset.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/knowledge_base/template.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/knowledge_base/third_dataset.mdx": "2025-07-24T13:00:27+08:00", - "document/content/docs/introduction/guide/knowledge_base/websync.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/guide/knowledge_base/websync.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/knowledge_base/yuque_dataset.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/bing_search_plugin.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-07-30T22:30:03+08:00", @@ -90,19 +90,19 @@ "document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/index.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/protocol/index.mdx": "2025-07-30T15:38:30+08:00", - "document/content/docs/protocol/open-source.en.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/protocol/open-source.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/protocol/open-source.en.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/protocol/open-source.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/protocol/privacy.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/privacy.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2025-08-04T13:42:36+08:00", + "document/content/docs/toc.mdx": "2025-08-12T13:45:56+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4101.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-11/4111.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-11/4112.mdx": "2025-08-03T22:37:45+08:00", + "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", + "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-12T21:04:44+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", @@ -114,21 +114,21 @@ "document/content/docs/upgrading/4-8/445.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/446.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/447.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/45.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/45.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-8/451.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/452.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/46.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/46.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-8/461.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/462.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/462.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/upgrading/4-8/463.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/464.mdx": "2025-08-04T18:09:06+08:00", - "document/content/docs/upgrading/4-8/465.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/466.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/467.mdx": "2025-08-04T18:09:06+08:00", - "document/content/docs/upgrading/4-8/468.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/469.mdx": "2025-08-04T18:09:06+08:00", - "document/content/docs/upgrading/4-8/47.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/471.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/464.mdx": "2025-08-04T18:10:58+08:00", + "document/content/docs/upgrading/4-8/465.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/upgrading/4-8/466.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/upgrading/4-8/467.mdx": "2025-08-04T18:10:58+08:00", + "document/content/docs/upgrading/4-8/468.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/upgrading/4-8/469.mdx": "2025-08-04T18:10:58+08:00", + "document/content/docs/upgrading/4-8/47.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/upgrading/4-8/471.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-8/48.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/481.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4810.mdx": "2025-08-02T19:38:37+08:00", @@ -136,13 +136,13 @@ "document/content/docs/upgrading/4-8/4812.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4813.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4814.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/4815.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/4816.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/4815.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/upgrading/4-8/4816.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-8/4817.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4818.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4819.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/482.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-8/4820.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-8/4820.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-8/4821.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4822.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/4823.mdx": "2025-08-02T19:38:37+08:00", @@ -153,18 +153,18 @@ "document/content/docs/upgrading/4-8/487.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/488.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/489.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-9/490.mdx": "2025-08-04T18:09:06+08:00", + "document/content/docs/upgrading/4-9/490.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-9/491.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-9/4910.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-9/4910.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/upgrading/4-9/4911.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/4912.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/4913.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/4914.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-9/492.mdx": "2025-08-04T18:09:06+08:00", + "document/content/docs/upgrading/4-9/492.mdx": "2025-08-04T18:10:58+08:00", "document/content/docs/upgrading/4-9/493.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/494.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/495.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-9/496.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-9/496.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/upgrading/4-9/497.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/498.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-9/499.mdx": "2025-08-02T19:38:37+08:00", @@ -176,11 +176,11 @@ "document/content/docs/use-cases/app-cases/google_search.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/multi_turn_translation_bot.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/use-cases/app-cases/submit_application_template.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/use-cases/app-cases/submit_application_template.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/use-cases/app-cases/translate-subtitle-using-gpt.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/external-integration/dingtalk.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00", - "document/content/docs/use-cases/external-integration/official_account.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-04T18:09:06+08:00", + "document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-04T18:10:58+08:00", "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" } \ No newline at end of file diff --git a/document/lib/generateToc.js b/document/lib/generateToc.js index c0aac0adf..6fb85eea7 100644 --- a/document/lib/generateToc.js +++ b/document/lib/generateToc.js @@ -1,6 +1,6 @@ -import * as fs from 'node:fs/promises'; -import path from 'node:path'; -import fg from 'fast-glob'; +const fs = require('node:fs/promises'); +const path = require('node:path'); +const fg = require('fast-glob'); // 假设 i18n.defaultLanguage = 'zh-CN',这里不用 i18n 直接写两份逻辑即可 @@ -15,8 +15,8 @@ const blacklist = [ ]; function filePathToUrl(filePath, lang) { - const baseDir = path.resolve('../content/docs'); - let relativePath = path.relative(baseDir, path.resolve(filePath)).replace(/\\/g, '/'); + const baseDir = path.join(__dirname, '../content/docs'); + let relativePath = filePath.replace(baseDir, ''); const basePath = lang === 'zh-CN' ? '/docs' : '/en/docs'; if (lang !== 'zh-CN' && relativePath.endsWith('.en.mdx')) { @@ -44,7 +44,7 @@ function isZhFile(file) { async function generateToc() { // 匹配所有 mdx 文件 - const allFiles = await fg('../content/docs/**/*.mdx'); + const allFiles = await fg(path.join(__dirname, '../content/docs/**/*.mdx')) // 筛选中英文文件 const zhFiles = allFiles.filter(isZhFile); @@ -72,7 +72,7 @@ ${urls.map((url) => `- [${url}](${url})`).join('\n')} `; // 写文件路径 - const baseDir = path.resolve('../content/docs'); + const baseDir = path.join(__dirname, '../content/docs'); const zhOutputPath = path.join(baseDir, 'toc.mdx'); const enOutputPath = path.join(baseDir, 'toc.en.mdx'); diff --git a/package.json b/package.json index 14ea30c9b..463cee837 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "initDocToc": "node ./document/lib/generateToc.js", "gen:theme-typings": "chakra-cli tokens packages/web/styles/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts", "postinstall": "pnpm gen:theme-typings", - "initIcon": "node ./scripts/icon/init.js", + "initIcon": "node ./scripts/icon/init.js && prettier --config \"./.prettierrc.js\" --write \"packages/web/components/common/Icon/constants.ts\"", "previewIcon": "node ./scripts/icon/index.js", "api:gen": "tsc ./scripts/openapi/index.ts && node ./scripts/openapi/index.js && npx @redocly/cli build-docs ./scripts/openapi/openapi.json -o ./projects/app/public/openapi/index.html", "create:i18n": "node ./scripts/i18n/index.js", @@ -54,7 +54,12 @@ "mdast-util-gfm-autolink-literal": "2.0.0" }, "engines": { - "node": ">=18.16.0", - "pnpm": ">=9.0.0" + "node": ">=20" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "9.15.9" + } } } diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 7710ee12f..c52feedb4 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -45,6 +45,7 @@ export type FastGPTFeConfigsType = { show_workorder?: boolean; show_emptyChat?: boolean; isPlus?: boolean; + hideChatCopyrightSetting?: boolean; register_method?: ['email' | 'phone' | 'sync']; login_method?: ['email' | 'phone']; // Attention: login method is diffrent with oauth find_password_method?: ['email' | 'phone']; diff --git a/packages/global/core/ai/model.ts b/packages/global/core/ai/model.ts index c059b1b78..5de8129e0 100644 --- a/packages/global/core/ai/model.ts +++ b/packages/global/core/ai/model.ts @@ -14,8 +14,8 @@ export const defaultQAModels: LLMModelItemType[] = [ { type: ModelTypeEnum.llm, provider: 'OpenAI', - model: 'gpt-4o-mini', - name: 'gpt-4o-mini', + model: 'gpt-5', + name: 'gpt-5', maxContext: 16000, maxResponse: 16000, quoteMaxToken: 13000, diff --git a/packages/global/core/ai/provider.ts b/packages/global/core/ai/provider.ts index e2fd97bf6..55270a0b9 100644 --- a/packages/global/core/ai/provider.ts +++ b/packages/global/core/ai/provider.ts @@ -21,14 +21,19 @@ export type ModelProviderIdType = | 'Hunyuan' | 'Baichuan' | 'StepFun' + | 'ai360' | 'Yi' | 'Siliconflow' | 'PPIO' + | 'OpenRouter' | 'Ollama' + | 'novita' + | 'vertexai' | 'BAAI' | 'FishAudio' | 'Intern' | 'Moka' + | 'Jina' | 'Other'; export type ModelProviderType = { @@ -133,17 +138,16 @@ export const ModelProviderList: ModelProviderType[] = [ 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: 'Ollama', - name: 'Ollama', - avatar: 'model/ollama' - }, { id: 'BAAI', name: i18nT('common:model_baai'), @@ -164,6 +168,31 @@ export const ModelProviderList: ModelProviderType[] = [ 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'), diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 7a0cf126b..92216c8f6 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -13,7 +13,8 @@ export enum AppTypeEnum { plugin = 'plugin', httpPlugin = 'httpPlugin', toolSet = 'toolSet', - tool = 'tool' + tool = 'tool', + hidden = 'hidden' } export const AppFolderTypeList = [AppTypeEnum.folder, AppTypeEnum.httpPlugin]; @@ -33,7 +34,7 @@ export const defaultWhisperConfig: AppWhisperConfigType = { export const defaultQGConfig: AppQGConfigType = { open: false, - model: 'gpt-4o-mini', + model: 'gpt-5', customPrompt: '' }; diff --git a/packages/global/core/app/logs/api.d.ts b/packages/global/core/app/logs/api.d.ts new file mode 100644 index 000000000..63541399c --- /dev/null +++ b/packages/global/core/app/logs/api.d.ts @@ -0,0 +1,29 @@ +import type { AppLogTimespanEnum } from './constants'; +import type { AppChatLogAppData, AppChatLogChatData, AppChatLogUserData } from './type'; + +export type getChartDataBody = { + appId: string; + dateStart: Date; + dateEnd: Date; + source?: ChatSourceEnum[]; + offset: number; + userTimespan: AppLogTimespanEnum; + chatTimespan: AppLogTimespanEnum; + appTimespan: AppLogTimespanEnum; +}; + +export type getChartDataResponse = { + userData: AppChatLogUserData; + chatData: AppChatLogChatData; + appData: AppChatLogAppData; +}; + +export type getTotalDataQuery = { + appId: string; +}; + +export type getTotalDataResponse = { + totalUsers: number; + totalChats: number; + totalPoints: number; +}; diff --git a/packages/global/core/app/logs/constants.ts b/packages/global/core/app/logs/constants.ts index cc8122080..8bce49ac9 100644 --- a/packages/global/core/app/logs/constants.ts +++ b/packages/global/core/app/logs/constants.ts @@ -47,3 +47,290 @@ export const DefaultAppLogKeys = [ { key: AppLogKeysEnum.RESPONSE_TIME, enable: false }, { key: AppLogKeysEnum.ERROR_COUNT, enable: false } ]; + +export enum AppLogTimespanEnum { + day = 'day', + week = 'week', + month = 'month', + quarter = 'quarter' +} + +export const offsetOptions = [ + { label: 'T+1', value: '1' }, + { label: 'T+3', value: '3' }, + { label: 'T+7', value: '7' }, + { label: 'T+14', value: '14' } +]; + +export const fakeChartData = { + user: [ + { + x: '07-30', + xLabel: '07-30', + userCount: 8, + newUserCount: 5, + retentionUserCount: 3, + points: 100, + sourceCountMap: { + test: 1, + online: 1, + share: 1, + api: 2, + cronJob: 0, + team: 1, + feishu: 0, + official_account: 1, + wecom: 1, + mcp: 0 + } + }, + { + x: '07-31', + xLabel: '07-31', + userCount: 12, + newUserCount: 8, + retentionUserCount: 4, + points: 160, + sourceCountMap: { + test: 2, + online: 2, + share: 2, + api: 3, + cronJob: 0, + team: 2, + feishu: 0, + official_account: 1, + wecom: 1, + mcp: 0 + } + }, + { + x: '08-01', + xLabel: '08-01', + userCount: 18, + newUserCount: 12, + retentionUserCount: 6, + points: 220, + sourceCountMap: { + test: 2, + online: 3, + share: 2, + api: 4, + cronJob: 1, + team: 2, + feishu: 0, + official_account: 1, + wecom: 1, + mcp: 0 + } + }, + { + x: '08-02', + xLabel: '08-02', + userCount: 15, + newUserCount: 7, + retentionUserCount: 8, + points: 180, + sourceCountMap: { + test: 1, + online: 2, + share: 2, + api: 3, + cronJob: 1, + team: 2, + feishu: 1, + official_account: 1, + wecom: 0, + mcp: 0 + } + }, + { + x: '08-03', + xLabel: '08-03', + userCount: 20, + newUserCount: 15, + retentionUserCount: 5, + points: 250, + sourceCountMap: { + test: 2, + online: 4, + share: 2, + api: 5, + cronJob: 1, + team: 2, + feishu: 1, + official_account: 1, + wecom: 0, + mcp: 0 + } + }, + { + x: '08-04', + xLabel: '08-04', + userCount: 14, + newUserCount: 6, + retentionUserCount: 8, + points: 170, + sourceCountMap: { + test: 1, + online: 3, + share: 1, + api: 4, + cronJob: 1, + team: 2, + feishu: 1, + official_account: 1, + wecom: 0, + mcp: 0 + } + }, + { + x: '08-05', + xLabel: '08-05', + userCount: 22, + newUserCount: 17, + retentionUserCount: 5, + points: 280, + sourceCountMap: { + test: 2, + online: 5, + share: 2, + api: 6, + cronJob: 1, + team: 2, + feishu: 1, + official_account: 1, + wecom: 0, + mcp: 0 + } + } + ], + chat: [ + { + x: '07-30', + xLabel: '07-30', + chatItemCount: 20, + chatCount: 12, + pointsPerChat: 5.5, + errorCount: 2, + errorRate: 0.1 + }, + { + x: '07-31', + xLabel: '07-31', + chatItemCount: 35, + chatCount: 20, + pointsPerChat: 8.0, + errorCount: 1, + errorRate: 0.028 + }, + { + x: '08-01', + xLabel: '08-01', + chatItemCount: 50, + chatCount: 30, + pointsPerChat: 7.3, + errorCount: 3, + errorRate: 0.06 + }, + { + x: '08-02', + xLabel: '08-02', + chatItemCount: 28, + chatCount: 18, + pointsPerChat: 6.2, + errorCount: 1, + errorRate: 0.036 + }, + { + x: '08-03', + xLabel: '08-03', + chatItemCount: 60, + chatCount: 40, + pointsPerChat: 7.8, + errorCount: 4, + errorRate: 0.067 + }, + { + x: '08-04', + xLabel: '08-04', + chatItemCount: 32, + chatCount: 22, + pointsPerChat: 6.5, + errorCount: 2, + errorRate: 0.062 + }, + { + x: '08-05', + xLabel: '08-05', + chatItemCount: 55, + chatCount: 35, + pointsPerChat: 8.1, + errorCount: 1, + errorRate: 0.018 + } + ], + app: [ + { + x: '07-30', + xLabel: '07-30', + goodFeedBackCount: 2, + badFeedBackCount: 1, + avgDuration: 2.5 + }, + { + x: '07-31', + xLabel: '07-31', + goodFeedBackCount: 5, + badFeedBackCount: 2, + avgDuration: 2.1 + }, + { + x: '08-01', + xLabel: '08-01', + goodFeedBackCount: 3, + badFeedBackCount: 1, + avgDuration: 2.8 + }, + { + x: '08-02', + xLabel: '08-02', + goodFeedBackCount: 6, + badFeedBackCount: 3, + avgDuration: 2.0 + }, + { + x: '08-03', + xLabel: '08-03', + goodFeedBackCount: 4, + badFeedBackCount: 2, + avgDuration: 2.7 + }, + { + x: '08-04', + xLabel: '08-04', + goodFeedBackCount: 7, + badFeedBackCount: 1, + avgDuration: 2.3 + }, + { + x: '08-05', + xLabel: '08-05', + goodFeedBackCount: 3, + badFeedBackCount: 2, + avgDuration: 2.9 + } + ], + cumulative: { + userCount: 109, + points: 1360, + chatItemCount: 280, + chatCount: 177, + pointsPerChat: 7.2, + errorCount: 14, + errorRate: 0.053, + goodFeedBackCount: 30, + badFeedBackCount: 12, + avgDuration: 2.47 + } +}; diff --git a/packages/global/core/app/logs/type.d.ts b/packages/global/core/app/logs/type.d.ts index 84e198d9f..d02eb1361 100644 --- a/packages/global/core/app/logs/type.d.ts +++ b/packages/global/core/app/logs/type.d.ts @@ -1,3 +1,4 @@ +import type { ChatSourceEnum } from '../../core/chat/constants'; import type { AppLogKeysEnum } from './constants'; export type AppLogKeysType = { @@ -10,3 +11,55 @@ export type AppLogKeysSchemaType = { appId: string; logKeys: AppLogKeysType[]; }; + +export type AppChatLogSchema = { + _id: string; + appId: string; + teamId: string; + chatId: string; + userId: string; + source: string; + sourceName?: string; + createTime: Date; + updateTime: Date; + + chatItemCount: number; + errorCount: number; + totalPoints: number; + goodFeedbackCount: number; + badFeedbackCount: number; + totalResponseTime: number; + + isFirstChat: boolean; // whether this is the user's first session in the app +}; + +export type AppChatLogUserData = { + timestamp: number; + summary: { + userCount: number; + newUserCount: number; + retentionUserCount: number; + points: number; + sourceCountMap: Record; + }; +}[]; + +export type AppChatLogChatData = { + timestamp: number; + summary: { + chatItemCount: number; + chatCount: number; + errorCount: number; + points: number; + }; +}[]; + +export type AppChatLogAppData = { + timestamp: number; + summary: { + goodFeedBackCount: number; + badFeedBackCount: number; + chatCount: number; + totalResponseTime: number; + }; +}[]; diff --git a/packages/global/core/app/logs/utils.ts b/packages/global/core/app/logs/utils.ts new file mode 100644 index 000000000..93783bb95 --- /dev/null +++ b/packages/global/core/app/logs/utils.ts @@ -0,0 +1,59 @@ +import dayjs from 'dayjs'; +import { AppLogTimespanEnum } from './constants'; + +export const formatDateByTimespan = (timestamp: number, timespan: AppLogTimespanEnum) => { + const date = new Date(timestamp); + + if (timespan === AppLogTimespanEnum.day) { + return { + date: dayjs(date).format('MM-DD'), + xLabel: dayjs(date).format('YYYY-MM-DD') + }; + } else if (timespan === AppLogTimespanEnum.week) { + const startStr = dayjs(date).format('MM/DD'); + const endStr = dayjs(date).add(6, 'day').format('MM/DD'); + + return { + date: `${startStr}-${endStr}`, + xLabel: `${startStr}-${endStr}` + }; + } else if (timespan === AppLogTimespanEnum.month) { + return { + date: dayjs(date).format('YYYY-MM'), + xLabel: dayjs(date).format('YYYY-MM') + }; + } else { + const year = date.getFullYear(); + const quarter = Math.ceil((date.getMonth() + 1) / 3); + return { + date: `${year}Q${quarter}`, + xLabel: `${year}Q${quarter}` + }; + } +}; + +export const calculateOffsetDates = ( + start: Date, + end: Date, + offset: number, + timespan: AppLogTimespanEnum +) => { + const offsetStart = new Date(start); + const offsetEnd = new Date(end); + + if (timespan === AppLogTimespanEnum.quarter) { + offsetStart.setMonth(offsetStart.getMonth() + offset * 3); + offsetEnd.setMonth(offsetEnd.getMonth() + offset * 3); + } else if (timespan === AppLogTimespanEnum.month) { + offsetStart.setMonth(offsetStart.getMonth() + offset); + offsetEnd.setMonth(offsetEnd.getMonth() + offset); + } else if (timespan === AppLogTimespanEnum.week) { + offsetStart.setDate(offsetStart.getDate() + offset * 7); + offsetEnd.setDate(offsetEnd.getDate() + offset * 7); + } else { + offsetStart.setDate(offsetStart.getDate() + offset); + offsetEnd.setDate(offsetEnd.getDate() + offset); + } + + return { offsetStart, offsetEnd }; +}; diff --git a/packages/global/core/app/plugin/type.d.ts b/packages/global/core/app/plugin/type.d.ts index 5848c9ffc..807e15a4d 100644 --- a/packages/global/core/app/plugin/type.d.ts +++ b/packages/global/core/app/plugin/type.d.ts @@ -19,6 +19,7 @@ export type PluginRuntimeType = { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[]; currentCost?: number; + systemKeyCost?: number; hasTokenFee?: boolean; }; @@ -44,6 +45,7 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { // commercial plugin config originCost?: number; // n points/one time currentCost?: number; + systemKeyCost?: number; hasTokenFee?: boolean; pluginOrder?: number; @@ -52,6 +54,7 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { // Admin config inputList?: FlowNodeInputItemType['inputList']; + inputListVal?: Record; hasSystemSecret?: boolean; }; diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts index f1447c83a..e967f7584 100644 --- a/packages/global/core/app/utils.ts +++ b/packages/global/core/app/utils.ts @@ -13,7 +13,7 @@ import pluginErrList from '../../common/error/code/plugin'; export const getDefaultAppForm = (): AppSimpleEditFormType => { return { aiSettings: { - model: 'gpt-4o-mini', + model: '', systemPrompt: '', temperature: 0, isResponseAnswerText: true, diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index 43b18c27b..424678f68 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -44,34 +44,44 @@ export enum ChatSourceEnum { export const ChatSourceMap = { [ChatSourceEnum.test]: { - name: i18nT('common:core.chat.logs.test') + name: i18nT('common:core.chat.logs.test'), + color: '#5E8FFF' }, [ChatSourceEnum.online]: { - name: i18nT('common:core.chat.logs.online') + name: i18nT('common:core.chat.logs.online'), + color: '#47B2FF' }, [ChatSourceEnum.share]: { - name: i18nT('common:core.chat.logs.share') + name: i18nT('common:core.chat.logs.share'), + color: '#9E8DFB' }, [ChatSourceEnum.api]: { - name: i18nT('common:core.chat.logs.api') + name: i18nT('common:core.chat.logs.api'), + color: '#D389F6' }, [ChatSourceEnum.cronJob]: { - name: i18nT('chat:source_cronJob') + name: i18nT('chat:source_cronJob'), + color: '#FF81AE' }, [ChatSourceEnum.team]: { - name: i18nT('common:core.chat.logs.team') + name: i18nT('common:core.chat.logs.team'), + color: '#42CFC6' }, [ChatSourceEnum.feishu]: { - name: i18nT('common:core.chat.logs.feishu') + name: i18nT('common:core.chat.logs.feishu'), + color: '#39CC83' }, [ChatSourceEnum.official_account]: { - name: i18nT('common:core.chat.logs.official_account') + name: i18nT('common:core.chat.logs.official_account'), + color: '#FDB022' }, [ChatSourceEnum.wecom]: { - name: i18nT('common:core.chat.logs.wecom') + name: i18nT('common:core.chat.logs.wecom'), + color: '#FD853A' }, [ChatSourceEnum.mcp]: { - name: i18nT('common:core.chat.logs.mcp') + name: i18nT('common:core.chat.logs.mcp'), + color: '#F97066' } }; diff --git a/packages/global/core/chat/setting/type.d.ts b/packages/global/core/chat/setting/type.d.ts new file mode 100644 index 000000000..24f7d05ea --- /dev/null +++ b/packages/global/core/chat/setting/type.d.ts @@ -0,0 +1,28 @@ +export type ChatSettingSchema = { + _id: string; + appId: string; + teamId: string; + slogan: string; + dialogTips: string; + homeTabTitle: string; + wideLogoUrl?: string; + squareLogoUrl?: string; + selectedTools: { + pluginId: string; + name: string; + avatar: string; + inputs?: Record<`${NodeInputKeyEnum}` | string, any>; + }[]; +}; + +export type ChatSettingUpdateParams = { + slogan?: string; + dialogTips?: string; + homeTabTitle?: string; + wideLogoUrl?: string; + squareLogoUrl?: string; + selectedTools: { + pluginId: string; + inputs?: Record<`${NodeInputKeyEnum}` | string, any>; + }[]; +}; diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index 817e08cd9..8d2c7b0dd 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -8,7 +8,7 @@ import type { ChatStatusEnum } from './constants'; import type { FlowNodeTypeEnum } from '../workflow/node/constant'; -import type { NodeOutputKeyEnum } from '../workflow/constants'; +import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '../workflow/constants'; import type { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; import type { AppSchema, VariableItemType } from '../app/type'; import { AppChatConfigType } from '../app/type'; @@ -18,6 +18,7 @@ import type { DispatchNodeResponseType } from '../workflow/runtime/type.d'; import type { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type'; import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; import type { FlowNodeInputItemType } from '../workflow/type/io'; +import type { FlowNodeTemplateType } from '../workflow/type/node.d'; export type ChatSchema = { _id: string; @@ -130,6 +131,7 @@ export type ResponseTagItemType = { totalQuoteList?: SearchDataResponseItemType[]; llmModuleAccount?: number; historyPreviewLength?: number; + toolCiteLinks?: ToolCiteLinksType[]; }; export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & { @@ -149,7 +151,6 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt errorMsg?: string; } & ChatBoxInputType & ResponseTagItemType; - /* --------- team chat --------- */ export type ChatAppListSchema = { apps: AppType[]; @@ -196,6 +197,10 @@ export type ToolModuleResponseItemType = { functionName: string; }; +export type ToolCiteLinksType = { + name: string; + url: string; +}; /* dispatch run time */ export type RuntimeUserPromptType = { files: UserChatItemValueItemType['file'][]; diff --git a/packages/global/core/dataset/training/utils.ts b/packages/global/core/dataset/training/utils.ts index ac9715eb3..670ee0dec 100644 --- a/packages/global/core/dataset/training/utils.ts +++ b/packages/global/core/dataset/training/utils.ts @@ -26,7 +26,7 @@ export const getLLMDefaultChunkSize = (model?: LLMModelItemType) => { export const getLLMMaxChunkSize = (model?: LLMModelItemType) => { if (!model) return 8000; - return Math.max(model.maxContext - model.maxResponse, 2000); + return Math.max(model.maxContext, 4000); }; // Index size diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index a9db069be..ed9b4c3da 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -29,6 +29,7 @@ import type { WorkflowInteractiveResponseType } from '../template/system/interactive/type'; import type { SearchDataResponseItemType } from '../../dataset/type'; +import type { localeType } from '../../../common/i18n/type'; export type ExternalProviderType = { openaiAccount?: OpenaiAccountType; externalWorkflowVariables?: Record; @@ -37,6 +38,7 @@ export type ExternalProviderType = { /* workflow props */ export type ChatDispatchProps = { res?: NextApiResponse; + lang?: localeType; requestOrigin?: string; mode: 'test' | 'chat' | 'debug'; timezone: string; @@ -49,6 +51,10 @@ export type ChatDispatchProps = { isChildApp?: boolean; }; runningUserInfo: { + username: string; + teamName: string; + memberName: string; + contact: string; teamId: string; tmbId: string; }; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 1da7ad002..ee4d1ba1c 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -77,6 +77,7 @@ export type FlowNodeCommonType = { // Not store, just computed currentCost?: number; + systemKeyCost?: number; hasTokenFee?: boolean; hasSystemSecret?: boolean; }; @@ -135,6 +136,7 @@ export type NodeTemplateListItemType = { author?: string; unique?: boolean; // 唯一的 currentCost?: number; // 当前积分消耗 + systemKeyCost?: number; // 系统密钥费用,统一为数字 hasTokenFee?: boolean; // 是否配置积分 instructions?: string; // 使用说明 courseUrl?: string; // 教程链接 diff --git a/packages/global/support/permission/app/constant.ts b/packages/global/support/permission/app/constant.ts index 01f42ece5..83fe63148 100644 --- a/packages/global/support/permission/app/constant.ts +++ b/packages/global/support/permission/app/constant.ts @@ -22,6 +22,7 @@ export const AppPerList: PermissionListType = { export const AppRoleList: RoleListType = { [CommonPerKeyEnum.read]: { ...CommonRoleList[CommonPerKeyEnum.read], + name: i18nT('app:permission.name.read'), description: i18nT('app:permission.des.read') }, [CommonPerKeyEnum.write]: { @@ -36,18 +37,24 @@ export const AppRoleList: RoleListType = { value: 0b1000, checkBoxType: 'multiple', name: i18nT('app:permission.name.readChatLog'), - description: i18nT('app:permission.des.readChatLog') + description: '' } }; export const AppRolePerMap: RolePerMapType = new Map([ ...CommonRolePerMap, [ - AppRoleList[AppPermissionKeyEnum.ReadChatLog].value, + CommonRoleList[CommonPerKeyEnum.manage].value, sumPer( CommonPerList[CommonPerKeyEnum.read], + CommonPerList[CommonPerKeyEnum.write], + CommonPerList[CommonPerKeyEnum.manage], AppPerList[AppPermissionKeyEnum.ReadChatLog] - ) as PermissionValueType + )! + ], + [ + AppRoleList[AppPermissionKeyEnum.ReadChatLog].value, + sumPer(CommonPerList[CommonPerKeyEnum.read], AppPerList[AppPermissionKeyEnum.ReadChatLog])! ] ]); diff --git a/packages/service/common/api/plusRequest.ts b/packages/service/common/api/plusRequest.ts index 33cc52035..c2de458e5 100644 --- a/packages/service/common/api/plusRequest.ts +++ b/packages/service/common/api/plusRequest.ts @@ -5,6 +5,7 @@ import axios, { type AxiosRequestConfig } from 'axios'; import { FastGPTProUrl } from '../system/constants'; +import { UserError } from '@fastgpt/global/common/error/utils'; interface ConfigType { headers?: { [key: string]: string }; @@ -78,7 +79,7 @@ instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err) export function request(url: string, data: any, config: ConfigType, method: Method): any { if (!FastGPTProUrl) { console.log('未部署商业版接口', url); - return Promise.reject('The The request was denied...'); + return Promise.reject(new UserError('The request was denied...')); } /* 去空 */ diff --git a/packages/service/common/file/gridfs/controller.ts b/packages/service/common/file/gridfs/controller.ts index b69d9fcde..afa99571e 100644 --- a/packages/service/common/file/gridfs/controller.ts +++ b/packages/service/common/file/gridfs/controller.ts @@ -151,10 +151,6 @@ export async function getFileById({ _id: new Types.ObjectId(fileId) }); - // if (!file) { - // return Promise.reject('File not found'); - // } - return file || undefined; } diff --git a/packages/service/common/file/image/controller.ts b/packages/service/common/file/image/controller.ts index b3b69e670..a271905d4 100644 --- a/packages/service/common/file/image/controller.ts +++ b/packages/service/common/file/image/controller.ts @@ -11,6 +11,7 @@ import { UserError } from '@fastgpt/global/common/error/utils'; export const maxImgSize = 1024 * 1024 * 12; const base64MimeRegex = /data:image\/([^\)]+);base64/; + export async function uploadMongoImg({ base64Img, teamId, @@ -22,13 +23,13 @@ export async function uploadMongoImg({ forever?: Boolean; }) { if (base64Img.length > maxImgSize) { - return Promise.reject('Image too large'); + return Promise.reject(new UserError('Image too large')); } const [base64Mime, base64Data] = base64Img.split(','); // Check if mime type is valid if (!base64MimeRegex.test(base64Mime)) { - return Promise.reject('Invalid image base64'); + return Promise.reject(new UserError('Invalid image base64')); } const mime = `image/${base64Mime.match(base64MimeRegex)?.[1] ?? 'image/jpeg'}`; @@ -39,7 +40,7 @@ export async function uploadMongoImg({ } if (!extension || !imageFileType.includes(`.${extension}`)) { - return Promise.reject(`Invalid image file type: ${mime}`); + return Promise.reject(new UserError(`Invalid image file type: ${mime}`)); } const { _id } = await retryFn(() => diff --git a/packages/service/common/file/multer.ts b/packages/service/common/file/multer.ts index 235a61df9..237407656 100644 --- a/packages/service/common/file/multer.ts +++ b/packages/service/common/file/multer.ts @@ -4,6 +4,7 @@ import path from 'path'; import type { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { UserError } from '@fastgpt/global/common/error/utils'; export type FileType = { fieldname: string; @@ -61,7 +62,7 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { // check bucket name const bucketName = (req.body?.bucketName || originBucketName) as `${BucketNameEnum}`; if (bucketName && !bucketNameMap[bucketName]) { - return reject('BucketName is invalid'); + return reject(new UserError('BucketName is invalid')); } // @ts-ignore diff --git a/packages/service/common/vectorDB/controller.d.ts b/packages/service/common/vectorDB/controller.d.ts index 1ae24b202..5f0328ce2 100644 --- a/packages/service/common/vectorDB/controller.d.ts +++ b/packages/service/common/vectorDB/controller.d.ts @@ -17,8 +17,7 @@ export type InsertVectorProps = { collectionId: string; }; export type InsertVectorControllerProps = InsertVectorProps & { - vector: number[]; - retry?: number; + vectors: number[][]; }; export type EmbeddingRecallProps = { diff --git a/packages/service/common/vectorDB/controller.ts b/packages/service/common/vectorDB/controller.ts index 6c25ea3cc..a2658b97e 100644 --- a/packages/service/common/vectorDB/controller.ts +++ b/packages/service/common/vectorDB/controller.ts @@ -2,7 +2,12 @@ import { PgVectorCtrl } from './pg'; import { ObVectorCtrl } from './oceanbase'; import { getVectorsByText } from '../../core/ai/embedding'; -import { type DelDatasetVectorCtrlProps, type InsertVectorProps } from './controller.d'; +import type { + EmbeddingRecallCtrlProps} from './controller.d'; +import { + type DelDatasetVectorCtrlProps, + type InsertVectorProps +} from './controller.d'; import { type EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d'; import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS } from './constants'; import { MilvusCtrl } from './milvus'; @@ -35,7 +40,8 @@ const onIncrCache = (teamId: string) => incrValueToCache(getChcheKey(teamId), 1) const Vector = getVectorObj(); export const initVectorStore = Vector.init; -export const recallFromVectorStore = Vector.embRecall; +export const recallFromVectorStore = (props: EmbeddingRecallCtrlProps) => + retryFn(() => Vector.embRecall(props)); export const getVectorDataByTime = Vector.getVectorDataByTime; export const getVectorCountByTeamId = async (teamId: string) => { @@ -58,34 +64,34 @@ export const getVectorCountByCollectionId = Vector.getVectorCountByCollectionId; export const insertDatasetDataVector = async ({ model, - query, + inputs, ...props }: InsertVectorProps & { - query: string; + inputs: string[]; model: EmbeddingModelItemType; }) => { - return retryFn(async () => { - const { vectors, tokens } = await getVectorsByText({ - model, - input: query, - type: 'db' - }); - const { insertId } = await Vector.insert({ - ...props, - vector: vectors[0] - }); - - onIncrCache(props.teamId); - - return { - tokens, - insertId - }; + const { vectors, tokens } = await getVectorsByText({ + model, + input: inputs, + type: 'db' }); + const { insertIds } = await retryFn(() => + Vector.insert({ + ...props, + vectors + }) + ); + + onIncrCache(props.teamId); + + return { + tokens, + insertIds + }; }; export const deleteDatasetDataVector = async (props: DelDatasetVectorCtrlProps) => { - const result = await Vector.delete(props); + const result = await retryFn(() => Vector.delete(props)); onDelCache(props.teamId); return result; }; diff --git a/packages/service/common/vectorDB/milvus/index.ts b/packages/service/common/vectorDB/milvus/index.ts index 4656134d4..a6db2b173 100644 --- a/packages/service/common/vectorDB/milvus/index.ts +++ b/packages/service/common/vectorDB/milvus/index.ts @@ -11,7 +11,7 @@ import type { EmbeddingRecallResponse, InsertVectorControllerProps } from '../controller.d'; -import { delay } from '@fastgpt/global/common/system/utils'; +import { delay, retryFn } from '@fastgpt/global/common/system/utils'; import { addLog } from '../../system/log'; import { customNanoid } from '@fastgpt/global/common/string/tools'; @@ -27,6 +27,7 @@ export class MilvusCtrl { address: MILVUS_ADDRESS, token: MILVUS_TOKEN }); + await global.milvusClient.connectPromise; addLog.info(`Milvus connected`); @@ -124,9 +125,9 @@ export class MilvusCtrl { } }; - insert = async (props: InsertVectorControllerProps): Promise<{ insertId: string }> => { + insert = async (props: InsertVectorControllerProps): Promise<{ insertIds: string[] }> => { const client = await this.getClient(); - const { teamId, datasetId, collectionId, vector, retry = 3 } = props; + const { teamId, datasetId, collectionId, vectors } = props; const generateId = () => { // in js, the max safe integer is 2^53 - 1: 9007199254740991 @@ -136,45 +137,32 @@ export class MilvusCtrl { const restDigits = customNanoid('1234567890', 15); return Number(`${firstDigit}${restDigits}`); }; - const id = generateId(); - try { - const result = await client.insert({ - collection_name: DatasetVectorTableName, - data: [ - { - id, - vector, - teamId: String(teamId), - datasetId: String(datasetId), - collectionId: String(collectionId), - createTime: Date.now() - } - ] - }); - const insertId = (() => { - if ('int_id' in result.IDs) { - return `${result.IDs.int_id.data?.[0]}`; - } - return `${result.IDs.str_id.data?.[0]}`; - })(); + const result = await client.insert({ + collection_name: DatasetVectorTableName, + data: vectors.map((vector) => ({ + id: generateId(), + vector, + teamId: String(teamId), + datasetId: String(datasetId), + collectionId: String(collectionId), + createTime: Date.now() + })) + }); - return { - insertId: insertId - }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); + const insertIds = (() => { + if ('int_id' in result.IDs) { + return result.IDs.int_id.data.map((id) => String(id)); } - await delay(500); - return this.insert({ - ...props, - retry: retry - 1 - }); - } + return result.IDs.str_id.data.map((id) => String(id)); + })(); + + return { + insertIds + }; }; delete = async (props: DelDatasetVectorCtrlProps): Promise => { - const { teamId, retry = 2 } = props; + const { teamId } = props; const client = await this.getClient(); const teamIdWhere = `(teamId=="${String(teamId)}")`; @@ -206,33 +194,15 @@ export class MilvusCtrl { const concatWhere = `${teamIdWhere} and ${where}`; - try { - await client.delete({ - collection_name: DatasetVectorTableName, - filter: concatWhere - }); - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - await delay(500); - return this.delete({ - ...props, - retry: retry - 1 - }); - } + await client.delete({ + collection_name: DatasetVectorTableName, + filter: concatWhere + }); }; embRecall = async (props: EmbeddingRecallCtrlProps): Promise => { const client = await this.getClient(); - const { - teamId, - datasetIds, - vector, - limit, - forbidCollectionIdList, - filterCollectionIdList, - retry = 2 - } = props; + const { teamId, datasetIds, vector, limit, forbidCollectionIdList, filterCollectionIdList } = + props; // Forbid collection const formatForbidCollectionIdList = (() => { @@ -262,37 +232,29 @@ export class MilvusCtrl { return { results: [] }; } - try { - const { results } = await client.search({ + const { results } = await retryFn(() => + client.search({ collection_name: DatasetVectorTableName, data: vector, limit, filter: `(teamId == "${teamId}") and (datasetId in [${datasetIds.map((id) => `"${id}"`).join(',')}]) ${collectionIdQuery} ${forbidColQuery}`, output_fields: ['collectionId'] - }); + }) + ); - const rows = results as { - score: number; - id: string; - collectionId: string; - }[]; + const rows = results as { + score: number; + id: string; + collectionId: string; + }[]; - return { - results: rows.map((item) => ({ - id: String(item.id), - collectionId: item.collectionId, - score: item.score - })) - }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - return this.embRecall({ - ...props, - retry: retry - 1 - }); - } + return { + results: rows.map((item) => ({ + id: String(item.id), + collectionId: item.collectionId, + score: item.score + })) + }; }; getVectorCountByTeamId = async (teamId: string) => { diff --git a/packages/service/common/vectorDB/oceanbase/index.ts b/packages/service/common/vectorDB/oceanbase/index.ts index 09110c69c..79592a413 100644 --- a/packages/service/common/vectorDB/oceanbase/index.ts +++ b/packages/service/common/vectorDB/oceanbase/index.ts @@ -1,6 +1,6 @@ /* oceanbase vector crud */ import { DatasetVectorTableName } from '../constants'; -import { delay } from '@fastgpt/global/common/system/utils'; +import { delay, retryFn } from '@fastgpt/global/common/system/utils'; import { ObClient } from './controller'; import { type RowDataPacket } from 'mysql2/promise'; import { @@ -42,41 +42,30 @@ export class ObVectorCtrl { addLog.error('init oceanbase error', error); } }; - insert = async (props: InsertVectorControllerProps): Promise<{ insertId: string }> => { - const { teamId, datasetId, collectionId, vector, retry = 3 } = props; + insert = async (props: InsertVectorControllerProps): Promise<{ insertIds: string[] }> => { + const { teamId, datasetId, collectionId, vectors } = props; - try { - const { rowCount, rows } = await ObClient.insert(DatasetVectorTableName, { - values: [ - [ - { key: 'vector', value: `[${vector}]` }, - { key: 'team_id', value: String(teamId) }, - { key: 'dataset_id', value: String(datasetId) }, - { key: 'collection_id', value: String(collectionId) } - ] - ] - }); + const values = vectors.map((vector) => [ + { key: 'vector', value: `[${vector}]` }, + { key: 'team_id', value: String(teamId) }, + { key: 'dataset_id', value: String(datasetId) }, + { key: 'collection_id', value: String(collectionId) } + ]); - if (rowCount === 0) { - return Promise.reject('insertDatasetData: no insert'); - } + const { rowCount, rows } = await ObClient.insert(DatasetVectorTableName, { + values + }); - return { - insertId: rows[0].id - }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - await delay(500); - return this.insert({ - ...props, - retry: retry - 1 - }); + if (rowCount === 0) { + return Promise.reject('insertDatasetData: no insert'); } + + return { + insertIds: rows.map((row) => row.id) + }; }; delete = async (props: DelDatasetVectorCtrlProps): Promise => { - const { teamId, retry = 2 } = props; + const { teamId } = props; const teamIdWhere = `team_id='${String(teamId)}' AND`; @@ -106,31 +95,13 @@ export class ObVectorCtrl { if (!where) return; - try { - await ObClient.delete(DatasetVectorTableName, { - where: [where] - }); - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - await delay(500); - return this.delete({ - ...props, - retry: retry - 1 - }); - } + await ObClient.delete(DatasetVectorTableName, { + where: [where] + }); }; embRecall = async (props: EmbeddingRecallCtrlProps): Promise => { - const { - teamId, - datasetIds, - vector, - limit, - forbidCollectionIdList, - filterCollectionIdList, - retry = 2 - } = props; + const { teamId, datasetIds, vector, limit, forbidCollectionIdList, filterCollectionIdList } = + props; // Get forbid collection const formatForbidCollectionIdList = (() => { @@ -161,15 +132,14 @@ export class ObVectorCtrl { return { results: [] }; } - try { - const rows = await ObClient.query< - ({ - id: string; - collection_id: string; - score: number; - } & RowDataPacket)[][] - >( - `BEGIN; + const rows = await ObClient.query< + ({ + id: string; + collection_id: string; + score: number; + } & RowDataPacket)[][] + >( + `BEGIN; SET ob_hnsw_ef_search = ${global.systemEnv?.hnswEfSearch || 100}; SELECT id, collection_id, inner_product(vector, [${vector}]) AS score FROM ${DatasetVectorTableName} @@ -179,24 +149,15 @@ export class ObVectorCtrl { ${forbidCollectionSql} ORDER BY score desc APPROXIMATE LIMIT ${limit}; COMMIT;` - ).then(([rows]) => rows[2]); + ).then(([rows]) => rows[2]); - return { - results: rows.map((item) => ({ - id: String(item.id), - collectionId: item.collection_id, - score: item.score - })) - }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - return this.embRecall({ - ...props, - retry: retry - 1 - }); - } + return { + results: rows.map((item) => ({ + id: String(item.id), + collectionId: item.collection_id, + score: item.score + })) + }; }; getVectorDataByTime = async (start: Date, end: Date) => { const rows = await ObClient.query< diff --git a/packages/service/common/vectorDB/pg/index.ts b/packages/service/common/vectorDB/pg/index.ts index fbf268868..abe51f4ca 100644 --- a/packages/service/common/vectorDB/pg/index.ts +++ b/packages/service/common/vectorDB/pg/index.ts @@ -1,6 +1,6 @@ /* pg vector crud */ import { DatasetVectorTableName } from '../constants'; -import { delay } from '@fastgpt/global/common/system/utils'; +import { delay, retryFn } from '@fastgpt/global/common/system/utils'; import { PgClient, connectPg } from './controller'; import { type PgSearchRawType } from '@fastgpt/global/core/dataset/api'; import type { @@ -65,41 +65,30 @@ export class PgVectorCtrl { addLog.error('init pg error', error); } }; - insert = async (props: InsertVectorControllerProps): Promise<{ insertId: string }> => { - const { teamId, datasetId, collectionId, vector, retry = 3 } = props; + insert = async (props: InsertVectorControllerProps): Promise<{ insertIds: string[] }> => { + const { teamId, datasetId, collectionId, vectors } = props; - try { - const { rowCount, rows } = await PgClient.insert(DatasetVectorTableName, { - values: [ - [ - { key: 'vector', value: `[${vector}]` }, - { key: 'team_id', value: String(teamId) }, - { key: 'dataset_id', value: String(datasetId) }, - { key: 'collection_id', value: String(collectionId) } - ] - ] - }); + const values = vectors.map((vector) => [ + { key: 'vector', value: `[${vector}]` }, + { key: 'team_id', value: String(teamId) }, + { key: 'dataset_id', value: String(datasetId) }, + { key: 'collection_id', value: String(collectionId) } + ]); - if (rowCount === 0) { - return Promise.reject('insertDatasetData: no insert'); - } + const { rowCount, rows } = await PgClient.insert(DatasetVectorTableName, { + values + }); - return { - insertId: rows[0].id - }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - await delay(500); - return this.insert({ - ...props, - retry: retry - 1 - }); + if (rowCount === 0) { + return Promise.reject('insertDatasetData: no insert'); } + + return { + insertIds: rows.map((row) => row.id) + }; }; delete = async (props: DelDatasetVectorCtrlProps): Promise => { - const { teamId, retry = 2 } = props; + const { teamId } = props; const teamIdWhere = `team_id='${String(teamId)}' AND`; @@ -129,31 +118,13 @@ export class PgVectorCtrl { if (!where) return; - try { - await PgClient.delete(DatasetVectorTableName, { - where: [where] - }); - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - await delay(500); - return this.delete({ - ...props, - retry: retry - 1 - }); - } + await PgClient.delete(DatasetVectorTableName, { + where: [where] + }); }; embRecall = async (props: EmbeddingRecallCtrlProps): Promise => { - const { - teamId, - datasetIds, - vector, - limit, - forbidCollectionIdList, - filterCollectionIdList, - retry = 2 - } = props; + const { teamId, datasetIds, vector, limit, forbidCollectionIdList, filterCollectionIdList } = + props; // Get forbid collection const formatForbidCollectionIdList = (() => { @@ -184,9 +155,8 @@ export class PgVectorCtrl { return { results: [] }; } - try { - const results: any = await PgClient.query( - `BEGIN; + const results: any = await PgClient.query( + `BEGIN; SET LOCAL hnsw.ef_search = ${global.systemEnv?.hnswEfSearch || 100}; SET LOCAL hnsw.max_scan_tuples = ${global.systemEnv?.hnswMaxScanTuples || 100000}; SET LOCAL hnsw.iterative_scan = relaxed_order; @@ -199,31 +169,22 @@ export class PgVectorCtrl { order by score limit ${limit} ) SELECT id, collection_id, score FROM relaxed_results ORDER BY score; COMMIT;` - ); - const rows = results?.[results.length - 2]?.rows as PgSearchRawType[]; - - if (!Array.isArray(rows)) { - return { - results: [] - }; - } + ); + const rows = results?.[results.length - 2]?.rows as PgSearchRawType[]; + if (!Array.isArray(rows)) { return { - results: rows.map((item) => ({ - id: String(item.id), - collectionId: item.collection_id, - score: item.score * -1 - })) + results: [] }; - } catch (error) { - if (retry <= 0) { - return Promise.reject(error); - } - return this.embRecall({ - ...props, - retry: retry - 1 - }); } + + return { + results: rows.map((item) => ({ + id: String(item.id), + collectionId: item.collection_id, + score: item.score * -1 + })) + }; }; getVectorDataByTime = async (start: Date, end: Date) => { const { rows } = await PgClient.query<{ diff --git a/packages/service/core/ai/audio/transcriptions.ts b/packages/service/core/ai/audio/transcriptions.ts index 91f9ae70c..a74057224 100644 --- a/packages/service/core/ai/audio/transcriptions.ts +++ b/packages/service/core/ai/audio/transcriptions.ts @@ -3,6 +3,7 @@ import { getAxiosConfig } from '../config'; import axios from 'axios'; import FormData from 'form-data'; import { type STTModelType } from '@fastgpt/global/core/ai/model.d'; +import { UserError } from '@fastgpt/global/common/error/utils'; export const aiTranscriptions = async ({ model: modelData, @@ -14,7 +15,7 @@ export const aiTranscriptions = async ({ headers?: Record; }) => { if (!modelData) { - return Promise.reject('no model'); + return Promise.reject(new UserError('no model')); } const data = new FormData(); diff --git a/packages/service/core/ai/embedding/index.ts b/packages/service/core/ai/embedding/index.ts index 45599c173..ba31eaf29 100644 --- a/packages/service/core/ai/embedding/index.ts +++ b/packages/service/core/ai/embedding/index.ts @@ -6,7 +6,7 @@ import { addLog } from '../../../common/system/log'; type GetVectorProps = { model: EmbeddingModelItemType; - input: string; + input: string[] | string; type?: `${EmbeddingTypeEnm}`; headers?: Record; }; @@ -19,60 +19,85 @@ export async function getVectorsByText({ model, input, type, headers }: GetVecto message: 'input is empty' }); } + const ai = getAIApi(); + + const formatInput = Array.isArray(input) ? input : [input]; + + // 20 size every request + const chunkSize = 20; + const chunks = []; + for (let i = 0; i < formatInput.length; i += chunkSize) { + chunks.push(formatInput.slice(i, i + chunkSize)); + } try { - const ai = getAIApi(); + // Process chunks sequentially + let totalTokens = 0; + const allVectors: number[][] = []; - // input text to vector - const result = await ai.embeddings - .create( - { - ...model.defaultConfig, - ...(type === EmbeddingTypeEnm.db && model.dbConfig), - ...(type === EmbeddingTypeEnm.query && model.queryConfig), - model: model.model, - input: [input] - }, - model.requestUrl - ? { - path: model.requestUrl, - headers: { - ...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}), - ...headers + for (const chunk of chunks) { + // input text to vector + const result = await ai.embeddings + .create( + { + ...model.defaultConfig, + ...(type === EmbeddingTypeEnm.db && model.dbConfig), + ...(type === EmbeddingTypeEnm.query && model.queryConfig), + model: model.model, + input: chunk + }, + model.requestUrl + ? { + path: model.requestUrl, + headers: { + ...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}), + ...headers + } } - } - : { headers } - ) - .then(async (res) => { - if (!res.data) { - addLog.error('Embedding API is not responding', res); - return Promise.reject('Embedding API is not responding'); - } - if (!res?.data?.[0]?.embedding) { - console.log(res); - // @ts-ignore - return Promise.reject(res.data?.err?.message || 'Embedding API Error'); - } + : { headers } + ) + .then(async (res) => { + if (!res.data) { + addLog.error('Embedding API is not responding', res); + return Promise.reject('Embedding API is not responding'); + } + if (!res?.data?.[0]?.embedding) { + console.log(res); + // @ts-ignore + return Promise.reject(res.data?.err?.message || 'Embedding API Error'); + } - const [tokens, vectors] = await Promise.all([ - countPromptTokens(input), - Promise.all( - res.data - .map((item) => unityDimensional(item.embedding)) - .map((item) => { - if (model.normalization) return normalization(item); - return item; - }) - ) - ]); + const [tokens, vectors] = await Promise.all([ + (async () => { + if (res.usage) return res.usage.total_tokens; - return { - tokens, - vectors - }; - }); + const tokens = await Promise.all(chunk.map((item) => countPromptTokens(item))); + return tokens.reduce((sum, item) => sum + item, 0); + })(), + Promise.all( + res.data + .map((item) => unityDimensional(item.embedding)) + .map((item) => { + if (model.normalization) return normalization(item); + return item; + }) + ) + ]); - return result; + return { + tokens, + vectors + }; + }); + + totalTokens += result.tokens; + allVectors.push(...result.vectors); + } + + return { + tokens: totalTokens, + vectors: allVectors + }; } catch (error) { addLog.error(`Embedding Error`, error); diff --git a/packages/service/core/app/logs/chatLogsSchema.ts b/packages/service/core/app/logs/chatLogsSchema.ts new file mode 100644 index 000000000..d0bce2dcf --- /dev/null +++ b/packages/service/core/app/logs/chatLogsSchema.ts @@ -0,0 +1,77 @@ +import type { AppChatLogSchema } from '@fastgpt/global/core/app/logs/type'; +import { getMongoLogModel, Schema } from '../../../common/mongo'; +import { AppCollectionName } from '../schema'; + +export const ChatLogCollectionName = 'app_chat_logs'; + +const ChatLogSchema = new Schema({ + appId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + required: true + }, + teamId: { + type: Schema.Types.ObjectId, + required: true + }, + chatId: { + type: String, + required: true + }, + userId: { + type: String, + required: true + }, + source: { + type: String, + required: true + }, + sourceName: { + type: String + }, + createTime: { + type: Date, + required: true + }, + updateTime: { + type: Date, + required: true + }, + // 累计统计字段 + chatItemCount: { + type: Number, + default: 0 + }, + errorCount: { + type: Number, + default: 0 + }, + totalPoints: { + type: Number, + default: 0 + }, + goodFeedbackCount: { + type: Number, + default: 0 + }, + badFeedbackCount: { + type: Number, + default: 0 + }, + totalResponseTime: { + type: Number, + default: 0 + }, + isFirstChat: { + type: Boolean, + default: false + } +}); + +ChatLogSchema.index({ teamId: 1, appId: 1, source: 1, updateTime: -1 }); +ChatLogSchema.index({ userId: 1, appId: 1, source: 1, createTime: -1 }); + +export const MongoAppChatLog = getMongoLogModel( + ChatLogCollectionName, + ChatLogSchema +); diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index 8b236ed2d..eb509373a 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -46,6 +46,7 @@ import { getMCPParentId, getMCPToolRuntimeNode } from '@fastgpt/global/core/app/ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { getMCPChildren } from '../mcp'; import { cloneDeep } from 'lodash'; +import { UserError } from '@fastgpt/global/common/error/utils'; type ChildAppType = SystemPluginTemplateItemType & { teamId?: string; @@ -80,7 +81,7 @@ export const getSystemPluginByIdAndVersionId = async ( app }) : await getAppLatestVersion(plugin.associatedPluginId, app); - if (!version.versionId) return Promise.reject('App version not found'); + if (!version.versionId) return Promise.reject(new UserError('App version not found')); const isLatest = version.versionId ? await checkIsLatestVersion({ appId: plugin.associatedPluginId, @@ -119,7 +120,7 @@ export const getSystemPluginByIdAndVersionId = async ( const versionList = (plugin.versionList as SystemPluginTemplateItemType['versionList']) || []; if (versionList.length === 0) { - return Promise.reject('Can not find plugin version list'); + return Promise.reject(new UserError('Can not find plugin version list')); } const version = versionId @@ -304,11 +305,13 @@ export async function getChildAppPreviewNode({ ? { systemToolSet: { toolId: app.id, - toolList: children.map((item) => ({ - toolId: item.id, - name: parseI18nString(item.name, lang), - description: parseI18nString(item.intro, lang) - })) + toolList: children + .filter((item) => item.isActive !== false) + .map((item) => ({ + toolId: item.id, + name: parseI18nString(item.name, lang), + description: parseI18nString(item.intro, lang) + })) } } : { systemTool: { toolId: app.id } }) @@ -378,8 +381,10 @@ export async function getChildAppPreviewNode({ showTargetHandle: true, currentCost: app.currentCost, + systemKeyCost: app.systemKeyCost, hasTokenFee: app.hasTokenFee, hasSystemSecret: app.hasSystemSecret, + isFolder: app.isFolder, ...nodeIOConfig, outputs: nodeIOConfig.outputs.some((item) => item.type === FlowNodeOutputTypeEnum.error) @@ -432,6 +437,7 @@ export async function getChildAppRuntimeById({ originCost: 0, currentCost: 0, + systemKeyCost: 0, hasTokenFee: false, pluginOrder: 0 }; @@ -448,6 +454,7 @@ export async function getChildAppRuntimeById({ avatar: app.avatar || '', showStatus: true, currentCost: app.currentCost, + systemKeyCost: app.systemKeyCost, nodes: app.workflow.nodes, edges: app.workflow.edges, hasTokenFee: app.hasTokenFee @@ -474,6 +481,7 @@ const dbPluginFormat = (item: SystemPluginConfigSchemaType): SystemPluginTemplat currentCost: item.currentCost, hasTokenFee: item.hasTokenFee, pluginOrder: item.pluginOrder, + systemKeyCost: item.systemKeyCost, associatedPluginId, userGuide, workflow: { @@ -515,61 +523,63 @@ export const getSystemTools = async (): Promise const tools = await APIGetSystemToolList(); // 从数据库里加载插件配置进行替换 - const systemPluginsArray = await MongoSystemPlugin.find({}).lean(); - const systemPlugins = new Map(systemPluginsArray.map((plugin) => [plugin.pluginId, plugin])); + const systemToolsArray = await MongoSystemPlugin.find({}).lean(); + const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin])); - tools.forEach((tool) => { - // 如果有插件的配置信息,则需要进行替换 - const dbPluginConfig = systemPlugins.get(tool.id); + // tools.forEach((tool) => { + // // 如果有插件的配置信息,则需要进行替换 + // const dbPluginConfig = systemTools.get(tool.id); - if (dbPluginConfig) { - const children = tools.filter((item) => item.parentId === tool.id); - const list = [tool, ...children]; - list.forEach((item) => { - item.isActive = dbPluginConfig.isActive ?? item.isActive ?? true; - item.originCost = dbPluginConfig.originCost ?? 0; - item.currentCost = dbPluginConfig.currentCost ?? 0; - item.hasTokenFee = dbPluginConfig.hasTokenFee ?? false; - item.pluginOrder = dbPluginConfig.pluginOrder ?? 0; - }); - } - }); + // if (dbPluginConfig) { + // const children = tools.filter((item) => item.parentId === tool.id); + // const list = [tool, ...children]; + // list.forEach((item) => { + // item.isActive = dbPluginConfig.isActive ?? item.isActive ?? true; + // item.originCost = dbPluginConfig.originCost ?? 0; + // item.currentCost = dbPluginConfig.currentCost ?? 0; + // item.hasTokenFee = dbPluginConfig.hasTokenFee ?? false; + // item.pluginOrder = dbPluginConfig.pluginOrder ?? 0; + // }); + // } + // }); const formatTools = tools.map((item) => { - const dbPluginConfig = systemPlugins.get(item.id); + const dbPluginConfig = systemTools.get(item.id); + const isFolder = tools.some((tool) => tool.parentId === item.id); const versionList = (item.versionList as SystemPluginTemplateItemType['versionList']) || []; return { id: item.id, parentId: item.parentId, - isFolder: tools.some((tool) => tool.parentId === item.id), - + isFolder, name: item.name, avatar: item.avatar, intro: item.description, - author: item.author, courseUrl: item.courseUrl, weight: item.weight, - workflow: { nodes: [], edges: [] }, versionList, - templateType: item.templateType, showStatus: true, - - isActive: item.isActive, + isActive: dbPluginConfig?.isActive ?? item.isActive ?? true, inputList: item?.secretInputConfig, - hasSystemSecret: !!dbPluginConfig?.inputListVal + hasSystemSecret: !!dbPluginConfig?.inputListVal, + + originCost: dbPluginConfig?.originCost ?? 0, + currentCost: dbPluginConfig?.currentCost ?? 0, + systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0, + hasTokenFee: dbPluginConfig?.hasTokenFee ?? false, + pluginOrder: dbPluginConfig?.pluginOrder }; }); // TODO: Check the app exists - const dbPlugins = systemPluginsArray + const dbPlugins = systemToolsArray .filter((item) => item.customConfig?.associatedPluginId) .map((item) => dbPluginFormat(item)); diff --git a/packages/service/core/app/plugin/systemPluginSchema.ts b/packages/service/core/app/plugin/systemPluginSchema.ts index 76f4d91e3..c83bdc521 100644 --- a/packages/service/core/app/plugin/systemPluginSchema.ts +++ b/packages/service/core/app/plugin/systemPluginSchema.ts @@ -27,6 +27,10 @@ const SystemPluginSchema = new Schema({ pluginOrder: { type: Number }, + systemKeyCost: { + type: Number, + default: 0 + }, customConfig: Object, inputListVal: Object, diff --git a/packages/service/core/app/plugin/type.d.ts b/packages/service/core/app/plugin/type.d.ts index 8628b3d2e..31848103a 100644 --- a/packages/service/core/app/plugin/type.d.ts +++ b/packages/service/core/app/plugin/type.d.ts @@ -1,6 +1,7 @@ import { SystemPluginListItemType } from '@fastgpt/global/core/app/type'; import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants'; import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; +import type { InputConfigType } from '@fastgpt/global/core/workflow/type/io'; export type SystemPluginConfigSchemaType = { pluginId: string; @@ -10,6 +11,7 @@ export type SystemPluginConfigSchemaType = { hasTokenFee: boolean; isActive: boolean; pluginOrder?: number; + systemKeyCost?: number; customConfig?: { name: string; diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 37acfcbff..391f04f40 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -82,8 +82,10 @@ export async function rewriteAppWorkflowToDetail({ node.version = preview.version; node.currentCost = preview.currentCost; + node.systemKeyCost = preview.systemKeyCost; node.hasTokenFee = preview.hasTokenFee; node.hasSystemSecret = preview.hasSystemSecret; + node.isFolder = preview.isFolder; node.toolConfig = preview.toolConfig; diff --git a/packages/service/core/chat/chatSchema.ts b/packages/service/core/chat/chatSchema.ts index a0c30d56d..b7564d6fe 100644 --- a/packages/service/core/chat/chatSchema.ts +++ b/packages/service/core/chat/chatSchema.ts @@ -1,7 +1,7 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; const { Schema } = connectionMongo; import { type ChatSchema as ChatType } from '@fastgpt/global/core/chat/type.d'; -import { ChatSourceEnum, ChatSourceMap } from '@fastgpt/global/core/chat/constants'; +import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { TeamCollectionName, TeamMemberCollectionName diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index 52588ad8d..b6113869a 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -4,6 +4,7 @@ import { addLog } from '../../common/system/log'; import { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller'; import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { MongoChat } from './chatSchema'; +import { UserError } from '@fastgpt/global/common/error/utils'; export async function getChatItems({ appId, @@ -72,7 +73,8 @@ export const deleteChatFiles = async ({ chatIdList?: string[]; appId?: string; }) => { - if (!appId && !chatIdList) return Promise.reject('appId or chatIdList is required'); + if (!appId && !chatIdList) + return Promise.reject(new UserError('appId or chatIdList is required')); const appChatIdList = await (async () => { if (appId) { diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 59fe5e4fc..084217e79 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -14,6 +14,7 @@ import { pushChatLog } from './pushChatLog'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; +import { MongoAppChatLog } from '../app/logs/chatLogsSchema'; type Props = { chatId: string; @@ -163,6 +164,62 @@ export async function saveChat({ }); }); + try { + const userId = outLinkUid || tmbId; + const now = new Date(); + const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000); + + const aiResponse = processedContent.find((item) => item.obj === ChatRoleEnum.AI); + const errorCount = aiResponse?.responseData?.some((item) => item.errorText) ? 1 : 0; + const totalPoints = + aiResponse?.responseData?.reduce( + (sum: number, item: any) => sum + (item.totalPoints || 0), + 0 + ) || 0; + + const hasHistoryChat = await MongoAppChatLog.exists({ + appId, + userId, + createTime: { $lt: now } + }); + + await MongoAppChatLog.updateOne( + { + chatId, + appId, + updateTime: { $gte: fifteenMinutesAgo } + }, + { + $inc: { + chatItemCount: 1, + errorCount, + totalPoints, + totalResponseTime: durationSeconds + }, + $set: { + updateTime: now, + sourceName + }, + $setOnInsert: { + appId, + teamId, + chatId, + userId, + source, + createTime: now, + goodFeedbackCount: 0, + badFeedbackCount: 0, + isFirstChat: !hasHistoryChat + } + }, + { + upsert: true + } + ); + } catch (error) { + addLog.error('update chat log error', error); + } + if (isUpdateUseTime) { await MongoApp.findByIdAndUpdate(appId, { updateTime: new Date() diff --git a/packages/service/core/chat/setting/schema.ts b/packages/service/core/chat/setting/schema.ts new file mode 100644 index 000000000..af926a033 --- /dev/null +++ b/packages/service/core/chat/setting/schema.ts @@ -0,0 +1,37 @@ +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { type ChatSettingSchema as ChatSettingType } from '@fastgpt/global/core/chat/setting/type'; +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { AppCollectionName } from '../../app/schema'; + +const { Schema } = connectionMongo; + +export const ChatSettingCollectionName = 'chat_settings'; + +const ChatSettingSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + appId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + required: true + }, + slogan: String, + dialogTips: String, + selectedTools: { + type: Array, + default: [] + }, + homeTabTitle: String, + wideLogoUrl: String, + squareLogoUrl: String +}); + +ChatSettingSchema.index({ teamId: 1 }); + +export const MongoChatSetting = getMongoModel( + ChatSettingCollectionName, + ChatSettingSchema +); diff --git a/packages/service/core/dataset/controller.ts b/packages/service/core/dataset/controller.ts index 095a7cfde..dade64db9 100644 --- a/packages/service/core/dataset/controller.ts +++ b/packages/service/core/dataset/controller.ts @@ -14,6 +14,7 @@ import { MongoDatasetCollectionTags } from './tag/schema'; import { removeDatasetSyncJobScheduler } from './datasetSync'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { removeImageByPath } from '../../common/file/image/controller'; +import { UserError } from '@fastgpt/global/common/error/utils'; /* ============= dataset ========== */ /* find all datasetId by top datasetId */ @@ -50,7 +51,7 @@ export async function findDatasetAndAllChildren({ ]); if (!dataset) { - return Promise.reject('Dataset not found'); + return Promise.reject(new UserError('Dataset not found')); } return [dataset, ...childDatasets]; @@ -79,7 +80,7 @@ export async function delDatasetRelevantData({ const teamId = datasets[0].teamId; if (!teamId) { - return Promise.reject('TeamId is required'); + return Promise.reject(new UserError('TeamId is required')); } const datasetIds = datasets.map((item) => item._id); diff --git a/packages/service/core/dataset/read.ts b/packages/service/core/dataset/read.ts index e6f417cc8..694aefcec 100644 --- a/packages/service/core/dataset/read.ts +++ b/packages/service/core/dataset/read.ts @@ -16,6 +16,7 @@ import { text2Chunks } from '../../worker/function'; import { addLog } from '../../common/system/log'; import { retryFn } from '@fastgpt/global/common/system/utils'; import { getFileMaxSize } from '../../common/file/utils'; +import { UserError } from '@fastgpt/global/common/error/utils'; export const readFileRawTextByUrl = async ({ teamId, @@ -200,7 +201,7 @@ export const readDatasetSourceRawText = async ({ rawText: content }; } else if (type === DatasetSourceReadTypeEnum.externalFile) { - if (!externalFileId) return Promise.reject('FileId not found'); + if (!externalFileId) return Promise.reject(new UserError('FileId not found')); const rawText = await readFileRawTextByUrl({ teamId, tmbId, diff --git a/packages/service/core/dataset/search/controller.ts b/packages/service/core/dataset/search/controller.ts index 87ae0747c..080e6fe2e 100644 --- a/packages/service/core/dataset/search/controller.ts +++ b/packages/service/core/dataset/search/controller.ts @@ -7,6 +7,10 @@ import { recallFromVectorStore } from '../../../common/vectorDB/controller'; import { getVectorsByText } from '../../ai/embedding'; import { getEmbeddingModel, getDefaultRerankModel, getLLMModel } from '../../ai/model'; import { MongoDatasetData } from '../data/schema'; +import type { + DatasetCollectionSchemaType, + DatasetDataSchemaType +} from '@fastgpt/global/core/dataset/type'; import { type DatasetDataTextSchemaType, type SearchDataResponseItemType @@ -27,7 +31,6 @@ import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { datasetSearchQueryExtension } from './utils'; import type { RerankModelItemType } from '@fastgpt/global/core/ai/model.d'; -import { addLog } from '../../../common/system/log'; import { formatDatasetDataValue } from '../data/controller'; export type SearchDatasetDataProps = { @@ -435,214 +438,114 @@ export async function searchDatasetData( } catch (error) {} }; const embeddingRecall = async ({ - query, + queries, limit, forbidCollectionIdList, filterCollectionIdList }: { - query: string; + queries: string[]; limit: number; forbidCollectionIdList: string[]; filterCollectionIdList?: string[]; - }) => { + }): Promise<{ + embeddingRecallResults: SearchDataResponseItemType[][]; + tokens: number; + }> => { + if (limit === 0) { + return { + embeddingRecallResults: [], + tokens: 0 + }; + } + const { vectors, tokens } = await getVectorsByText({ model: getEmbeddingModel(model), - input: query, + input: queries, type: 'query' }); - const { results } = await recallFromVectorStore({ - teamId, - datasetIds, - vector: vectors[0], - limit, - forbidCollectionIdList, - filterCollectionIdList - }); + const recallResults = await Promise.all( + vectors.map(async (vector) => { + return await recallFromVectorStore({ + teamId, + datasetIds, + vector, + limit, + forbidCollectionIdList, + filterCollectionIdList + }); + }) + ); // Get data and collections - const collectionIdList = Array.from(new Set(results.map((item) => item.collectionId))); - const [dataList, collections] = await Promise.all([ + const collectionIdList = Array.from( + new Set(recallResults.map((item) => item.results.map((item) => item.collectionId)).flat()) + ); + const indexDataIds = Array.from( + new Set(recallResults.map((item) => item.results.map((item) => item.id?.trim())).flat()) + ); + + const [dataMaps, collectionMaps] = await Promise.all([ MongoDatasetData.find( { teamId, datasetId: { $in: datasetIds }, collectionId: { $in: collectionIdList }, - 'indexes.dataId': { $in: results.map((item) => item.id?.trim()) } + 'indexes.dataId': { $in: indexDataIds } }, datasetDataSelectField, { ...readFromSecondary } - ).lean(), + ) + .lean() + .then((res) => { + const map = new Map(); + + res.forEach((item) => { + item.indexes.forEach((index) => { + map.set(String(index.dataId), item); + }); + }); + + return map; + }), MongoDatasetCollection.find( { _id: { $in: collectionIdList } }, datsaetCollectionSelectField, { ...readFromSecondary } - ).lean() + ) + .lean() + .then((res) => { + const map = new Map(); + + res.forEach((item) => { + map.set(String(item._id), item); + }); + + return map; + }) ]); - const set = new Set(); - const formatResult = results - .map((item, index) => { - const collection = collections.find((col) => String(col._id) === String(item.collectionId)); - if (!collection) { - console.log('Collection is not found', item); - return; - } - const data = dataList.find((data) => - data.indexes.some((index) => index.dataId === item.id) - ); - if (!data) { - console.log('Data is not found', item); - return; - } - - const result: SearchDataResponseItemType = { - id: String(data._id), - updateTime: data.updateTime, - ...formatDatasetDataValue({ - teamId, - datasetId: data.datasetId, - q: data.q, - a: data.a, - imageId: data.imageId, - imageDescMap: data.imageDescMap - }), - chunkIndex: data.chunkIndex, - datasetId: String(data.datasetId), - collectionId: String(data.collectionId), - ...getCollectionSourceData(collection), - score: [{ type: SearchScoreTypeEnum.embedding, value: item?.score || 0, index }] - }; - - return result; - }) - .filter((item) => { - if (!item) return false; - if (set.has(item.id)) return false; - set.add(item.id); - return true; - }) - .map((item, index) => { - if (!item) return; - return { - ...item, - score: item.score.map((item) => ({ ...item, index })) - }; - }) as SearchDataResponseItemType[]; - - return { - embeddingRecallResults: formatResult, - tokens - }; - }; - const fullTextRecall = async ({ - query, - limit, - filterCollectionIdList, - forbidCollectionIdList - }: { - query: string; - limit: number; - filterCollectionIdList?: string[]; - forbidCollectionIdList: string[]; - }): Promise<{ - fullTextRecallResults: SearchDataResponseItemType[]; - tokenLen: number; - }> => { - if (limit === 0) { - return { - fullTextRecallResults: [], - tokenLen: 0 - }; - } - - try { - const searchResults = (await MongoDatasetDataText.aggregate( - [ - { - $match: { - teamId: new Types.ObjectId(teamId), - $text: { $search: await jiebaSplit({ text: query }) }, - datasetId: { $in: datasetIds.map((id) => new Types.ObjectId(id)) }, - ...(filterCollectionIdList - ? { - collectionId: { - $in: filterCollectionIdList - .filter((id) => !forbidCollectionIdList.includes(id)) - .map((id) => new Types.ObjectId(id)) - } - } - : forbidCollectionIdList?.length - ? { - collectionId: { - $nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id)) - } - } - : {}) - } - }, - { - $sort: { - score: { $meta: 'textScore' } - } - }, - { - $limit: limit - }, - { - $project: { - _id: 1, - collectionId: 1, - dataId: 1, - score: { $meta: 'textScore' } - } - } - ], - { - ...readFromSecondary - } - )) as (DatasetDataTextSchemaType & { score: number })[]; - - // Get data and collections - const [dataList, collections] = await Promise.all([ - MongoDatasetData.find( - { - _id: { $in: searchResults.map((item) => item.dataId) } - }, - datasetDataSelectField, - { ...readFromSecondary } - ).lean(), - MongoDatasetCollection.find( - { - _id: { $in: searchResults.map((item) => item.collectionId) } - }, - datsaetCollectionSelectField, - { ...readFromSecondary } - ).lean() - ]); - - return { - fullTextRecallResults: searchResults + const embeddingRecallResults = recallResults.map((item) => { + const set = new Set(); + return ( + item.results .map((item, index) => { - const collection = collections.find( - (col) => String(col._id) === String(item.collectionId) - ); + const collection = collectionMaps.get(String(item.collectionId)); if (!collection) { console.log('Collection is not found', item); return; } - const data = dataList.find((data) => String(data._id) === String(item.dataId)); + + const data = dataMaps.get(String(item.id)); if (!data) { console.log('Data is not found', item); return; } - return { + const result: SearchDataResponseItemType = { id: String(data._id), - datasetId: String(data.datasetId), - collectionId: String(data.collectionId), updateTime: data.updateTime, ...formatDatasetDataValue({ teamId, @@ -653,37 +556,204 @@ export async function searchDatasetData( imageDescMap: data.imageDescMap }), chunkIndex: data.chunkIndex, - indexes: data.indexes, + datasetId: String(data.datasetId), + collectionId: String(data.collectionId), ...getCollectionSourceData(collection), - score: [ - { - type: SearchScoreTypeEnum.fullText, - value: item.score || 0, - index - } - ] + score: [{ type: SearchScoreTypeEnum.embedding, value: item?.score || 0, index }] }; + + return result; }) + // 多个向量对应一个数据,每一路召回,保障数据只有一份,并且取最高排名 .filter((item) => { if (!item) return false; + if (set.has(item.id)) return false; + set.add(item.id); return true; }) .map((item, index) => { - if (!item) return; return { - ...item, - score: item.score.map((item) => ({ ...item, index })) + ...item!, + score: item!.score.map((item) => ({ ...item, index })) }; - }) as SearchDataResponseItemType[], - tokenLen: 0 - }; - } catch (error) { - addLog.error('Full text search error', error); + }) as SearchDataResponseItemType[] + ); + }); + + return { + embeddingRecallResults, + tokens + }; + }; + const fullTextRecall = async ({ + queries, + limit, + filterCollectionIdList, + forbidCollectionIdList + }: { + queries: string[]; + limit: number; + filterCollectionIdList?: string[]; + forbidCollectionIdList: string[]; + }): Promise<{ + fullTextRecallResults: SearchDataResponseItemType[][]; + }> => { + if (limit === 0) { return { - fullTextRecallResults: [], - tokenLen: 0 + fullTextRecallResults: [] }; } + + const recallResults = await Promise.all( + queries.map(async (query) => { + return (await MongoDatasetDataText.aggregate( + [ + { + $match: { + teamId: new Types.ObjectId(teamId), + $text: { $search: await jiebaSplit({ text: query }) }, + datasetId: { $in: datasetIds.map((id) => new Types.ObjectId(id)) }, + ...(filterCollectionIdList + ? { + collectionId: { + $in: filterCollectionIdList + .filter((id) => !forbidCollectionIdList.includes(id)) + .map((id) => new Types.ObjectId(id)) + } + } + : forbidCollectionIdList?.length + ? { + collectionId: { + $nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id)) + } + } + : {}) + } + }, + { + $sort: { + score: { $meta: 'textScore' } + } + }, + { + $limit: limit + }, + { + $project: { + _id: 1, + collectionId: 1, + dataId: 1, + score: { $meta: 'textScore' } + } + } + ], + { + ...readFromSecondary + } + )) as (DatasetDataTextSchemaType & { score: number })[]; + }) + ); + + const dataIds = Array.from( + new Set(recallResults.map((item) => item.map((item) => item.dataId)).flat()) + ); + const collectionIds = Array.from( + new Set(recallResults.map((item) => item.map((item) => item.collectionId)).flat()) + ); + + // Get data and collections + const [dataMaps, collectionMaps] = await Promise.all([ + MongoDatasetData.find( + { + _id: { $in: dataIds } + }, + datasetDataSelectField, + { ...readFromSecondary } + ) + .lean() + .then((res) => { + const map = new Map(); + + res.forEach((item) => { + map.set(String(item._id), item); + }); + + return map; + }), + MongoDatasetCollection.find( + { + _id: { $in: collectionIds } + }, + datsaetCollectionSelectField, + { ...readFromSecondary } + ) + .lean() + .then((res) => { + const map = new Map(); + + res.forEach((item) => { + map.set(String(item._id), item); + }); + + return map; + }) + ]); + + const fullTextRecallResults = recallResults.map((item) => { + return item + .map((item, index) => { + const collection = collectionMaps.get(String(item.collectionId)); + if (!collection) { + console.log('Collection is not found', item); + return; + } + + const data = dataMaps.get(String(item.dataId)); + if (!data) { + console.log('Data is not found', item); + return; + } + + return { + id: String(data._id), + datasetId: String(data.datasetId), + collectionId: String(data.collectionId), + updateTime: data.updateTime, + ...formatDatasetDataValue({ + teamId, + datasetId: data.datasetId, + q: data.q, + a: data.a, + imageId: data.imageId, + imageDescMap: data.imageDescMap + }), + chunkIndex: data.chunkIndex, + indexes: data.indexes, + ...getCollectionSourceData(collection), + score: [ + { + type: SearchScoreTypeEnum.fullText, + value: item.score || 0, + index + } + ] + }; + }) + .filter((item) => { + if (!item) return false; + return true; + }) + .map((item, index) => { + return { + ...item, + score: item!.score.map((item) => ({ ...item, index })) + }; + }) as SearchDataResponseItemType[]; + }); + + return { + fullTextRecallResults + }; }; const multiQueryRecall = async ({ embeddingLimit, @@ -692,50 +762,36 @@ export async function searchDatasetData( embeddingLimit: number; fullTextLimit: number; }) => { - // multi query recall - const embeddingRecallResList: SearchDataResponseItemType[][] = []; - const fullTextRecallResList: SearchDataResponseItemType[][] = []; - let totalTokens = 0; - const [{ forbidCollectionIdList }, filterCollectionIdList] = await Promise.all([ getForbidData(), filterCollectionByMetadata() ]); - await Promise.all( - queries.map(async (query) => { - const [{ tokens, embeddingRecallResults }, { fullTextRecallResults }] = await Promise.all([ - embeddingRecall({ - query, - limit: embeddingLimit, - forbidCollectionIdList, - filterCollectionIdList - }), - // FullText tmp - fullTextRecall({ - query, - limit: fullTextLimit, - filterCollectionIdList, - forbidCollectionIdList - }) - ]); - totalTokens += tokens; - - embeddingRecallResList.push(embeddingRecallResults); - fullTextRecallResList.push(fullTextRecallResults); + const [{ tokens, embeddingRecallResults }, { fullTextRecallResults }] = await Promise.all([ + embeddingRecall({ + queries, + limit: embeddingLimit, + forbidCollectionIdList, + filterCollectionIdList + }), + fullTextRecall({ + queries, + limit: fullTextLimit, + filterCollectionIdList, + forbidCollectionIdList }) - ); + ]); // rrf concat const rrfEmbRecall = datasetSearchResultConcat( - embeddingRecallResList.map((list) => ({ k: 60, list })) + embeddingRecallResults.map((list) => ({ k: 60, list })) ).slice(0, embeddingLimit); const rrfFTRecall = datasetSearchResultConcat( - fullTextRecallResList.map((list) => ({ k: 60, list })) + fullTextRecallResults.map((list) => ({ k: 60, list })) ).slice(0, fullTextLimit); return { - tokens: totalTokens, + tokens, embeddingRecallResults: rrfEmbRecall, fullTextRecallResults: rrfFTRecall }; diff --git a/packages/service/core/dataset/training/controller.ts b/packages/service/core/dataset/training/controller.ts index 9216e6488..a2daac822 100644 --- a/packages/service/core/dataset/training/controller.ts +++ b/packages/service/core/dataset/training/controller.ts @@ -53,7 +53,7 @@ export async function pushDataListToTrainingQueue({ const { model, maxToken, weight } = await (async () => { if (mode === TrainingModeEnum.chunk) { return { - maxToken: getLLMMaxChunkSize(agentModelData), + maxToken: Infinity, model: vectorModelData.model, weight: vectorModelData.weight }; diff --git a/packages/service/core/workflow/dispatch/child/runApp.ts b/packages/service/core/workflow/dispatch/child/runApp.ts index d35f30c58..c735a5fde 100644 --- a/packages/service/core/workflow/dispatch/child/runApp.ts +++ b/packages/service/core/workflow/dispatch/child/runApp.ts @@ -21,6 +21,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getAppVersionById } from '../../../app/version/controller'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team'; +import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.userChatInput]: string; @@ -147,6 +148,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { tmbId: String(appData.tmbId), isChildApp: true }, + runningUserInfo: await getRunningUserInfoByTmbId(appData.tmbId), runtimeNodes, runtimeEdges, histories: chatHistories, diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 57a5c7e0e..5cb099d32 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -90,6 +90,10 @@ export const dispatchRunTool = async (props: RunToolProps): Promise { */ export const rewriteRuntimeWorkFlow = async ({ nodes, - edges + edges, + lang }: { nodes: RuntimeNodeItemType[]; edges: RuntimeEdgeItemType[]; + lang?: localeType; }) => { const toolSetNodes = nodes.filter((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet); @@ -195,7 +198,8 @@ export const rewriteRuntimeWorkFlow = async ({ // systemTool if (systemToolId) { const children = await getSystemToolRunTimeNodeFromSystemToolset({ - toolSetNode + toolSetNode, + lang }); children.forEach((node) => { nodes.push(node); diff --git a/packages/service/core/workflow/utils.ts b/packages/service/core/workflow/utils.ts index 47399885c..b78152f64 100644 --- a/packages/service/core/workflow/utils.ts +++ b/packages/service/core/workflow/utils.ts @@ -6,6 +6,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import type { localeType } from '@fastgpt/global/common/i18n/type'; /* filter search result */ export const filterSearchResultsByMaxChars = async ( @@ -31,9 +32,11 @@ export const filterSearchResultsByMaxChars = async ( }; export async function getSystemToolRunTimeNodeFromSystemToolset({ - toolSetNode + toolSetNode, + lang = 'en' }: { toolSetNode: RuntimeNodeItemType; + lang?: localeType; }): Promise { const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId!; @@ -41,13 +44,14 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ (item) => item.key === NodeInputKeyEnum.systemInputConfig ); const tools = await getSystemTools(); - const children = tools.filter((item) => item.parentId === systemToolId); - + const children = tools.filter( + (item) => item.parentId === systemToolId && item.isActive !== false + ); const nodes = await Promise.all( children.map(async (child) => { const toolListItem = toolSetNode.toolConfig?.systemToolSet?.toolList.find( (item) => item.toolId === child.id - )!; + ); const tool = await getSystemPluginByIdAndVersionId(child.id); @@ -63,8 +67,8 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ ...tool, inputs, outputs: tool.outputs ?? [], - name: toolListItem.name ?? parseI18nString(tool.name, 'en'), - intro: toolListItem.description ?? parseI18nString(tool.intro, 'en'), + name: toolListItem?.name || parseI18nString(tool.name, lang), + intro: toolListItem?.description || parseI18nString(tool.intro, lang), flowNodeType: FlowNodeTypeEnum.tool, nodeId: getNanoid(), toolConfig: { diff --git a/packages/service/package.json b/packages/service/package.json index b7f1c7dc8..47357d134 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "dependencies": { - "@fastgpt-sdk/plugin": "^0.1.7", + "@fastgpt-sdk/plugin": "^0.1.8", "@fastgpt/global": "workspace:*", "@modelcontextprotocol/sdk": "^1.12.1", "@node-rs/jieba": "2.0.1", @@ -15,7 +15,7 @@ "@opentelemetry/winston-transport": "^0.14.0", "@vercel/otel": "^1.13.0", "@xmldom/xmldom": "^0.8.10", - "@zilliz/milvus2-sdk-node": "2.4.2", + "@zilliz/milvus2-sdk-node": "2.4.10", "axios": "^1.8.2", "bullmq": "^5.52.2", "chalk": "^5.3.0", diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 08b0a129f..c4027a5e7 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -2,17 +2,22 @@ import { MongoApp } from '../../../core/app/schema'; import { type AppDetailType } from '@fastgpt/global/core/app/type.d'; import { parseHeaderCert } from '../controller'; -import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { + PerResourceTypeEnum, + ReadPermissionVal, + ReadRoleVal +} from '@fastgpt/global/support/permission/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { getResourcePermission } from '../controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; +import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; import { type AuthModeType, type AuthResponseType } from '../type'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; +import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; export const authPluginByTmbId = async ({ tmbId, @@ -68,6 +73,21 @@ export const authAppByTmbId = async ({ return Promise.reject(AppErrEnum.unAuthApp); } + if (app.type === AppTypeEnum.hidden) { + if (per === AppReadChatLogPerVal) { + if (!tmbPer.hasManagePer) { + return Promise.reject(AppErrEnum.unAuthApp); + } + } else if (per !== ReadPermissionVal) { + return Promise.reject(AppErrEnum.unAuthApp); + } + + return { + ...app, + permission: new AppPermission({ isOwner: false, role: ReadRoleVal }) + }; + } + const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); const { Per } = await (async () => { @@ -134,7 +154,7 @@ export const authApp = async ({ appId: ParentIdType; per: PermissionValueType; }): Promise< - AuthResponseType & { + AuthResponseType & { app: AppDetailType; } > => { diff --git a/packages/service/support/user/team/utils.ts b/packages/service/support/user/team/utils.ts new file mode 100644 index 000000000..eccdffac4 --- /dev/null +++ b/packages/service/support/user/team/utils.ts @@ -0,0 +1,35 @@ +import { MongoTeamMember } from '../../user/team/teamMemberSchema'; +import { type UserModelSchema } from '@fastgpt/global/support/user/type'; +import { type TeamSchema } from '@fastgpt/global/support/user/team/type'; +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; + +// TODO: 数据库优化 +export async function getRunningUserInfoByTmbId(tmbId: string) { + if (tmbId) { + const tmb = await MongoTeamMember.findById(tmbId, 'teamId name userId') // team_members name is the user's name + .populate<{ team: TeamSchema; user: UserModelSchema }>([ + { + path: 'team', + select: 'name' + }, + { + path: 'user', + select: 'username contact' + } + ]) + .lean(); + + if (!tmb) return Promise.reject(TeamErrEnum.notUser); + + return { + username: tmb.user.username, + teamName: tmb.team.name, + memberName: tmb.name, + contact: tmb.user.contact || '', + teamId: tmb.teamId, + tmbId: tmb._id + }; + } + + return Promise.reject(TeamErrEnum.notUser); +} diff --git a/packages/templates/src/CQ/template.json b/packages/templates/src/CQ/template.json index 7909383eb..d8094e175 100644 --- a/packages/templates/src/CQ/template.json +++ b/packages/templates/src/CQ/template.json @@ -70,7 +70,7 @@ "renderTypeList": ["settingLLMModel", "reference"], "label": "core.module.input.label.aiModel", "valueType": "string", - "value": "gpt-4o-mini" + "value": "gpt-5" }, { "key": "temperature", @@ -189,7 +189,7 @@ "required": true, "valueType": "string", "llmModelType": "classify", - "value": "gpt-4o-mini" + "value": "gpt-5" }, { "key": "systemPrompt", diff --git a/packages/templates/src/longTranslate/template.json b/packages/templates/src/longTranslate/template.json index 9f9bcc913..11396b36c 100644 --- a/packages/templates/src/longTranslate/template.json +++ b/packages/templates/src/longTranslate/template.json @@ -1428,7 +1428,7 @@ "description": "", "debugLabel": "", "toolDescription": "", - "value": "gpt-4o-mini" + "value": "gpt-5" }, { "key": "datasetSearchExtensionBg", diff --git a/packages/templates/src/simpleDatasetChat/template.json b/packages/templates/src/simpleDatasetChat/template.json index 93f5aefa1..5e3aac255 100644 --- a/packages/templates/src/simpleDatasetChat/template.json +++ b/packages/templates/src/simpleDatasetChat/template.json @@ -73,7 +73,7 @@ ], "label": "core.module.input.label.aiModel", "valueType": "string", - "value": "gpt-4o-mini" + "value": "gpt-5" }, { "key": "temperature", diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 53d3e0ab9..18211c892 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -5,6 +5,7 @@ export const iconPaths = { backup: () => import('./icons/backup.svg'), book: () => import('./icons/book.svg'), change: () => import('./icons/change.svg'), + chart: () => import('./icons/chart.svg'), chatSend: () => import('./icons/chatSend.svg'), check: () => import('./icons/check.svg'), checkCircle: () => import('./icons/checkCircle.svg'), @@ -112,9 +113,9 @@ export const iconPaths = { 'common/tickFill': () => import('./icons/common/tickFill.svg'), 'common/toolkit': () => import('./icons/common/toolkit.svg'), 'common/trash': () => import('./icons/common/trash.svg'), + 'common/upRightArrowLight': () => import('./icons/common/upRightArrowLight.svg'), 'common/uploadFileFill': () => import('./icons/common/uploadFileFill.svg'), 'common/upperRight': () => import('./icons/common/upperRight.svg'), - 'common/upRightArrowLight': () => import('./icons/common/upRightArrowLight.svg'), 'common/userInfo': () => import('./icons/common/userInfo.svg'), 'common/variable': () => import('./icons/common/variable.svg'), 'common/viewLight': () => import('./icons/common/viewLight.svg'), @@ -151,6 +152,8 @@ export const iconPaths = { 'core/app/simpleMode/tts': () => import('./icons/core/app/simpleMode/tts.svg'), 'core/app/simpleMode/variable': () => import('./icons/core/app/simpleMode/variable.svg'), 'core/app/simpleMode/whisper': () => import('./icons/core/app/simpleMode/whisper.svg'), + 'core/app/templates/TranslateRobot': () => + import('./icons/core/app/templates/TranslateRobot.svg'), 'core/app/templates/animalLife': () => import('./icons/core/app/templates/animalLife.svg'), 'core/app/templates/chinese': () => import('./icons/core/app/templates/chinese.svg'), 'core/app/templates/divination': () => import('./icons/core/app/templates/divination.svg'), @@ -160,8 +163,6 @@ export const iconPaths = { 'core/app/templates/plugin-dalle': () => import('./icons/core/app/templates/plugin-dalle.svg'), 'core/app/templates/plugin-feishu': () => import('./icons/core/app/templates/plugin-feishu.svg'), 'core/app/templates/stock': () => import('./icons/core/app/templates/stock.svg'), - 'core/app/templates/TranslateRobot': () => - import('./icons/core/app/templates/TranslateRobot.svg'), 'core/app/toolCall': () => import('./icons/core/app/toolCall.svg'), 'core/app/ttsFill': () => import('./icons/core/app/ttsFill.svg'), 'core/app/type/httpPlugin': () => import('./icons/core/app/type/httpPlugin.svg'), @@ -180,6 +181,7 @@ export const iconPaths = { 'core/app/variable/input': () => import('./icons/core/app/variable/input.svg'), 'core/app/variable/select': () => import('./icons/core/app/variable/select.svg'), 'core/app/variable/textarea': () => import('./icons/core/app/variable/textarea.svg'), + 'core/chat/QGFill': () => import('./icons/core/chat/QGFill.svg'), 'core/chat/backText': () => import('./icons/core/chat/backText.svg'), 'core/chat/cancelSpeak': () => import('./icons/core/chat/cancelSpeak.svg'), 'core/chat/chatFill': () => import('./icons/core/chat/chatFill.svg'), @@ -196,15 +198,19 @@ export const iconPaths = { 'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg'), 'core/chat/finishSpeak': () => import('./icons/core/chat/finishSpeak.svg'), 'core/chat/imgSelect': () => import('./icons/core/chat/imgSelect.svg'), - 'core/chat/QGFill': () => import('./icons/core/chat/QGFill.svg'), 'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'), 'core/chat/quoteSign': () => import('./icons/core/chat/quoteSign.svg'), 'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'), 'core/chat/sendFill': () => import('./icons/core/chat/sendFill.svg'), 'core/chat/sendLight': () => import('./icons/core/chat/sendLight.svg'), 'core/chat/setTopLight': () => import('./icons/core/chat/setTopLight.svg'), + 'core/chat/setting/share': () => import('./icons/core/chat/setting/share.svg'), 'core/chat/sideLine': () => import('./icons/core/chat/sideLine.svg'), + 'core/chat/sidebar/expand': () => import('./icons/core/chat/sidebar/expand.svg'), + 'core/chat/sidebar/fold': () => import('./icons/core/chat/sidebar/fold.svg'), + '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/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'), @@ -285,12 +291,13 @@ export const iconPaths = { 'core/workflow/redo': () => import('./icons/core/workflow/redo.svg'), 'core/workflow/revertVersion': () => import('./icons/core/workflow/revertVersion.svg'), 'core/workflow/runError': () => import('./icons/core/workflow/runError.svg'), - 'core/workflow/running': () => import('./icons/core/workflow/running.svg'), 'core/workflow/runSkip': () => import('./icons/core/workflow/runSkip.svg'), 'core/workflow/runSuccess': () => import('./icons/core/workflow/runSuccess.svg'), + 'core/workflow/running': () => import('./icons/core/workflow/running.svg'), + 'core/workflow/template/BI': () => import('./icons/core/workflow/template/BI.svg'), + 'core/workflow/template/FileRead': () => import('./icons/core/workflow/template/FileRead.svg'), 'core/workflow/template/aiChat': () => import('./icons/core/workflow/template/aiChat.svg'), 'core/workflow/template/baseChart': () => import('./icons/core/workflow/template/baseChart.svg'), - 'core/workflow/template/BI': () => import('./icons/core/workflow/template/BI.svg'), 'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'), 'core/workflow/template/bocha': () => import('./icons/core/workflow/template/bocha.svg'), 'core/workflow/template/codeRun': () => import('./icons/core/workflow/template/codeRun.svg'), @@ -307,7 +314,6 @@ export const iconPaths = { 'core/workflow/template/extractJson': () => import('./icons/core/workflow/template/extractJson.svg'), 'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'), - 'core/workflow/template/FileRead': () => import('./icons/core/workflow/template/FileRead.svg'), 'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'), 'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'), 'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'), @@ -337,12 +343,12 @@ export const iconPaths = { 'core/workflow/template/textConcat': () => import('./icons/core/workflow/template/textConcat.svg'), 'core/workflow/template/toolCall': () => import('./icons/core/workflow/template/toolCall.svg'), + 'core/workflow/template/toolParams': () => + import('./icons/core/workflow/template/toolParams.svg'), 'core/workflow/template/toolkitActive': () => import('./icons/core/workflow/template/toolkitActive.svg'), 'core/workflow/template/toolkitInactive': () => import('./icons/core/workflow/template/toolkitInactive.svg'), - 'core/workflow/template/toolParams': () => - import('./icons/core/workflow/template/toolParams.svg'), 'core/workflow/template/userSelect': () => import('./icons/core/workflow/template/userSelect.svg'), 'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'), @@ -387,6 +393,7 @@ export const iconPaths = { history: () => import('./icons/history.svg'), image: () => import('./icons/image.svg'), infoRounded: () => import('./icons/infoRounded.svg'), + invisible: () => import('./icons/invisible.svg'), kbTest: () => import('./icons/kbTest.svg'), key: () => import('./icons/key.svg'), keyPrimary: () => import('./icons/keyPrimary.svg'), @@ -403,15 +410,17 @@ 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/BAAI': () => import('./icons/model/BAAI.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'), @@ -428,13 +437,16 @@ export const iconPaths = { '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'), @@ -455,6 +467,7 @@ export const iconPaths = { 'price/right': () => import('./icons/price/right.svg'), save: () => import('./icons/save.svg'), sliderTag: () => import('./icons/sliderTag.svg'), + star: () => import('./icons/star.svg'), stop: () => import('./icons/stop.svg'), 'support/account/coupon': () => import('./icons/support/account/coupon.svg'), 'support/account/laf': () => import('./icons/support/account/laf.svg'), @@ -485,8 +498,8 @@ export const iconPaths = { 'support/user/usersLight': () => import('./icons/support/user/usersLight.svg'), text: () => import('./icons/text.svg'), union: () => import('./icons/union.svg'), + upload: () => import('./icons/upload.svg'), user: () => import('./icons/user.svg'), visible: () => import('./icons/visible.svg'), - invisible: () => import('./icons/invisible.svg'), wx: () => import('./icons/wx.svg') }; diff --git a/packages/web/components/common/Icon/icons/chart.svg b/packages/web/components/common/Icon/icons/chart.svg new file mode 100644 index 000000000..a3d16b6bf --- /dev/null +++ b/packages/web/components/common/Icon/icons/chart.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/chat/setting/share.svg b/packages/web/components/common/Icon/icons/core/chat/setting/share.svg new file mode 100644 index 000000000..91cea6c69 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/setting/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/core/chat/sidebar/expand.svg b/packages/web/components/common/Icon/icons/core/chat/sidebar/expand.svg new file mode 100644 index 000000000..45b5d374c --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/sidebar/expand.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/web/components/common/Icon/icons/core/chat/sidebar/fold.svg b/packages/web/components/common/Icon/icons/core/chat/sidebar/fold.svg new file mode 100644 index 000000000..5be27c479 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/sidebar/fold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/web/components/common/Icon/icons/core/chat/sidebar/home.svg b/packages/web/components/common/Icon/icons/core/chat/sidebar/home.svg new file mode 100644 index 000000000..7f6f5ac10 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/sidebar/home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/chat/sidebar/menu.svg b/packages/web/components/common/Icon/icons/core/chat/sidebar/menu.svg new file mode 100644 index 000000000..e018f3ea1 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/sidebar/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/model/ai360.svg b/packages/web/components/common/Icon/icons/model/ai360.svg new file mode 100644 index 000000000..2a1e86155 --- /dev/null +++ b/packages/web/components/common/Icon/icons/model/ai360.svg @@ -0,0 +1 @@ +AI360 \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/model/coze.svg b/packages/web/components/common/Icon/icons/model/coze.svg new file mode 100644 index 000000000..743f6d6a6 --- /dev/null +++ b/packages/web/components/common/Icon/icons/model/coze.svg @@ -0,0 +1 @@ +Coze \ 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 new file mode 100644 index 000000000..0658ce0f0 --- /dev/null +++ b/packages/web/components/common/Icon/icons/model/novita.svg @@ -0,0 +1 @@ +Novita AI \ 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 new file mode 100644 index 000000000..c1237c133 --- /dev/null +++ b/packages/web/components/common/Icon/icons/model/openrouter.svg @@ -0,0 +1 @@ +OpenRouter \ 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 new file mode 100644 index 000000000..e721368de --- /dev/null +++ b/packages/web/components/common/Icon/icons/model/vertexai.svg @@ -0,0 +1 @@ +VertexAI \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/star.svg b/packages/web/components/common/Icon/icons/star.svg new file mode 100644 index 000000000..d9684979b --- /dev/null +++ b/packages/web/components/common/Icon/icons/star.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/upload.svg b/packages/web/components/common/Icon/icons/upload.svg new file mode 100644 index 000000000..9d0f37cf3 --- /dev/null +++ b/packages/web/components/common/Icon/icons/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Input/NumberInput/index.tsx b/packages/web/components/common/Input/NumberInput/index.tsx index 12e139585..dfa59e7a0 100644 --- a/packages/web/components/common/Input/NumberInput/index.tsx +++ b/packages/web/components/common/Input/NumberInput/index.tsx @@ -55,7 +55,7 @@ const MyNumberInput = (props: Props) => { } }} onChange={(e) => { - const numE = e === '' ? '' : e.endsWith('.') ? e : Number(e); + const numE = e === '' ? '' : e.endsWith('.') || /^\d+\.0+$/.test(e) ? e : Number(e); if (onChange) { if (numE === '') { // @ts-ignore diff --git a/packages/web/components/common/MyModal/index.tsx b/packages/web/components/common/MyModal/index.tsx index aeb6fbafa..2e5bb86d8 100644 --- a/packages/web/components/common/MyModal/index.tsx +++ b/packages/web/components/common/MyModal/index.tsx @@ -23,6 +23,7 @@ export interface MyModalProps extends ModalContentProps { onClose?: () => void; closeOnOverlayClick?: boolean; size?: 'md' | 'lg'; + showCloseButton?: boolean; } const MyModal = ({ @@ -38,6 +39,7 @@ const MyModal = ({ closeOnOverlayClick = true, iconColor, size = 'md', + showCloseButton = true, ...props }: MyModalProps) => { const { isPc } = useSystem(); @@ -65,7 +67,7 @@ const MyModal = ({ boxShadow={'7'} {...props} > - {!title && onClose && } + {!title && onClose && showCloseButton && } {!!title && ( ( : { color: 'myGray.900' })} - onClick={() => { + onClick={(e) => { + e.stopPropagation(); if (value !== item.value) { onClickChange(item.value); } @@ -172,7 +173,7 @@ const MySelect = ( display={'block'} mb={0.5} > - + {item.icon && ( )} @@ -303,6 +304,9 @@ const MySelect = ( zIndex={99} maxH={'45vh'} overflowY={'auto'} + onClick={(e) => { + e.stopPropagation(); + }} > {ScrollData ? {ListRender} : ListRender} diff --git a/packages/web/components/common/charts/AreaChartComponent.tsx b/packages/web/components/common/charts/AreaChartComponent.tsx new file mode 100644 index 000000000..1d071738b --- /dev/null +++ b/packages/web/components/common/charts/AreaChartComponent.tsx @@ -0,0 +1,226 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Box, HStack, useTheme } from '@chakra-ui/react'; +import { + ResponsiveContainer, + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + type TooltipProps +} from 'recharts'; +import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import { formatNumber } from '@fastgpt/global/common/math/tools'; +import FillRowTabs from '../Tabs/FillRowTabs'; +import { useTranslation } from 'next-i18next'; +import { cloneDeep } from 'lodash'; + +type AreaConfig = { + dataKey: string; + name: string; + color: string; + gradient?: boolean; +}; + +type TooltipItem = { + label: string; + dataKey: string; + color: string; + formatter?: (value: number) => string; + customValue?: (data: Record) => number; +}; + +type AreaChartComponentProps = { + data: Record[]; + title: string; + HeaderLeftChildren?: React.ReactNode; + lines: AreaConfig[]; + tooltipItems?: TooltipItem[]; + + defaultDisplayMode?: 'incremental' | 'cumulative'; + enableIncremental?: boolean; + enableCumulative?: boolean; + enableTooltip?: boolean; + startDateValue?: number; +}; + +const CustomTooltip = ({ + active, + payload, + tooltipItems +}: TooltipProps & { tooltipItems?: TooltipItem[] }) => { + const data = payload?.[0]?.payload; + + if (!active || !data || !tooltipItems) { + return null; + } + + return ( + + + {data.xLabel || data.x} + + {tooltipItems.map((item, index) => { + const value = item.customValue ? item.customValue(data) : data[item.dataKey]; + const displayValue = item.formatter ? item.formatter(value) : formatNumber(value); + + return ( + + + {item.label} + {displayValue.toLocaleString()} + + ); + })} + + ); +}; + +const AreaChartComponent = ({ + data, + title, + HeaderLeftChildren, + lines, + tooltipItems, + defaultDisplayMode = 'incremental', + enableIncremental = true, + enableCumulative = true, + startDateValue = 0 +}: AreaChartComponentProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [displayMode, setDisplayMode] = useState<'incremental' | 'cumulative'>(defaultDisplayMode); + + // Tab list constant + const tabList = useMemo( + () => [ + ...(enableIncremental + ? [{ label: t('common:chart_mode_incremental'), value: 'incremental' as const }] + : []), + ...(enableCumulative + ? [{ label: t('common:chart_mode_cumulative'), value: 'cumulative' as const }] + : []) + ], + [enableCumulative, enableIncremental, t] + ); + + // Y-axis number formatter function + const formatYAxisNumber = useCallback((value: number): string => { + if (value >= 1000000) { + return value / 1000000 + 'M'; + } else if (value >= 1000) { + return value / 1000 + 'K'; + } + return value.toString(); + }, []); + + // Process data based on display mode + const processedData = useMemo(() => { + if (displayMode === 'incremental') { + return data; + } + + // Cumulative mode: accumulate values for each line's dataKey + const cloneData = cloneDeep(data); + + const dataKeys = lines.map((item) => item.dataKey); + + return cloneData.map((item, index) => { + if (index === 0) { + item[dataKeys[0]] = startDateValue + item[dataKeys[0]]; + return item; + } + + dataKeys.forEach((key) => { + if (typeof item[key] === 'number') { + item[key] += cloneData[index - 1][key]; + } + }); + + return item; + }); + }, [displayMode, data, lines, startDateValue]); + + // Generate gradient definitions + const gradientDefs = useMemo( + () => ( + + {lines.map((line) => ( + + + + + ))} + + ), + [lines] + ); + + return ( + <> + + + {title} + + + {HeaderLeftChildren} + {tabList.length > 1 && ( + + list={tabList} + py={0.5} + px={2} + value={displayMode} + onChange={setDisplayMode} + /> + )} + + + + + {gradientDefs} + + + + {tooltipItems && } />} + {lines.map((line, index) => ( + + ))} + + + + ); +}; + +export default AreaChartComponent; diff --git a/packages/web/components/common/charts/BarChartComponent.tsx b/packages/web/components/common/charts/BarChartComponent.tsx new file mode 100644 index 000000000..f505d5c09 --- /dev/null +++ b/packages/web/components/common/charts/BarChartComponent.tsx @@ -0,0 +1,152 @@ +import React, { useCallback, useMemo } from 'react'; +import { Box, Flex, HStack, useTheme } from '@chakra-ui/react'; +import { + ResponsiveContainer, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + type TooltipProps, + BarChart, + Bar +} from 'recharts'; +import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import { formatNumber } from '@fastgpt/global/common/math/tools'; +import { useTranslation } from 'next-i18next'; +import QuestionTip from '../MyTooltip/QuestionTip'; + +type BarConfig = { + dataKey: string; + name: string; + color: string; + stackId?: string; +}; + +type TooltipItem = { + label: string; + dataKey: string; + color: string; + formatter?: (value: number) => string; + customValue?: (data: Record) => number; +}; + +type BarChartComponentProps = { + data: Record[]; + title: string; + description?: string; + HeaderRightChildren?: React.ReactNode; + bars: BarConfig[]; + tooltipItems?: TooltipItem[]; + blur?: boolean; +}; + +const CustomTooltip = ({ + active, + payload, + tooltipItems +}: TooltipProps & { tooltipItems?: TooltipItem[] }) => { + const data = payload?.[0]?.payload; + + if (!active || !data || !tooltipItems) { + return null; + } + + return ( + + + {data.xLabel || data.x} + + {tooltipItems.map((item, index) => { + const value = item.customValue ? item.customValue(data) : data[item.dataKey]; + const displayValue = item.formatter ? item.formatter(value) : formatNumber(value); + + return ( + + + {item.label} + {displayValue.toLocaleString()} + + ); + })} + + ); +}; + +const BarChartComponent = ({ + data, + title, + description, + HeaderRightChildren, + bars, + tooltipItems, + blur = false +}: BarChartComponentProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + // Y-axis number formatter function + const formatYAxisNumber = useCallback((value: number): string => { + if (value >= 1000000) { + return value / 1000000 + 'M'; + } else if (value >= 1000) { + return value / 1000 + 'K'; + } + return value.toString(); + }, []); + + return ( + <> + + + + {title} + + + + + {HeaderRightChildren} + + + + + + + + {tooltipItems && } />} + {bars.map((bar) => ( + + ))} + + + + ); +}; + +export default BarChartComponent; diff --git a/packages/web/components/common/charts/LineChartComponent.tsx b/packages/web/components/common/charts/LineChartComponent.tsx index f5d7b8944..8faf70dfc 100644 --- a/packages/web/components/common/charts/LineChartComponent.tsx +++ b/packages/web/components/common/charts/LineChartComponent.tsx @@ -1,20 +1,20 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Box, HStack, useTheme } from '@chakra-ui/react'; +import React, { useCallback, useMemo } from 'react'; +import { Box, Flex, HStack, useTheme } from '@chakra-ui/react'; import { ResponsiveContainer, - AreaChart, - Area, XAxis, YAxis, CartesianGrid, Tooltip, - type TooltipProps + type TooltipProps, + LineChart, + Line, + ReferenceLine } from 'recharts'; import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent'; import { formatNumber } from '@fastgpt/global/common/math/tools'; -import FillRowTabs from '../Tabs/FillRowTabs'; import { useTranslation } from 'next-i18next'; -import { cloneDeep } from 'lodash'; +import QuestionTip from '../MyTooltip/QuestionTip'; type LineConfig = { dataKey: string; @@ -34,15 +34,13 @@ type TooltipItem = { type LineChartComponentProps = { data: Record[]; title: string; - HeaderLeftChildren?: React.ReactNode; + description?: string; + HeaderRightChildren?: React.ReactNode; lines: LineConfig[]; tooltipItems?: TooltipItem[]; - - defaultDisplayMode?: 'incremental' | 'cumulative'; - enableIncremental?: boolean; - enableCumulative?: boolean; - enableTooltip?: boolean; - startDateValue?: number; + showAverage?: boolean; + averageKey?: string; + blur?: boolean; }; const CustomTooltip = ({ @@ -57,8 +55,15 @@ const CustomTooltip = ({ } return ( - - + 8 ? { position: 'relative', top: '-30px' } : {})} + > + 5 ? 1 : 2}> {data.xLabel || data.x} {tooltipItems.map((item, index) => { @@ -66,7 +71,7 @@ const CustomTooltip = ({ const displayValue = item.formatter ? item.formatter(value) : formatNumber(value); return ( - + 5 ? 0 : 1 }}> {item.label} {displayValue.toLocaleString()} @@ -80,30 +85,15 @@ const CustomTooltip = ({ const LineChartComponent = ({ data, title, - HeaderLeftChildren, + description, + HeaderRightChildren, lines, tooltipItems, - defaultDisplayMode = 'incremental', - enableIncremental = true, - enableCumulative = true, - startDateValue = 0 + showAverage = false, + averageKey, + blur = false }: LineChartComponentProps) => { const theme = useTheme(); - const { t } = useTranslation(); - const [displayMode, setDisplayMode] = useState<'incremental' | 'cumulative'>(defaultDisplayMode); - - // Tab list constant - const tabList = useMemo( - () => [ - ...(enableIncremental - ? [{ label: t('common:chart_mode_incremental'), value: 'incremental' as const }] - : []), - ...(enableCumulative - ? [{ label: t('common:chart_mode_cumulative'), value: 'cumulative' as const }] - : []) - ], - [enableCumulative, enableIncremental, t] - ); // Y-axis number formatter function const formatYAxisNumber = useCallback((value: number): string => { @@ -115,32 +105,13 @@ const LineChartComponent = ({ return value.toString(); }, []); - // Process data based on display mode - const processedData = useMemo(() => { - if (displayMode === 'incremental') { - return data; - } + // Calculate average value + const averageValue = useMemo(() => { + if (!showAverage || !averageKey || data.length === 0) return null; - // Cumulative mode: accumulate values for each line's dataKey - const cloneData = cloneDeep(data); - - const dataKeys = lines.map((item) => item.dataKey); - - return cloneData.map((item, index) => { - if (index === 0) { - item[dataKeys[0]] = startDateValue + item[dataKeys[0]]; - return item; - } - - dataKeys.forEach((key) => { - if (typeof item[key] === 'number') { - item[key] += cloneData[index - 1][key]; - } - }); - - return item; - }); - }, [displayMode, data, lines, startDateValue]); + const sum = data.reduce((acc, item) => acc + (item[averageKey] || 0), 0); + return sum / data.length; + }, [showAverage, averageKey, data]); // Generate gradient definitions const gradientDefs = useMemo( @@ -165,28 +136,49 @@ const LineChartComponent = ({ ); return ( - <> - - - {title} + { + const chartElement = e.currentTarget.querySelector('.recharts-wrapper'); + if (chartElement && showAverage && averageValue !== null) { + chartElement.classList.add('show-average'); + } + }} + onMouseLeave={(e) => { + const chartElement = e.currentTarget.querySelector('.recharts-wrapper'); + if (chartElement) { + chartElement.classList.remove('show-average'); + } + }} + h="100%" + > + + + + + {title} + + + + + {HeaderRightChildren} - - {HeaderLeftChildren} - {tabList.length > 1 && ( - - list={tabList} - py={0.5} - px={2} - value={displayMode} - onChange={setDisplayMode} - /> - )} - - - - + + {gradientDefs} {tooltipItems && } />} {lines.map((line, index) => ( - ))} - + {showAverage && averageValue !== null && ( + + )} + - + ); }; diff --git a/packages/web/context/useSystem.tsx b/packages/web/context/useSystem.tsx index f2e0c5c95..361ac5f4f 100644 --- a/packages/web/context/useSystem.tsx +++ b/packages/web/context/useSystem.tsx @@ -1,8 +1,7 @@ -import React, { type ReactNode, useMemo } from 'react'; +import React, { type ReactNode, useMemo, useEffect } from 'react'; import { createContext } from 'use-context-selector'; import { useMediaQuery } from '@chakra-ui/react'; import Cookies from 'js-cookie'; -import { useEffect } from 'react'; const CookieKey = 'NEXT_DEVICE_SIZE'; const setSize = (value: string) => { diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 35c4bf48b..db3482b78 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -1,6 +1,7 @@ { "Click_to_delete_this_field": "Click to delete this field", "Filed_is_deprecated": "This field is deprecated", + "Index": "Index", "MCP_tools_debug": "debug", "MCP_tools_detail": "check the details", "MCP_tools_list": "Tool list", @@ -10,8 +11,11 @@ "MCP_tools_url": "MCP Address", "MCP_tools_url_is_empty": "The MCP address cannot be empty", "MCP_tools_url_placeholder": "After filling in the MCP address, click Analysis", + "No_selected_dataset": "No selected dataset", "Role_setting": "Permission", "Run": "Execute", + "Search_dataset": "Search dataset", + "Selected": "Selected", "Team_Tags": "Team tags", "ai_point_price": "Billing", "ai_settings": "AI Configuration", @@ -54,13 +58,14 @@ "cron.every_month": "Run Monthly", "cron.every_week": "Run Weekly", "cron.interval": "Run at Intervals", + "dataset": "dataset", "dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.", "day": "Day", "deleted": "App deleted", "document_quote": "Document Reference", "document_quote_tip": "Usually used to accept user-uploaded document content (requires document parsing), and can also be used to reference other string data.", "document_upload": "Document Upload", - "edit_app": "Edit Application", + "edit_app": "Application details", "edit_info": "Edit", "execute_time": "Execution Time", "export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.", @@ -89,12 +94,26 @@ "llm_not_support_vision": "This model does not support image recognition", "llm_use_vision": "Vision", "llm_use_vision_tip": "After clicking on the model selection, you can see whether the model supports image recognition and the ability to control whether to start image recognition. \nAfter starting image recognition, the model will read the image content in the file link, and if the user question is less than 500 words, it will automatically parse the image in the user question.", + "log_chat_logs": "Dialogue log", + "log_detail": "Log details", + "logs_app_data": "Data board", + "logs_app_result": "Application effect", + "logs_average_response_time": "Average run time", + "logs_average_response_time_description": "Average of total workflow run time", + "logs_chat_count": "Number of sessions", + "logs_chat_count_description": "How many new sessions does this application create? \nSession definition: When the interval between the previous message exceeds 15 minutes, it is considered to be a new session (this definition only takes effect here)", + "logs_chat_data": "chat data", + "logs_chat_item_count": "Number of conversations", + "logs_chat_item_count_description": "How many conversations does this app generate? \nDialogue definition: The workflow runs once, and counts as a round of conversations", "logs_chat_user": "user", "logs_date": "date", "logs_empty": "No logs yet~", "logs_error_count": "Error Count", + "logs_error_rate": "Dialogue error ratio", + "logs_error_rate_description": "The proportion of the total number of dialogues reported in error", "logs_export_confirm_tip": "There are currently {{total}} conversation records, and each conversation can export up to 100 latest messages. \nConfirm export?", "logs_export_title": "Time, source, user, contact, title, total number of messages, user good feedback, user bad feedback, custom feedback, labeled answers, conversation details", + "logs_good_feedback": "Like", "logs_key_config": "Field Configuration", "logs_keys_annotatedCount": "Annotated Answer Count", "logs_keys_createdTime": "Created Time", @@ -110,11 +129,30 @@ "logs_keys_title": "Title", "logs_keys_user": "User", "logs_message_total": "Total Messages", + "logs_new_user_count": "New users", "logs_points": "Points Consumed", + "logs_points_description": "Points consumed by this application", + "logs_points_per_chat": "Average points consumption for a single session", + "logs_points_per_chat_description": "How many points are consumed on average for a workflow operation", "logs_response_time": "Average Response Time", "logs_search_chat": "Search for session title or session ID", "logs_source": "source", + "logs_source_count_description": "Number of users across channels", "logs_title": "Title", + "logs_total": "Grand total", + "logs_total_avg_points": "Average consumption", + "logs_total_chat": "Cumulative conversation count", + "logs_total_error": "{{count}} errors were reported in total, and the error rate was: {{rate}} %", + "logs_total_points": "Accumulated points consumption", + "logs_total_tips": "Cumulative indicators are not affected by time filtering", + "logs_total_users": "Cumulative number of users", + "logs_user_count": "Number of users", + "logs_user_count_description": "Number of people who have a conversation with the app in unit time", + "logs_user_data": "User data", + "logs_user_feedback": "User feedback", + "logs_user_feedback_description": "Like: Number of likes from users\n\nStep on: Users step on the number of points", + "logs_user_retention": "User retention", + "logs_user_retention_description": "Number of users who have added new users during the T cycle and are active in the T 1 cycle", "look_ai_point_price": "View all model billing standards", "manual_secret": "Manual secret", "mark_count": "Number of Marked Answers", @@ -142,14 +180,23 @@ "pdf_enhance_parse_tips": "Calling PDF recognition model for parsing, you can convert it into Markdown and retain pictures in the document. At the same time, you can also identify scanned documents, which will take a long time to identify them.", "permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.", "permission.des.read": "Use the app to have conversations", - "permission.des.write": "Can view and edit apps", "permission.des.readChatLog": "Can view chat logs", + "permission.des.write": "Can view and edit apps", + "permission.name.read": "Dialogue only", "permission.name.readChatLog": "View chat logs", "plugin.Instructions": "Instructions", "plugin_cost_by_token": "Charged based on token usage", + "plugin_cost_folder_tip": "This tool set contains subordinate tools, and the call points are determined based on the actual calling tool", "plugin_cost_per_times": "{{cost}} points/time", "plugin_dispatch": "Plugin Invocation", "plugin_dispatch_tip": "Adds extra capabilities to the model. The specific plugins to be invoked will be autonomously decided by the model.\nIf a plugin is selected, the Dataset invocation will automatically be treated as a special plugin.", + "pro_modal_feature_1": "External organization structure integration and multi-tenancy", + "pro_modal_feature_2": "Team-exclusive application showcase page", + "pro_modal_feature_3": "Knowledge base enhanced indexing", + "pro_modal_later_button": "Maybe Later", + "pro_modal_subtitle": "Join the business edition now to unlock more premium features", + "pro_modal_title": "Business Edition Exclusive!", + "pro_modal_unlock_button": "Unlock Now", "publish_channel": "Publish", "publish_success": "Publish Successful", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", @@ -203,9 +250,11 @@ "tool_active_manual_config_desc": "The temporary key is saved in this application and is only for use by this application.", "tool_active_system_config_desc": "Use the system configured key", "tool_active_system_config_price_desc": "Additional payment for key price ({{price}} points/time)", + "tool_active_system_config_price_desc_folder": "The additional key price is required, and the fee will be deducted based on the actual use of the tool.", "tool_detail": "Tool details", "tool_input_param_tip": "This plugin requires configuration of related information to run properly.", "tool_not_active": "This tool has not been activated yet", + "tool_run_free": "This tool runs without points consumption", "tool_type_communication": "Communication", "tool_type_design": "design", "tool_type_entertainment": "Business", @@ -248,6 +297,7 @@ "type.Workflow bot": "Workflow", "type.error.Workflow data is empty": "No workflow data was obtained", "type.error.workflowresponseempty": "Response content is empty", + "type.hidden": "Hide app", "type_not_recognized": "App type not recognized", "un_auth": "No permission", "upload_file_max_amount": "Maximum File Quantity", diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index 9e9ae2b26..5940a4fb5 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -39,6 +39,12 @@ "file_amount_over": "Exceeded maximum file quantity {{max}}", "file_input": "File input", "file_input_tip": "You can obtain the link to the corresponding file through the \"File Link\" of the [Plug-in Start] node", + "history_slider.home.title": "chat", + "home.chat_app": "HomeChat-{{name}}", + "home.chat_id": "Chat ID", + "home.no_available_tools": "No tools available", + "home.select_tools": "Select Tool", + "home.tools": "Tool: {{num}}", "in_progress": "In Progress", "input_guide": "Input Guide", "input_guide_lexicon": "Lexicon", @@ -77,6 +83,43 @@ "select_file": "Upload File", "select_file_img": "Upload file / image", "select_img": "Upload Image", + "setting.copyright.basic_configuration": "Basic configuration", + "setting.copyright.copyright_configuration": "Copyright configuration", + "setting.copyright.diagram": "Schematic diagram", + "setting.copyright.file_size_exceeds_limit": "File size exceeds the limit, maximum support for {{maxSize}}", + "setting.copyright.immediate_upload_required": "Immediate upload is required for this feature", + "setting.copyright.logo": "Logo", + "setting.copyright.preview_fail": "File preview failed", + "setting.copyright.save_fail": "Logo failed to save", + "setting.copyright.save_success": "Logo Saved successfully", + "setting.copyright.select_logo_image": "Please select the logo image to upload first", + "setting.copyright.style_diagram": "Style diagram", + "setting.copyright.tips": "Suggested ratio 4:1", + "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.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram_en.png", + "setting.home.available_tools.add": "Add", + "setting.home.commercial_version": "Commercial version", + "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.slogan": "Slogan", + "setting.home.slogan.default": "Hello 👋, I am FastGPT! Is there anything I can help you?", + "setting.home.slogan_placeholder": "Please enter Slogan", + "setting.home.title": "Home", + "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.home": "Home", + "sidebar.team_apps": "Team Apps", "source_cronJob": "Scheduled execution", "start_chat": "Start", "stream_output": "Stream Output", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index df71646a8..99b147311 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -64,7 +64,6 @@ "Parse": "Analysis", "Permission": "Permission", "Permission_tip": "Individual permissions are greater than group permissions", - "permission_other": "Other permissions (multiple)", "Preview": "Preview", "Remove": "Remove", "Rename": "Rename", @@ -129,11 +128,11 @@ "code_error.account_error": "Incorrect account name or password", "code_error.account_exist": "Account has been registered", "code_error.account_not_found": "User is not registered", + "code_error.app_error.can_not_edit_admin_permission": "Can not edit admin permission", "code_error.app_error.invalid_app_type": "Invalid Application Type", "code_error.app_error.invalid_owner": "Unauthorized Application Owner", "code_error.app_error.not_exist": "Application Does Not Exist", "code_error.app_error.un_auth_app": "Unauthorized to Operate This Application", - "code_error.app_error.can_not_edit_admin_permission": "Can not edit admin permission", "code_error.chat_error.un_auth": "Unauthorized to Operate This Chat Record", "code_error.error_code.400": "Request Failed", "code_error.error_code.401": "No Access Permission", @@ -478,6 +477,7 @@ "core.dataset.embedding model tip": "The index model can convert natural language into vectors for semantic search.\nNote that different index models cannot be used together. Once an index model is selected, it cannot be changed.", "core.dataset.error.Data not found": "Data Not Found or Deleted", "core.dataset.error.Start Sync Failed": "Failed to Start Sync", + "core.dataset.error.canNotEditAdminPermission": "You cannot edit the admin permission", "core.dataset.error.invalidVectorModelOrQAModel": "Invalid Vector Model or QA Model", "core.dataset.error.unAuthDataset": "Unauthorized to Operate This Dataset", "core.dataset.error.unAuthDatasetCollection": "Unauthorized to Operate This Dataset", @@ -486,7 +486,6 @@ "core.dataset.error.unCreateCollection": "Unauthorized to Operate This Data", "core.dataset.error.unExistDataset": "The knowledge base does not exist", "core.dataset.error.unLinkCollection": "Not a Web Link Collection", - "core.dataset.error.canNotEditAdminPermission": "You cannot edit the admin permission", "core.dataset.externalFile": "External File Library", "core.dataset.file": "File", "core.dataset.folder": "Directory", @@ -670,6 +669,7 @@ "core.module.template.UnKnow Module": "Unknown Module", "core.module.template.ai_chat": "AI conversation", "core.module.template.ai_chat_intro": "AI large model dialogue", + "core.module.template.all_team_app": "All", "core.module.template.config_params": "Can configure application system parameters", "core.module.template.empty_plugin": "Blank plugin", "core.module.template.empty_workflow": "Blank workflow", @@ -693,7 +693,6 @@ "core.module.variable.variable option is value is required": "Option Content Cannot Be Empty", "core.module.variable.variable options": "Options", "core.plugin.Custom headers": "Custom Request Headers", - "core.plugin.Free": "This plugin does not consume points", "core.plugin.Get Plugin Module Detail Failed": "Failed to Retrieve Plugin Information", "core.plugin.Http plugin intro placeholder": "For display only, no actual effect", "core.plugin.cost": "Points Consumption:", @@ -981,6 +980,7 @@ "permission.manager": "administrator", "permission.read": "Read permission", "permission.write": "write permission", + "permission_other": "Other permissions (multiple)", "please_input_name": "Please Enter a Name", "plugin.App": "Select App", "plugin.Currentapp": "Current App", @@ -998,7 +998,6 @@ "plugin.Path": "Path", "plugin.Please bind laf accout first": "Please Bind Laf Account First", "plugin.Plugin List": "Plugin List", - "plugin.Search plugin": "Search Plugin", "plugin.Search_app": "Search App", "plugin.Set Name": "Name the Plugin", "plugin.contribute": "Contribute Plugin", @@ -1019,10 +1018,11 @@ "required": "Required", "rerank_weight": "Rearrange weights", "resume_failed": "Resume Failed", - "root_folder": "Root Folder", + "root_folder": "Root", "save_failed": "save_failed", "save_success": "Saved Successfully", "scan_code": "Scan the QR code to pay", + "search_tool": "Search Tools", "secret_key": "Secret", "secret_tips": "The value will not return plaintext again after saving", "select_file_failed": "File Selection Failed", @@ -1079,11 +1079,11 @@ "support.user.info.verification_code": "Verification Code", "support.user.inform.System message": "System Message", "support.user.login.Email": "Email", - "support.user.login.Github": "GitHub Login", - "support.user.login.Google": "Google Login", - "support.user.login.Microsoft": "Microsoft Login", + "support.user.login.Github": "GitHub", + "support.user.login.Google": "Google", + "support.user.login.Microsoft": "Microsoft", "support.user.login.Password": "Password", - "support.user.login.Password login": "Password Login", + "support.user.login.Password login": "Password", "support.user.login.Phone": "Phone Login", "support.user.login.Phone number": "Phone Number", "support.user.login.Provider error": "Login Error, Please Try Again", @@ -1250,6 +1250,7 @@ "unusable_variable": "No Usable Variables", "update_failed": "Update Failed", "update_success": "Updated Successfully", + "upgrade": "upgrade", "upload_file": "Upload File", "upload_file_error": "File Upload Failed", "use_helper": "Use Helper", diff --git a/packages/web/i18n/en/file.json b/packages/web/i18n/en/file.json index b2946e2f3..d53d0a88c 100644 --- a/packages/web/i18n/en/file.json +++ b/packages/web/i18n/en/file.json @@ -15,6 +15,7 @@ "Please wait for all files to upload": "Please wait for all files to be uploaded to complete", "bucket_chat": "Conversation Files", "bucket_file": "Dataset Documents", + "bucket_image": "picture", "click_to_view_raw_source": "Click to View Original Source", "common.Some images failed to process": "Some images failed to process", "common.dataset_data_input_image_support_format": "Support .jpg, .jpeg, .png, .gif, .webp formats", diff --git a/packages/web/i18n/en/login.json b/packages/web/i18n/en/login.json index 7fee938ec..5041732eb 100644 --- a/packages/web/i18n/en/login.json +++ b/packages/web/i18n/en/login.json @@ -9,7 +9,7 @@ "no_remind": "Don't remind again", "password_condition": "Password maximum 60 characters", "password_tip": "Password must be at least 8 characters long and contain at least two combinations: numbers, letters, or special characters", - "policy_tip": "By using this service, you agree to our", + "policy_tip": "By using it, you have read and agree to\n
our Terms & Privacy Policy
", "privacy": "Privacy Policy", "privacy_policy": "Privacy Policy", "redirect": "Jump", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 30f5dd3d1..748c0f4f0 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -1,6 +1,7 @@ { "Click_to_delete_this_field": "点击删除该字段", "Filed_is_deprecated": "该字段已弃用", + "Index": "索引", "MCP_tools_debug": "调试", "MCP_tools_detail": "查看详情", "MCP_tools_list": "工具列表", @@ -10,8 +11,11 @@ "MCP_tools_url": "MCP 地址", "MCP_tools_url_is_empty": "MCP 地址不能为空", "MCP_tools_url_placeholder": "填入 MCP 地址后,点击解析", + "No_selected_dataset": "未选择知识库", "Role_setting": "权限设置", "Run": "运行", + "Search_dataset": "搜索知识库", + "Selected": "已选择", "Team_Tags": "团队标签", "ai_point_price": "AI积分计费", "ai_settings": "AI 配置", @@ -54,13 +58,14 @@ "cron.every_month": "每月执行", "cron.every_week": "每周执行", "cron.interval": "间隔执行", + "dataset": "知识库", "dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。", "day": "日", "deleted": "应用已删除", "document_quote": "文档引用", "document_quote_tip": "通常用于接受用户上传的文档内容(这需要文档解析),也可以用于引用其他字符串数据。", "document_upload": "文档上传", - "edit_app": "编辑应用", + "edit_app": "应用详情", "edit_info": "编辑信息", "execute_time": "执行时间", "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", @@ -89,12 +94,27 @@ "llm_not_support_vision": "该模型不支持图片识别", "llm_use_vision": "图片识别", "llm_use_vision_tip": "点击模型选择后,可以看到模型是否支持图片识别以及控制是否启动图片识别的能力。启动图片识别后,模型会读取文件链接里图片内容,并且如果用户问题少于 500 字,会自动解析用户问题中的图片。", + "log_chat_logs": "对话日志", + "log_detail": "日志详情", + "logs_app_data": "数据看板", + "logs_app_result": "应用效果", + "logs_average_response_time": "平均运行时长(s)", + "logs_average_response_time_description": "工作流总运行时间的平均值", + "logs_bad_feedback": "点踩", + "logs_chat_count": "会话次数", + "logs_chat_count_description": "该应用共新建多少个会话。 会话定义:当与上条消息间隔超过15min,认为是产生新会话(该定义仅在此生效)", + "logs_chat_data": "对话数据", + "logs_chat_item_count": "对话次数", + "logs_chat_item_count_description": "该应用共产生多少次对话。 对话定义:工作流运行一次,算一轮对话", "logs_chat_user": "使用者", "logs_date": "日期", "logs_empty": "还没有日志噢~", "logs_error_count": "报错数量", + "logs_error_rate": "对话报错比例", + "logs_error_rate_description": "报错对话占总对话数量的比例", "logs_export_confirm_tip": "当前共有 {{total}} 条对话记录,每条对话最多可导出最新 100 条消息。确认导出?", "logs_export_title": "时间,来源,使用者,联系方式,标题,消息总数,用户赞同反馈,用户反对反馈,自定义反馈,标注答案,对话详情", + "logs_good_feedback": "点赞", "logs_key_config": "字段配置", "logs_keys_annotatedCount": "标注答案数量", "logs_keys_createdTime": "创建时间", @@ -110,11 +130,38 @@ "logs_keys_title": "标题", "logs_keys_user": "使用者", "logs_message_total": "消息总数", + "logs_new_user_count": "新增用户", "logs_points": "积分消耗", + "logs_points_description": "该应用消耗的积分", + "logs_points_per_chat": "单次会话平均积分消耗", + "logs_points_per_chat_description": "工作流运行一次平均消耗多少积分", "logs_response_time": "平均响应时长", "logs_search_chat": "搜索会话标题或会话 ID", "logs_source": "来源", + "logs_source_count": "渠道用户", + "logs_source_count_description": "各渠道用户的数量", + "logs_timespan_day": "按日", + "logs_timespan_month": "按月", + "logs_timespan_quarter": "按季", + "logs_timespan_week": "按周", "logs_title": "标题", + "logs_total": "累计", + "logs_total_avg_duration": "平均时长", + "logs_total_avg_points": "平均消耗", + "logs_total_chat": "累计对话数", + "logs_total_error": "共 {{count}} 次报错,报错率: {{rate}} %", + "logs_total_feedback": "共 {{goodFeedBack}} 赞 | 共 {{badFeedBack}} 踩", + "logs_total_points": "累计积分消耗", + "logs_total_tips": "累计指标不受时间筛选影响", + "logs_total_users": "累计用户数", + "logs_user_callback": "用户反馈", + "logs_user_count": "用户数", + "logs_user_count_description": "单位时间内与该应用产生对话的人数", + "logs_user_data": "用户数据", + "logs_user_feedback": "用户反馈", + "logs_user_feedback_description": "赞:用户点赞数量 \n踩:用户点踩数量", + "logs_user_retention": "用户留存", + "logs_user_retention_description": "T周期新增用户且在T+1周期活跃的用户数", "look_ai_point_price": "查看所有模型计费标准", "manual_secret": "临时密钥", "mark_count": "标注答案数量", @@ -142,14 +189,23 @@ "pdf_enhance_parse_tips": "调用 PDF 识别模型进行解析,可以将其转换成 Markdown 并保留文档中的图片,同时也可以对扫描件进行识别,识别时间较长。", "permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限", "permission.des.read": "可使用该应用进行对话", - "permission.des.write": "可查看和编辑应用", "permission.des.readChatLog": "可查看对话日志", + "permission.des.write": "可查看和编辑应用", + "permission.name.read": "仅对话", "permission.name.readChatLog": "查看对话日志", "plugin.Instructions": "使用说明", "plugin_cost_by_token": "依据 token 消耗计费", + "plugin_cost_folder_tip": "该工具集包含下属工具,调用积分依据实际调用工具决定", "plugin_cost_per_times": "{{cost}} 积分/次", "plugin_dispatch": "插件调用", "plugin_dispatch_tip": "给模型附加获取外部数据的能力,具体调用哪些插件,将由模型自主决定,所有插件都将以非流模式运行。\n若选择了插件,知识库调用将自动作为一个特殊的插件。", + "pro_modal_feature_1": "外部组织架构接入与多租户", + "pro_modal_feature_2": "团队专属的应用展示页", + "pro_modal_feature_3": "知识库增强索引", + "pro_modal_later_button": "我再想想", + "pro_modal_subtitle": "即刻加入商业版,解锁更多高级功能", + "pro_modal_title": "商业版专享!", + "pro_modal_unlock_button": "去解锁", "publish_channel": "发布渠道", "publish_success": "发布成功", "question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。", @@ -203,9 +259,11 @@ "tool_active_manual_config_desc": "临时密钥保存在本应用中,仅供该应用使用", "tool_active_system_config_desc": "使用系统已配置好的密钥", "tool_active_system_config_price_desc": "需额外支付密钥价格( {{price}} 积分/次)", + "tool_active_system_config_price_desc_folder": "需额外支付密钥价格,依据实际使用工具扣费。", "tool_detail": "工具详情", "tool_input_param_tip": "该插件正常运行需要配置相关信息", "tool_not_active": "该工具尚未激活", + "tool_run_free": "该工具运行无积分消耗", "tool_type_communication": "通讯", "tool_type_design": "设计", "tool_type_entertainment": "商业", @@ -248,6 +306,7 @@ "type.Workflow bot": "工作流", "type.error.Workflow data is empty": "没有获取到工作流数据", "type.error.workflowresponseempty": "响应内容为空", + "type.hidden": "隐藏应用", "type_not_recognized": "未识别到应用类型", "un_auth": "无权限", "upload_file_max_amount": "最大文件数量", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index 254b4551f..96de827ca 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -39,6 +39,11 @@ "file_amount_over": "超出最大文件数量 {{max}}", "file_input": "系统文件", "file_input_tip": "可通过【插件开始】节点的“文件链接”获取对应文件的链接", + "history_slider.home.title": "聊天", + "home.chat_app": "首页聊天-{{name}}", + "home.no_available_tools": "暂无可用工具", + "home.select_tools": "选择工具", + "home.tools": "工具:{{num}}", "in_progress": "进行中", "input_guide": "输入引导", "input_guide_lexicon": "词库", @@ -77,6 +82,46 @@ "select_file": "上传文件", "select_file_img": "上传文件/图片", "select_img": "上传图片", + "setting.copyright.basic_configuration": "基础配置", + "setting.copyright.copyright_configuration": "版权配置", + "setting.copyright.diagram": "示意图", + "setting.copyright.file_size_exceeds_limit": "文件大小超出限制,最大支持 {{maxSize}}", + "setting.copyright.immediate_upload_required": "此功能需要立即上传", + "setting.copyright.logo": "Logo", + "setting.copyright.preview_fail": "文件预览失败", + "setting.copyright.save_fail": "Logo 保存失败", + "setting.copyright.save_success": "Logo 保存成功", + "setting.copyright.select_logo_image": "请先选择要上传的 Logo 图片", + "setting.copyright.style_diagram": "样式示意图", + "setting.copyright.tips": "建议比例 4:1", + "setting.copyright.tips.square": "建议比例 1:1", + "setting.copyright.title": "版权信息", + "setting.copyright.upload_fail": "文件上传失败", + "setting.data_dashboard.title": "数据看板", + "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram.png", + "setting.home.available_tools": "可用工具", + "setting.home.available_tools.add": "添加", + "setting.home.commercial_version": "商业版", + "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.slogan": "Slogan", + "setting.home.slogan.default": "你好👋,我是 FastGPT ! 请问有什么可以帮你?", + "setting.home.slogan_placeholder": "请输入 Slogan", + "setting.home.title": "首页配置", + "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.home": "首页", + "sidebar.team_apps": "团队应用", "source_cronJob": "定时执行", "start_chat": "开始对话", "stream_output": "流输出", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index bdc852e7d..43a6e47a1 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -64,7 +64,6 @@ "Parse": "解析", "Permission": "权限", "Permission_tip": "个人权限大于群组权限", - "permission_other": "其他权限(多选)", "Preview": "预览", "Remove": "移除", "Rename": "重命名", @@ -129,12 +128,12 @@ "code_error.account_error": "账号名或密码错误", "code_error.account_exist": "账号已注册", "code_error.account_not_found": "用户未注册", + "code_error.app_error.can_not_edit_admin_permission": "不能编辑管理员权限", "code_error.app_error.invalid_app_type": "错误的应用类型", "code_error.app_error.invalid_owner": "非法的应用所有者", "code_error.app_error.not_exist": "应用不存在", "code_error.app_error.un_auth_app": "无权操作该应用", "code_error.chat_error.un_auth": "没有权限操作此对话记录", - "code_error.app_error.can_not_edit_admin_permission": "不能编辑管理员权限", "code_error.error_code.400": "请求失败", "code_error.error_code.401": "无访问权限", "code_error.error_code.403": "紧张访问", @@ -343,7 +342,7 @@ "core.chat.Feedback Submit": "提交反馈", "core.chat.Feedback Success": "反馈成功!", "core.chat.Finish Speak": "语音输入完成", - "core.chat.History": "历史记录", + "core.chat.History": "记录", "core.chat.History Amount": "{{amount}} 条记录", "core.chat.Mark": "标注预期回答", "core.chat.Mark Description": "当前标注功能为测试版。\n\n点击添加标注后,需要选择一个知识库,以便存储标注数据。你可以通过该功能快速的标注问题和预期回答,以便引导模型下次的回答。\n\n目前,标注功能同知识库其他数据一样,受模型的影响,不代表标注后 100% 符合预期。\n\n标注数据仅单向与知识库同步,如果知识库修改了该标注数据,日志展示的标注数据无法同步。", @@ -478,6 +477,7 @@ "core.dataset.embedding model tip": "索引模型可以将自然语言转成向量,用于进行语义检索。\n注意,不同索引模型无法一起使用,选择完索引模型后将无法修改。", "core.dataset.error.Data not found": "数据不存在或已被删除", "core.dataset.error.Start Sync Failed": "开始同步失败", + "core.dataset.error.canNotEditAdminPermission": "无法修改管理员权限", "core.dataset.error.invalidVectorModelOrQAModel": "VectorModel 或 QA 模型错误", "core.dataset.error.unAuthDataset": "无权操作该知识库", "core.dataset.error.unAuthDatasetCollection": "无权操作该数据集", @@ -486,7 +486,6 @@ "core.dataset.error.unCreateCollection": "无权操作该数据", "core.dataset.error.unExistDataset": "知识库不存在", "core.dataset.error.unLinkCollection": "不是网络链接集合", - "core.dataset.error.canNotEditAdminPermission": "无法修改管理员权限", "core.dataset.externalFile": "外部文件库", "core.dataset.file": "文件", "core.dataset.folder": "目录", @@ -667,6 +666,7 @@ "core.module.template.System Plugin": "系统插件", "core.module.template.System input module": "系统输入", "core.module.template.Team app": "团队应用", + "core.module.template.all_team_app": "全部", "core.module.template.UnKnow Module": "未知模块", "core.module.template.ai_chat": "AI 对话", "core.module.template.ai_chat_intro": "AI 大模型对话", @@ -693,7 +693,6 @@ "core.module.variable.variable option is value is required": "选项内容不能为空", "core.module.variable.variable options": "选项", "core.plugin.Custom headers": "自定义请求头", - "core.plugin.Free": "该插件无需积分消耗~", "core.plugin.Get Plugin Module Detail Failed": "加载插件异常", "core.plugin.Http plugin intro placeholder": "仅做展示,无实际效果", "core.plugin.cost": "积分消耗:", @@ -981,6 +980,7 @@ "permission.manager": "管理员", "permission.read": "读权限", "permission.write": "写权限", + "permission_other": "其他权限(多选)", "please_input_name": "请输入名称", "plugin.App": "选择应用", "plugin.Currentapp": "当前应用", @@ -998,7 +998,6 @@ "plugin.Path": "路径", "plugin.Please bind laf accout first": "请先绑定 laf 账号", "plugin.Plugin List": "插件列表", - "plugin.Search plugin": "搜索插件", "plugin.Search_app": "搜索应用", "plugin.Set Name": "给插件取个名字", "plugin.contribute": "贡献插件", @@ -1023,6 +1022,7 @@ "save_failed": "保存异常", "save_success": "保存成功", "scan_code": "扫码支付", + "search_tool": "搜索工具", "secret_key": "密钥", "secret_tips": "值保存后不会再次明文返回", "select_file_failed": "选择文件异常", @@ -1251,6 +1251,7 @@ "unusable_variable": "无可用变量", "update_failed": "更新异常", "update_success": "更新成功", + "upgrade": "升级", "upload_file": "上传文件", "upload_file_error": "上传文件失败", "use_helper": "使用帮助", diff --git a/packages/web/i18n/zh-CN/file.json b/packages/web/i18n/zh-CN/file.json index 2e8b76ad1..bfd7df19c 100644 --- a/packages/web/i18n/zh-CN/file.json +++ b/packages/web/i18n/zh-CN/file.json @@ -15,6 +15,7 @@ "Please wait for all files to upload": "请等待所有文件上传完成", "bucket_chat": "对话文件", "bucket_file": "知识库文件", + "bucket_image": "图片", "click_to_view_raw_source": "点击查看来源", "common.Some images failed to process": "部分图片处理失败", "common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式", diff --git a/packages/web/i18n/zh-CN/login.json b/packages/web/i18n/zh-CN/login.json index 51816a916..25e3e1773 100644 --- a/packages/web/i18n/zh-CN/login.json +++ b/packages/web/i18n/zh-CN/login.json @@ -9,7 +9,7 @@ "no_remind": "不再提醒", "password_condition": "密码最多 60 位", "password_tip": "密码至少 8 位,且至少包含两种组合:数字、字母或特殊字符", - "policy_tip": "使用即代表你同意我们的", + "policy_tip": "使用即代表您已阅读并同意 服务协议 隐私协议", "privacy": "隐私协议", "privacy_policy": "隐私政策", "redirect": "跳转", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 525fe5741..834edbbdc 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -1,6 +1,7 @@ { "Click_to_delete_this_field": "點擊刪除該字段", "Filed_is_deprecated": "該字段已棄用", + "Index": "索引", "MCP_tools_debug": "偵錯", "MCP_tools_detail": "查看詳情", "MCP_tools_list": "工具列表", @@ -10,8 +11,11 @@ "MCP_tools_url": "MCP 地址", "MCP_tools_url_is_empty": "MCP 地址不能為空", "MCP_tools_url_placeholder": "填入 MCP 地址後,點擊解析", + "No_selected_dataset": "未選擇知識庫", "Role_setting": "權限設定", "Run": "執行", + "Search_dataset": "搜尋知識庫", + "Selected": "已選擇", "Team_Tags": "團隊標籤", "ai_point_price": "AI 積分計費", "ai_settings": "AI 設定", @@ -54,13 +58,14 @@ "cron.every_month": "每月執行", "cron.every_week": "每週執行", "cron.interval": "間隔執行", + "dataset": "知識庫", "dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。", "day": "日", "deleted": "應用已刪除", "document_quote": "文件引用", "document_quote_tip": "通常用於接受使用者上傳的文件內容(這需要文件解析),也可以用於引用其他字串資料。", "document_upload": "文件上傳", - "edit_app": "編輯應用程式", + "edit_app": "應用詳情", "edit_info": "編輯資訊", "execute_time": "執行時間", "export_config_successful": "已複製設定,自動過濾部分敏感資訊,請注意檢查是否仍有敏感資料", @@ -89,12 +94,26 @@ "llm_not_support_vision": "這個模型不支援圖片辨識", "llm_use_vision": "圖片辨識", "llm_use_vision_tip": "點選模型選擇後,可以看到模型是否支援圖片辨識以及控制是否啟用圖片辨識的功能。啟用圖片辨識後,模型會讀取檔案連結中的圖片內容,並且如果使用者問題少於 500 字,會自動解析使用者問題中的圖片。", + "log_chat_logs": "對話日誌", + "log_detail": "日誌詳情", + "logs_app_data": "數據看板", + "logs_app_result": "應用效果", + "logs_average_response_time": "平均運行時長", + "logs_average_response_time_description": "工作流總運行時間的平均值", + "logs_chat_count": "會話次數", + "logs_chat_count_description": "該應用共新建多少個會話。 \n會話定義:當與上條消息間隔超過15min,認為是產生新會話(該定義僅在此生效)", + "logs_chat_data": "對話數據", + "logs_chat_item_count": "對話次數", + "logs_chat_item_count_description": "該應用共產生多少次對話。 \n對話定義:工作流運行一次,算一輪對話", "logs_chat_user": "使用者", "logs_date": "日期", "logs_empty": "還沒有紀錄喔~", "logs_error_count": "錯誤數量", + "logs_error_rate": "對話報錯比例", + "logs_error_rate_description": "報錯對話佔總對話數量的比例", "logs_export_confirm_tip": "當前共有 {{total}} 條對話記錄,每條對話最多可導出最新 100 條消息。\n確認導出?", "logs_export_title": "時間,來源,使用者,聯絡方式,標題,訊息總數,使用者贊同回饋,使用者反對回饋,自定義回饋,標註答案,對話詳細資訊", + "logs_good_feedback": "點贊", "logs_key_config": "字段配置", "logs_keys_annotatedCount": "標記答案數量", "logs_keys_createdTime": "建立時間", @@ -110,11 +129,30 @@ "logs_keys_title": "標題", "logs_keys_user": "使用者", "logs_message_total": "訊息總數", + "logs_new_user_count": "新增用戶", "logs_points": "積分消耗", + "logs_points_description": "該應用消耗的積分", + "logs_points_per_chat": "單次會話平均積分消耗", + "logs_points_per_chat_description": "工作流運行一次平均消耗多少積分", "logs_response_time": "平均回應時長", "logs_search_chat": "搜索會話標題或會話 ID", "logs_source": "來源", + "logs_source_count_description": "各渠道用戶的數量", "logs_title": "標題", + "logs_total": "累計", + "logs_total_avg_points": "平均消耗", + "logs_total_chat": "累計對話數", + "logs_total_error": "共 {{count}} 次報錯,報錯率: {{rate}} %", + "logs_total_points": "累計積分消耗", + "logs_total_tips": "累計指標不受時間篩選影響", + "logs_total_users": "累計用戶數", + "logs_user_count": "用戶數", + "logs_user_count_description": "單位時間內與該應用產生對話的人數", + "logs_user_data": "用戶數據", + "logs_user_feedback": "用戶反饋", + "logs_user_feedback_description": "贊:用戶點贊數量 \n踩:用戶點踩數量", + "logs_user_retention": "用戶留存", + "logs_user_retention_description": "T週期新增用戶且在T 1週期活躍的用戶數", "look_ai_point_price": "檢視所有模型計費標準", "manual_secret": "臨時密鑰", "mark_count": "標記答案數量", @@ -142,14 +180,23 @@ "pdf_enhance_parse_tips": "呼叫 PDF 識別模型進行解析,可以將其轉換成 Markdown 並保留文件中的圖片,同時也可以對掃描件進行識別,識別時間較長。", "permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限", "permission.des.read": "可以使用這個應用程式進行對話", - "permission.des.write": "可以檢視和編輯應用程式", "permission.des.readChatLog": "可以檢視對話紀錄", + "permission.des.write": "可以檢視和編輯應用程式", + "permission.name.read": "僅對話", "permission.name.readChatLog": "檢視對話紀錄", "plugin.Instructions": "使用說明", "plugin_cost_by_token": "根據 token 消耗計費", + "plugin_cost_folder_tip": "該工具集包含下屬工具,調用積分依據實際調用工具決定", "plugin_cost_per_times": "{{cost}} 積分/次", "plugin_dispatch": "外掛呼叫", "plugin_dispatch_tip": "賦予模型取得外部資料的能力,具體呼叫哪些外掛,將由模型自主決定,所有外掛都將以非串流模式執行。\n若選擇了外掛,知識庫呼叫將自動作為一個特殊的外掛。", + "pro_modal_feature_1": "外部組織架構接入與多租戶", + "pro_modal_feature_2": "團隊專屬的應用展示頁", + "pro_modal_feature_3": "知識庫增強索引", + "pro_modal_later_button": "我再想想", + "pro_modal_subtitle": "即刻加入商業版,解鎖更多高級功能", + "pro_modal_title": "商業版專享!", + "pro_modal_unlock_button": "去解鎖", "publish_channel": "發布通道", "publish_success": "發布成功", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", @@ -203,9 +250,11 @@ "tool_active_manual_config_desc": "臨時密鑰保存在本應用中,僅供該應用使用", "tool_active_system_config_desc": "使用系統已配置好的密鑰", "tool_active_system_config_price_desc": "需額外支付密鑰價格( {{price}} 積分/次)", + "tool_active_system_config_price_desc_folder": "需額外支付密鑰價格,依據實際使用工具扣費。", "tool_detail": "工具詳情", "tool_input_param_tip": "這個外掛正常執行需要設定相關資訊", "tool_not_active": "該工具尚未激活", + "tool_run_free": "該工具運行無積分消耗", "tool_type_communication": "通訊", "tool_type_design": "設計", "tool_type_entertainment": "商業", @@ -248,6 +297,7 @@ "type.Workflow bot": "工作流程", "type.error.Workflow data is empty": "沒有獲取到工作流數據", "type.error.workflowresponseempty": "響應內容為空", + "type.hidden": "隱藏應用", "type_not_recognized": "未識別到應用程式類型", "un_auth": "無權限", "upload_file_max_amount": "最大檔案數量", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index 5dd49f08e..31c36787e 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -39,6 +39,12 @@ "file_amount_over": "超出檔案數量上限 {{max}}", "file_input": "檔案輸入", "file_input_tip": "可透過「外掛程式啟動」節點的「檔案連結」取得對應檔案的連結", + "history_slider.home.title": "聊天", + "home.chat_app": "首页聊天-{{name}}", + "home.chat_id": "會話ID", + "home.no_available_tools": "暫無可用工具", + "home.select_tools": "選擇工具", + "home.tools": "工具:{{num}}", "in_progress": "進行中", "input_guide": "輸入導引", "input_guide_lexicon": "詞彙庫", @@ -77,6 +83,42 @@ "select_file": "上傳檔案", "select_file_img": "上傳檔案 / 圖片", "select_img": "上傳圖片", + "setting.copyright.basic_configuration": "基礎配置", + "setting.copyright.copyright_configuration": "版權配置", + "setting.copyright.diagram": "示意圖", + "setting.copyright.file_size_exceeds_limit": "文件大小超出限制,最大支持 {{maxSize}}", + "setting.copyright.immediate_upload_required": "此功能需要立即上傳", + "setting.copyright.logo": "Logo", + "setting.copyright.preview_fail": "文件預覽失敗", + "setting.copyright.save_fail": "Logo 保存失敗", + "setting.copyright.select_logo_image": "請先選擇要上傳的 Logo 圖片", + "setting.copyright.style_diagram": "樣式示意圖", + "setting.copyright.tips": "建議比例 4:1", + "setting.copyright.tips.square": "建議比例 1:1", + "setting.copyright.title": "版權信息", + "setting.copyright.upload_fail": "文件上傳失敗", + "setting.data_dashboard.title": "數據看板", + "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram_zh-Hant.png", + "setting.home.available_tools.add": "添加", + "setting.home.commercial_version": "商業版", + "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.slogan": "Slogan", + "setting.home.slogan.default": "你好👋,我是 FastGPT ! 請問有什麼可以幫你?", + "setting.home.slogan_placeholder": "請輸入 Slogan", + "setting.home.title": "首頁配置", + "setting.incorrect_plan": "當前套餐不支持該功能,請升級訂閱套餐", + "setting.incorrect_version": "當前版本不支持該功能", + "setting.log_details.title": "首頁日誌", + "setting.logs.title": "首頁日誌", + "setting.save": "保存", + "setting.save_success": "保存成功", + "sidebar.home": "首頁", + "sidebar.team_apps": "團隊應用", "source_cronJob": "定時執行", "start_chat": "開始對話", "stream_output": "串流輸出", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 6f2b574d8..70a0f591c 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -64,7 +64,6 @@ "Parse": "解析", "Permission": "權限", "Permission_tip": "個人權限大於群組權限", - "permission_other": "其他權限(多選)", "Preview": "預覽", "Remove": "移除", "Rename": "重新命名", @@ -129,12 +128,12 @@ "code_error.account_error": "帳號名稱或密碼錯誤", "code_error.account_exist": "賬號已註冊", "code_error.account_not_found": "使用者未註冊", + "code_error.app_error.can_not_edit_admin_permission": "不能編輯管理員權限", "code_error.app_error.invalid_app_type": "無效的應用程式類型", "code_error.app_error.invalid_owner": "非法的應用程式擁有者", "code_error.app_error.not_exist": "應用程式不存在", "code_error.app_error.un_auth_app": "無權操作此應用程式", "code_error.chat_error.un_auth": "沒有權限操作此對話記錄", - "code_error.app_error.can_not_edit_admin_permission": "不能編輯管理員權限", "code_error.error_code.400": "請求失敗", "code_error.error_code.401": "無存取權限", "code_error.error_code.403": "禁止存取", @@ -343,7 +342,7 @@ "core.chat.Feedback Submit": "送出回饋", "core.chat.Feedback Success": "回饋成功!", "core.chat.Finish Speak": "語音輸入完成", - "core.chat.History": "歷史記錄", + "core.chat.History": "記錄", "core.chat.History Amount": "{{amount}} 筆記錄", "core.chat.Mark": "標記預期回答", "core.chat.Mark Description": "目前標記功能為測試版。\n\n點選新增標記後,需要選擇一個知識庫來儲存標記資料。您可以透過此功能快速標記問題和預期回答,以引導模型下次的回答。\n\n目前,標記功能與知識庫中的其他資料一樣,會受到模型的影響,不保證標記後一定 100% 符合預期。\n\n標記資料僅單向與知識庫同步。如果知識庫修改了標記資料,日誌中顯示的標記資料將無法同步。", @@ -424,7 +423,6 @@ "core.chat.response.text output": "文字輸出", "core.chat.response.update_var_result": "變數更新結果(依序顯示多個變數更新結果)", "core.chat.response.user_select_result": "使用者選擇結果", - "core.chat.retry": "重新產生", "core.chat.tts.Stop Speech": "停止", "core.dataset.Choose Dataset": "關聯知識庫", "core.dataset.Collection": "資料集", @@ -478,6 +476,7 @@ "core.dataset.embedding model tip": "索引模型可以將自然語言轉換成向量,用於進行語意搜尋。\n注意,不同索引模型無法一起使用。選擇索引模型後就無法修改。", "core.dataset.error.Data not found": "資料不存在或已被刪除", "core.dataset.error.Start Sync Failed": "開始同步失敗", + "core.dataset.error.canNotEditAdminPermission": "無法修改管理員權限", "core.dataset.error.invalidVectorModelOrQAModel": "向量模型或問答模型錯誤", "core.dataset.error.unAuthDataset": "無權操作此知識庫", "core.dataset.error.unAuthDatasetCollection": "無權操作此資料集", @@ -486,7 +485,6 @@ "core.dataset.error.unCreateCollection": "無權操作此資料", "core.dataset.error.unExistDataset": "知識庫不存在", "core.dataset.error.unLinkCollection": "不是網路連結集合", - "core.dataset.error.canNotEditAdminPermission": "無法修改管理員權限", "core.dataset.externalFile": "外部檔案庫", "core.dataset.file": "檔案", "core.dataset.folder": "目錄", @@ -670,6 +668,7 @@ "core.module.template.UnKnow Module": "未知模組", "core.module.template.ai_chat": "AI 對話", "core.module.template.ai_chat_intro": "AI 大型模型對話", + "core.module.template.all_team_app": "全部", "core.module.template.config_params": "可以設定應用程式的系統參數", "core.module.template.empty_plugin": "空白外掛程式", "core.module.template.empty_workflow": "空白工作流程", @@ -693,7 +692,6 @@ "core.module.variable.variable option is value is required": "選項內容不能為空", "core.module.variable.variable options": "選項", "core.plugin.Custom headers": "自訂請求標頭", - "core.plugin.Free": "此外掛程式不需消耗點數", "core.plugin.Get Plugin Module Detail Failed": "取得外掛程式資訊失敗", "core.plugin.Http plugin intro placeholder": "僅供展示,無實際效果", "core.plugin.cost": "點數消耗:", @@ -981,6 +979,7 @@ "permission.manager": "管理員", "permission.read": "讀取權限", "permission.write": "寫入權限", + "permission_other": "其他權限(多選)", "please_input_name": "請輸入名稱", "plugin.App": "選擇應用程式", "plugin.Currentapp": "目前應用程式", @@ -998,7 +997,6 @@ "plugin.Path": "路徑", "plugin.Please bind laf accout first": "請先綁定 LAF 帳戶", "plugin.Plugin List": "外掛程式列表", - "plugin.Search plugin": "搜尋外掛程式", "plugin.Search_app": "搜尋應用程式", "plugin.Set Name": "為外掛程式命名", "plugin.contribute": "貢獻外掛程式", @@ -1023,6 +1021,7 @@ "save_failed": "儲存失敗", "save_success": "儲存成功", "scan_code": "掃碼支付", + "search_tool": "搜索工具", "secret_key": "密鑰", "secret_tips": "值保存後不會再次明文返回", "select_file_failed": "選擇檔案失敗", @@ -1250,6 +1249,7 @@ "unusable_variable": "無可用變數", "update_failed": "更新失敗", "update_success": "更新成功", + "upgrade": "升級", "upload_file": "上傳檔案", "upload_file_error": "上傳檔案失敗", "use_helper": "使用說明", diff --git a/packages/web/i18n/zh-Hant/file.json b/packages/web/i18n/zh-Hant/file.json index 488717523..8445cf42e 100644 --- a/packages/web/i18n/zh-Hant/file.json +++ b/packages/web/i18n/zh-Hant/file.json @@ -15,6 +15,7 @@ "Please wait for all files to upload": "請等待所有文件上傳完成", "bucket_chat": "對話檔案", "bucket_file": "知識庫檔案", + "bucket_image": "圖片", "click_to_view_raw_source": "點選檢視原始來源", "common.Some images failed to process": "部分圖片處理失敗", "common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式", diff --git a/packages/web/i18n/zh-Hant/login.json b/packages/web/i18n/zh-Hant/login.json index a4371809d..734a3463b 100644 --- a/packages/web/i18n/zh-Hant/login.json +++ b/packages/web/i18n/zh-Hant/login.json @@ -9,7 +9,7 @@ "no_remind": "不再提醒", "password_condition": "密碼最多 60 個字元", "password_tip": "密碼至少 8 位,且至少包含兩種組合:數字、字母或特殊字元", - "policy_tip": "使用即代表您同意我們的", + "policy_tip": "使用即代表您已閱讀並同意 服務協議 隱私協議", "privacy": "隱私權政策", "privacy_policy": "隱私權政策", "redirect": "跳轉", diff --git a/packages/web/styles/theme.ts b/packages/web/styles/theme.ts index 5044e122d..27ee18a8d 100644 --- a/packages/web/styles/theme.ts +++ b/packages/web/styles/theme.ts @@ -545,7 +545,30 @@ const Checkbox = checkBoxMultiStyle({ borderColor: 'primary.400' } } - }) + }), + sizes: { + sm: checkBoxPart({ + control: { + width: '18px', + height: '18px', + borderWidth: '2px' + } + }), + md: checkBoxPart({ + control: { + width: '20px', + height: '20px', + borderWidth: '2px' + } + }), + lg: checkBoxPart({ + control: { + width: '24px', + height: '24px', + borderWidth: '2px' + } + }) + } }); const Modal = modalMultiStyle({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 108e682fb..d77757a52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,8 +121,8 @@ importers: packages/service: dependencies: '@fastgpt-sdk/plugin': - specifier: ^0.1.7 - version: 0.1.7(@types/node@20.17.24) + specifier: ^0.1.8 + version: 0.1.8(@types/node@20.17.24) '@fastgpt/global': specifier: workspace:* version: link:../global @@ -157,8 +157,8 @@ importers: specifier: ^0.8.10 version: 0.8.10 '@zilliz/milvus2-sdk-node': - specifier: 2.4.2 - version: 2.4.2 + specifier: 2.4.10 + version: 2.4.10 axios: specifier: ^1.8.2 version: 1.8.4 @@ -1973,8 +1973,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.7': - resolution: {integrity: sha512-/9szNeb1zLqThHenBYhYTyJr25dqRJwbXiWHFaf99tHWBjgMdMt2tfJhM9E6fz/zlAE3XlJIn/Dlgv82LJa7RQ==} + '@fastgpt-sdk/plugin@0.1.8': + resolution: {integrity: sha512-Db4wWJV/NjWcsKXXOUeNRNYeVBmW9yn9IqjYr7Mj+Z77YwN0gUFIek4tv+zQzsr9IoQgq+vptCEf6Ae9d48uaA==} '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} @@ -3838,8 +3838,8 @@ packages: '@zag-js/focus-visible@0.31.1': resolution: {integrity: sha512-dbLksz7FEwyFoANbpIlNnd3bVm0clQSUsnP8yUVQucStZPsuWjCrhL2jlAbGNrTrahX96ntUMXHb/sM68TibFg==} - '@zilliz/milvus2-sdk-node@2.4.2': - resolution: {integrity: sha512-fkPu7XXzfUvHoCnSPVOjqQpWuSnnn9x2NMmmCcIOyRzMeXIsrz4Mf/+M7LUzmT8J9F0Khx65B0rJgCu27YzWQw==} + '@zilliz/milvus2-sdk-node@2.4.10': + resolution: {integrity: sha512-KeXRFePLGoAMFQRM2w+oyH0X+R1uaj+Pt1o0rAdgQfGTV9aGdEx2zOJAt3XPWKovbphvF6ANmCGw2bbk7alNxQ==} abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} @@ -11208,7 +11208,7 @@ snapshots: '@eslint/js@8.57.1': {} - '@fastgpt-sdk/plugin@0.1.7(@types/node@20.17.24)': + '@fastgpt-sdk/plugin@0.1.8(@types/node@20.17.24)': dependencies: '@fortaine/fetch-event-source': 3.0.6 '@ts-rest/core': 3.52.1(@types/node@20.17.24)(zod@3.25.51) @@ -13384,7 +13384,7 @@ snapshots: dependencies: '@zag-js/dom-query': 0.31.1 - '@zilliz/milvus2-sdk-node@2.4.2': + '@zilliz/milvus2-sdk-node@2.4.10': dependencies: '@grpc/grpc-js': 1.13.0 '@grpc/proto-loader': 0.7.13 diff --git a/projects/app/.env.template b/projects/app/.env.template index 4f81d0493..dfde8f44e 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -3,6 +3,7 @@ LOG_DEPTH=3 DEFAULT_ROOT_PSW=123456 # 数据库最大连接数 DB_MAX_LINK=5 +TOKEN_KEY=fastgpt # 文件阅读时的密钥 FILE_TOKEN_KEY=filetokenkey # 密钥加密key @@ -12,6 +13,9 @@ ROOT_KEY=fdafasd # 强制将图片转成 base64 传递给模型 MULTIPLE_DATA_TO_BASE64=true +# 是否隐藏版权信息配置,只有值为 'true' 时隐藏 +HIDE_CHAT_COPYRIGHT_SETTING= + # Service url # 商业版地址 PRO_URL= diff --git a/projects/app/data/config.local.json b/projects/app/data/config.local.json index fcba250eb..1ecfdee11 100644 --- a/projects/app/data/config.local.json +++ b/projects/app/data/config.local.json @@ -10,8 +10,8 @@ }, "llmModels": [ { - "model": "gpt-4o-mini", // 模型名(对应OneAPI中渠道的模型名) - "name": "gpt-4o-mini", // 模型别名 + "model": "gpt-5", // 模型名(对应OneAPI中渠道的模型名) + "name": "gpt-5", // 模型别名 "avatar": "/imgs/model/openai.svg", // 模型的logo "maxContext": 125000, // 最大上下文 "maxResponse": 16000, // 最大回复 diff --git a/projects/app/data/model.json b/projects/app/data/model.json index 1e29974a8..3f74f57b7 100644 --- a/projects/app/data/model.json +++ b/projects/app/data/model.json @@ -12,8 +12,8 @@ "llmModels": [ { "provider": "OpenAI", // 模型提供商,主要用于分类展示,目前已经内置提供商包括:https://github.com/labring/FastGPT/blob/main/packages/global/core/ai/provider.ts, 可 pr 提供新的提供商,或直接填写 Other - "model": "gpt-4o-mini", // 模型名(对应OneAPI中渠道的模型名) - "name": "gpt-4o-mini", // 模型别名 + "model": "gpt-5", // 模型名(对应OneAPI中渠道的模型名) + "name": "gpt-5", // 模型别名 "maxContext": 128000, // 最大上下文 "maxResponse": 16000, // 最大回复 "quoteMaxToken": 120000, // 最大引用内容 diff --git a/projects/app/package.json b/projects/app/package.json index 5aa349d46..77f564473 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.11.1", + "version": "4.12.0", "private": false, "scripts": { "dev": "next dev", diff --git a/projects/app/public/icon/login-bg-phone.svg b/projects/app/public/icon/login-bg-phone.svg new file mode 100644 index 000000000..feb9beaa5 --- /dev/null +++ b/projects/app/public/icon/login-bg-phone.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/projects/app/public/imgs/chat/fastgpt_banner.svg b/projects/app/public/imgs/chat/fastgpt_banner.svg new file mode 100644 index 000000000..f181f566c --- /dev/null +++ b/projects/app/public/imgs/chat/fastgpt_banner.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/app/public/imgs/chat/fastgpt_banner_fold.svg b/projects/app/public/imgs/chat/fastgpt_banner_fold.svg new file mode 100644 index 000000000..cc83284d7 --- /dev/null +++ b/projects/app/public/imgs/chat/fastgpt_banner_fold.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..0386ed2cea4f87e51c3e289854f10574036371fd GIT binary patch literal 80206 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 L5AIdnwU7QE8{Ia} literal 0 HcmV?d00001 diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png new file mode 100644 index 0000000000000000000000000000000000000000..3100121b6fa9f1d60f5b165cb876d164b4fea67b GIT binary patch literal 84355 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쇈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%0Dx&r7nM?#VF7Ba0s@eDYSiE4;audef;J-k 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 literal 0 HcmV?d00001 diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png new file mode 100644 index 0000000000000000000000000000000000000000..20318678546907aeab11c606e986f9a83ab7dd36 GIT binary patch literal 81231 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

n=fTdVg3BC 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 + + diff --git a/projects/app/public/imgs/proModalBg.png b/projects/app/public/imgs/proModalBg.png new file mode 100644 index 0000000000000000000000000000000000000000..a54fe3d9922cbde9414f153699e1866677829d87 GIT binary patch literal 47434 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%0D?-r@pr!3k|8r-^kwWBI#VLfYpHL=s!MRV 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 LD3?C6di8$*)4Ii) literal 0 HcmV?d00001 diff --git a/projects/app/public/imgs/proTag.svg b/projects/app/public/imgs/proTag.svg new file mode 100644 index 000000000..d8ca325fa --- /dev/null +++ b/projects/app/public/imgs/proTag.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/projects/app/public/imgs/proTagEng.svg b/projects/app/public/imgs/proTagEng.svg new file mode 100644 index 000000000..4baafc083 --- /dev/null +++ b/projects/app/public/imgs/proTagEng.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/projects/app/src/components/Layout/auth.tsx b/projects/app/src/components/Layout/auth.tsx index 5df634fa0..eda9674a7 100644 --- a/projects/app/src/components/Layout/auth.tsx +++ b/projects/app/src/components/Layout/auth.tsx @@ -13,7 +13,6 @@ const unAuthPage: { [key: string]: boolean } = { '/appStore': true, '/chat': true, '/chat/share': true, - '/chat/team': true, '/tools/price': true, '/price': true }; diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index f682e8791..1859a0997 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -46,7 +46,6 @@ const pcUnShowLayoutRoute: Record = { '/login/provider': true, '/login/fastlogin': true, '/chat/share': true, - '/chat/team': true, '/app/edit': true, '/chat': true, '/tools/price': true, @@ -57,8 +56,8 @@ const phoneUnShowLayoutRoute: Record = { '/login': true, '/login/provider': true, '/login/fastlogin': true, + '/chat': true, '/chat/share': true, - '/chat/team': true, '/tools/price': true, '/price': true }; diff --git a/projects/app/src/components/MyInput/index.tsx b/projects/app/src/components/MyInput/index.tsx index 8dc369f6d..46daa8d5a 100644 --- a/projects/app/src/components/MyInput/index.tsx +++ b/projects/app/src/components/MyInput/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { Flex, Input, type InputProps } from '@chakra-ui/react'; interface Props extends InputProps { @@ -6,10 +6,11 @@ interface Props extends InputProps { rightIcon?: React.ReactNode; } -const MyInput = ({ leftIcon, rightIcon, ...props }: Props) => { +const MyInput = forwardRef(({ leftIcon, rightIcon, ...props }, ref) => { return ( { )} ); -}; +}); + +MyInput.displayName = 'MyInput'; export default MyInput; diff --git a/projects/app/src/components/PageContainer/index.tsx b/projects/app/src/components/PageContainer/index.tsx index 009d7753a..c1277454b 100644 --- a/projects/app/src/components/PageContainer/index.tsx +++ b/projects/app/src/components/PageContainer/index.tsx @@ -8,7 +8,6 @@ const PageContainer = ({ insertProps = {}, ...props }: BoxProps & { isLoading?: boolean; insertProps?: BoxProps }) => { - const theme = useTheme(); return ( void }) => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const [isOpen, setIsOpen] = useState(false); + + const openModal = props?.isOpen ?? isOpen; + const onClose = props?.onClose ?? (() => setIsOpen(false)); + + return feConfigs?.isPlus ? null : ( + + + + + + {t('app:pro_modal_title')} + + + {t('app:pro_modal_subtitle')} + + + {t('app:pro_modal_feature_1')} + {t('app:pro_modal_feature_2')} + {t('app:pro_modal_feature_3')} + + + + + + + + + {t('app:pro_modal_later_button')} + + + + + ); +}; + +export default ProModal; diff --git a/projects/app/src/components/ProTip/ProText.tsx b/projects/app/src/components/ProTip/ProText.tsx new file mode 100644 index 000000000..6c466a332 --- /dev/null +++ b/projects/app/src/components/ProTip/ProText.tsx @@ -0,0 +1,49 @@ +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import React, { useEffect, useState } from 'react'; +import ProModal from './ProModal'; +import { Box } from '@chakra-ui/react'; + +const ProText = ({ children, signKey }: { children: React.ReactNode; signKey: string }) => { + const { feConfigs } = useSystemStore(); + + const [isOpen, setIsOpen] = useState(false); + + const key = `proTip_${signKey}_lastShown`; + + // Check if modal should auto-open based on 6-hour interval + useEffect(() => { + if (feConfigs?.isPlus) return; + + const lastShown = localStorage.getItem(key); + + if (!lastShown) { + // First time, show modal immediately + setIsOpen(true); + } else { + const lastShownTime = parseInt(lastShown); + const sixHours = 6 * 60 * 60 * 1000; // 6 hours in milliseconds + + if (Date.now() - lastShownTime >= sixHours) { + setIsOpen(true); + } + } + }, [feConfigs?.isPlus, key, signKey]); + + const handleClose = () => { + setIsOpen(false); + if (!feConfigs?.isPlus) { + localStorage.setItem(key, Date.now().toString()); + } + }; + + return feConfigs?.isPlus ? null : ( + <> + setIsOpen(true)}> + {children} + + + + ); +}; + +export default ProText; diff --git a/projects/app/src/components/ProTip/Tag.tsx b/projects/app/src/components/ProTip/Tag.tsx new file mode 100644 index 000000000..0163b106b --- /dev/null +++ b/projects/app/src/components/ProTip/Tag.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import MyImage from '@fastgpt/web/components/common/Image/MyImage'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; + +const LangMap: Record = { + 'zh-CN': '/imgs/proTag.svg', + 'en-US': '/imgs/proTagEng.svg' +}; + +const ProTag = () => { + const { i18n } = useTranslation(); + const { feConfigs } = useSystemStore(); + + return feConfigs?.isPlus ? null : ; +}; + +export default ProTag; diff --git a/projects/app/src/components/Select/AIModelSelector.tsx b/projects/app/src/components/Select/AIModelSelector.tsx index 5660b1cee..03974b565 100644 --- a/projects/app/src/components/Select/AIModelSelector.tsx +++ b/projects/app/src/components/Select/AIModelSelector.tsx @@ -56,9 +56,10 @@ const OneRowSelector = ({ list, onChange, disableTip, ...props }: Props) => { borderRadius={'0'} mr={2} src={modelData?.avatar || HUGGING_FACE_ICON} - fallbackSrc={HUGGING_FACE_ICON} w={avatarSize} + fallbackSrc={HUGGING_FACE_ICON} /> + {modelData.name} ) diff --git a/projects/app/src/components/Select/FileSelector.tsx b/projects/app/src/components/Select/FileSelector.tsx index 9fc335d00..bf10bd09c 100644 --- a/projects/app/src/components/Select/FileSelector.tsx +++ b/projects/app/src/components/Select/FileSelector.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import type { UseFormReturn } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form'; import { useFileUpload } from '../core/chat/ChatContainer/ChatBox/hooks/useFileUpload'; diff --git a/projects/app/src/components/common/folder/Path.tsx b/projects/app/src/components/common/folder/Path.tsx index e6c04dd0d..3b58924da 100644 --- a/projects/app/src/components/common/folder/Path.tsx +++ b/projects/app/src/components/common/folder/Path.tsx @@ -145,9 +145,7 @@ const FolderPath = (props: { return paths.length === 0 && !!FirstPathDom ? ( <>{FirstPathDom} ) : ( - - {displayPaths.map((item, index) => renderPathItem(item, index))} - + {displayPaths.map((item, index) => renderPathItem(item, index))} ); }; diff --git a/projects/app/src/components/core/app/DatasetSelectModal.tsx b/projects/app/src/components/core/app/DatasetSelectModal.tsx index 2b26cba35..377ef7b15 100644 --- a/projects/app/src/components/core/app/DatasetSelectModal.tsx +++ b/projects/app/src/components/core/app/DatasetSelectModal.tsx @@ -1,26 +1,33 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { - Card, Flex, Box, Button, ModalBody, ModalFooter, - useTheme, Grid, - Divider + Checkbox, + VStack, + HStack, + IconButton, + Spacer } from '@chakra-ui/react'; +import { ChevronRightIcon, CloseIcon, InfoIcon } from '@chakra-ui/icons'; import Avatar from '@fastgpt/web/components/common/Avatar'; import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'; +import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type'; +import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { useTranslation } from 'next-i18next'; -import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal'; -import { useLoading } from '@fastgpt/web/hooks/useLoading'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { useDatasetSelect } from '@/components/core/dataset/SelectModal'; +import FolderPath from '@/components/common/folder/Path'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +// Dataset selection modal component export const DatasetSelectModal = ({ isOpen, defaultSelectedDatasets = [], @@ -32,190 +39,367 @@ export const DatasetSelectModal = ({ onChange: (e: SelectedDatasetType) => void; onClose: () => void; }) => { + // Translation function const { t } = useTranslation(); - const theme = useTheme(); + // Current selected datasets, initialized with defaultSelectedDatasets const [selectedDatasets, setSelectedDatasets] = useState(defaultSelectedDatasets); const { toast } = useToast(); - const { paths, setParentId, datasets, isFetching } = useDatasetSelect(); - const { Loading } = useLoading(); - const unSelectedDatasets = useMemo(() => { - return datasets.filter( - (item) => !selectedDatasets.some((dataset) => dataset.datasetId === item._id) - ); - }, [datasets, selectedDatasets]); + // Use server-side search, following the logic of the dataset list page + const { paths, setParentId, searchKey, setSearchKey, datasets, isFetching } = useDatasetSelect(); + // The vector model of the first selected dataset const activeVectorModel = selectedDatasets[0]?.vectorModel?.model; + // Check if a dataset is selected + const isDatasetSelected = useCallback( + (datasetId: string) => { + return selectedDatasets.some((dataset) => dataset.datasetId === datasetId); + }, + [selectedDatasets] + ); + + // Check if a dataset is disabled (vector model mismatch) + const isDatasetDisabled = (item: DatasetListItemType) => { + return !!activeVectorModel && activeVectorModel !== item.vectorModel.model; + }; + + // Cache compatible datasets by vector model to avoid repeated filtering + const compatibleDatasetsByModel = useMemo(() => { + const visibleDatasets = datasets.filter( + (item: DatasetListItemType) => item.type !== DatasetTypeEnum.folder + ); + + const targetModel = activeVectorModel || visibleDatasets[0]?.vectorModel?.model; + if (!targetModel) { + return []; + } + + return visibleDatasets.filter( + (item: DatasetListItemType) => item.vectorModel.model === targetModel + ); + }, [datasets, activeVectorModel]); + + // Check if all compatible datasets are selected + const isAllSelected = useMemo(() => { + if (compatibleDatasetsByModel.length === 0) { + return false; + } + + const selectedDatasetIds = new Set(selectedDatasets.map((dataset) => dataset.datasetId)); + return compatibleDatasetsByModel.every((item: DatasetListItemType) => + selectedDatasetIds.has(item._id) + ); + }, [compatibleDatasetsByModel, selectedDatasets]); + + const onSelect = (item: DatasetListItemType, checked: boolean) => { + if (checked) { + if (isDatasetDisabled(item)) { + return toast({ + status: 'warning', + title: t('common:dataset.Select Dataset Tips') + }); + } + setSelectedDatasets((prev) => [ + ...prev, + { + datasetId: item._id, + avatar: item.avatar, + name: item.name, + vectorModel: item.vectorModel + } + ]); + } else { + setSelectedDatasets((prev) => prev.filter((dataset) => dataset.datasetId !== item._id)); + } + }; + + // Render component return ( - - - + {/* Main vertical layout */} + + + {/* Two-column layout */} - {selectedDatasets.map((item) => - (() => { - return ( - - - - - - {item.name} - - { - setSelectedDatasets((state) => - state.filter((dataset) => dataset.datasetId !== item.datasetId) - ); - }} - /> - - - - ); - })() - )} - + {/* Left: search and dataset list */} + + {/* Search box */} + + setSearchKey(e.target.value?.trim())} + size="md" + /> + - {selectedDatasets.length > 0 && } - - - {unSelectedDatasets.map((item) => - (() => { - return ( - + {searchKey && ( + - + )} + {!searchKey && paths.length === 0 && ( + // Root directory path + + setParentId('')} + > + {t('common:root_folder')} + + + + )} + {!searchKey && paths.length > 0 && ( + // Subdirectory path + ({ + parentId: path.parentId, + parentName: path.parentName + }))} + FirstPathDom={t('common:root_folder')} + onClick={(e) => setParentId(e)} + /> + )} + + + {/* Dataset list */} + + {datasets.length === 0 && !isFetching && ( + + )} + {datasets.map((item: DatasetListItemType) => ( + + { if (item.type === DatasetTypeEnum.folder) { + if (searchKey) { + setSearchKey(''); + } setParentId(item._id); } else { - if (activeVectorModel && activeVectorModel !== item.vectorModel.model) { - return toast({ - status: 'warning', - title: t('common:dataset.Select Dataset Tips') - }); - } - setSelectedDatasets((state) => [ - ...state, - { - datasetId: item._id, - avatar: item.avatar, - name: item.name, - vectorModel: item.vectorModel - } - ]); + onSelect(item, !isDatasetSelected(item._id)); } }} > - - - + e.stopPropagation()} // Prevent parent click when clicking checkbox + > + {item.type !== DatasetTypeEnum.folder && ( + { + const checked = e.target.checked; + onSelect(item, checked); + }} + colorScheme="blue" + size="sm" + /> + )} + + + {/* Avatar */} + + + {/* Name and type */} + + {item.name} - - - {item.type === DatasetTypeEnum.folder ? ( - {t('common:Folder')} - ) : ( - <> - - {item.vectorModel.name} - - )} - - - - ); - })() - )} + + {item.type === DatasetTypeEnum.folder ? ( + <>{t('common:Folder')} + ) : ( + <> + {t('app:Index')}: {item.vectorModel.name} + + )} + + + + {/* Folder expand arrow */} + {item.type === DatasetTypeEnum.folder && ( + + + + )} + + + ))} + + + {/* Select all / Deselect all */} + {datasets.length > 0 && ( + + { + if (e.target.checked) { + const compatibleDatasets = compatibleDatasetsByModel.filter((dataset) => { + return !isDatasetSelected(dataset._id); + }); + const newSelections = compatibleDatasets.map( + (item: DatasetListItemType) => ({ + datasetId: item._id, + avatar: item.avatar, + name: item.name, + vectorModel: item.vectorModel + }) + ); + setSelectedDatasets((prev) => [...prev, ...newSelections]); + } else { + const datasetIdsToRemove = compatibleDatasetsByModel.map( + (item: DatasetListItemType) => item._id + ); + setSelectedDatasets((prev) => + prev.filter((dataset) => !datasetIdsToRemove.includes(dataset.datasetId)) + ); + } + }} + colorScheme="blue" + size="sm" + > + {t('common:Select_all')} + + + )} + + + {/* Right: selected datasets display */} + + {/* Selected count display */} + + {t('app:Selected')}: {selectedDatasets.length} {t('app:dataset')} + + {/* Selected dataset list */} + + {selectedDatasets.length === 0 && !isFetching && ( + + )} + {selectedDatasets.map((item) => ( + + + + {item.name} + + } + size="xs" + variant="ghost" + color="black" + _hover={{ bg: 'myGray.200' }} + onClick={() => + setSelectedDatasets((prev) => + prev.filter((dataset) => dataset.datasetId !== item.datasetId) + ) + } + /> + + ))} + + - {unSelectedDatasets.length === 0 && } + {/* Modal footer button area */} - + + + + + + {t('common:dataset.Select Dataset Tips')} + + + + - - - + ); }; diff --git a/projects/app/src/components/core/app/plugin/CostTooltip.tsx b/projects/app/src/components/core/app/plugin/CostTooltip.tsx index 2dcd49263..af5349130 100644 --- a/projects/app/src/components/core/app/plugin/CostTooltip.tsx +++ b/projects/app/src/components/core/app/plugin/CostTooltip.tsx @@ -2,10 +2,22 @@ import { Box, Flex, Divider } from '@chakra-ui/react'; import React from 'react'; import { useTranslation } from 'next-i18next'; -const CostTooltip = ({ cost, hasTokenFee }: { cost?: number; hasTokenFee?: boolean }) => { +const CostTooltip = ({ + cost, + hasTokenFee, + isFolder +}: { + cost?: number; + hasTokenFee?: boolean; + isFolder?: boolean; +}) => { const { t } = useTranslation(); const getCostText = () => { + if (isFolder) { + return t('app:plugin_cost_folder_tip'); + } + if (hasTokenFee && cost && cost > 0) { return `${t('app:plugin_cost_per_times', { cost: cost @@ -19,16 +31,13 @@ const CostTooltip = ({ cost, hasTokenFee }: { cost?: number; hasTokenFee?: boole cost: cost }); } - return t('common:core.plugin.Free'); + return t('app:tool_run_free'); }; return ( <> - - {t('common:core.plugin.cost')} - {getCostText()} - + {getCostText()} ); }; 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 9ecf7c5ad..e76183674 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 @@ -54,6 +54,8 @@ const ChatInput = ({ // Check voice input state const [mobilePreSpeak, setMobilePreSpeak] = useState(false); + const InputLeftComponent = useContextSelector(ChatBoxContext, (v) => v.InputLeftComponent); + const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData); const appId = useContextSelector(ChatBoxContext, (v) => v.appId); const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId); @@ -61,6 +63,7 @@ const ChatInput = ({ const whisperConfig = useContextSelector(ChatBoxContext, (v) => v.whisperConfig); const chatInputGuide = useContextSelector(ChatBoxContext, (v) => v.chatInputGuide); const fileSelectConfig = useContextSelector(ChatBoxContext, (v) => v.fileSelectConfig); + const dialogTips = useContextSelector(ChatBoxContext, (v) => v.dialogTips); const fileCtrl = useFieldArray({ control, @@ -127,13 +130,15 @@ const ChatInput = ({ border: 'none' }} placeholder={ - isPc ? t('common:core.chat.Type a message') : t('chat:input_placeholder_phone') + dialogTips || + (isPc ? t('common:core.chat.Type a message') : t('chat:input_placeholder_phone')) } resize={'none'} rows={1} height={[5, 6]} lineHeight={[5, 6]} maxHeight={[24, 32]} + minH={'50px'} mb={0} maxLength={-1} overflowY={'hidden'} @@ -217,16 +222,19 @@ const ChatInput = ({ ), [ - TextareaDom, fileList.length, - handleSend, - inputValue, + TextareaDom, + dialogTips, isPc, - onSelectFile, + t, + inputValue, + onFocus, + offFocus, setValue, + handleSend, showSelectFile, showSelectImg, - t + onSelectFile ] ); @@ -238,97 +246,106 @@ const ChatInput = ({ return ( - {/* Attachment and Voice Group */} - - {/* file selector button */} - {(showSelectFile || showSelectImg) && ( - { - e.stopPropagation(); - onOpenSelectFile(); - }} - > - - - - onSelectFile({ files })} /> - - )} - - {/* Voice input button */} - {whisperConfig?.open && !inputValue && ( - { - e.stopPropagation(); - VoiceInputRef.current?.onSpeak?.(); - }} - > - - - - - )} + {/* 左侧自定义按钮组 */} + + {InputLeftComponent} - {/* Divider Container */} - {((whisperConfig?.open && !inputValue) || showSelectFile || showSelectImg) && ( - - - - )} - - {/* Send Button Container */} - - { - e.stopPropagation(); - if (isChatting) { - return onStop(); - } - return handleSend(); - }} - > - {isChatting ? ( - - ) : ( - - - + {/* 右侧原有按钮组 */} + + {/* Attachment and Voice Group */} + + {/* file selector button */} + {(showSelectFile || showSelectImg) && ( + { + e.stopPropagation(); + onOpenSelectFile(); + }} + > + + + + onSelectFile({ files })} /> + )} + + {/* Voice input button */} + {whisperConfig?.open && !inputValue && ( + { + e.stopPropagation(); + VoiceInputRef.current?.onSpeak?.(); + }} + > + + + + + )} + + + {/* Divider Container */} + {((whisperConfig?.open && !inputValue) || showSelectFile || showSelectImg) && ( + + + + )} + + {/* Send Button Container */} + + { + e.stopPropagation(); + if (isChatting) { + return onStop(); + } + return handleSend(); + }} + > + {isChatting ? ( + + ) : ( + + + + )} + @@ -348,7 +365,8 @@ const ChatInput = ({ onOpenSelectFile, onSelectFile, handleSend, - onStop + onStop, + InputLeftComponent ]); const activeStyles: FlexProps = { @@ -392,7 +410,7 @@ const ChatInput = ({ direction={'column'} minH={mobilePreSpeak ? '48px' : ['96px', '120px']} pt={fileList.length > 0 ? '0' : mobilePreSpeak ? [0, 4] : [3, 4]} - pb={[2, 4]} + pb={InputLeftComponent ? 2 : 3} position={'relative'} borderRadius={['xl', 'xxl']} bg={'white'} @@ -427,8 +445,6 @@ const ChatInput = ({ )} - {/* loading spinner */} - {/* voice input and loading container */} {!inputValue && ( any; +}; + const ContextModal = dynamic(() => import('./ContextModal')); const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal')); @@ -43,7 +51,8 @@ const ResponseTags = ({ const { totalQuoteList: quoteList = [], llmModuleAccount = 0, - historyPreviewLength = 0 + historyPreviewLength = 0, + toolCiteLinks = [] } = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]); const [quoteFolded, setQuoteFolded] = useState(true); @@ -68,8 +77,9 @@ const ResponseTags = ({ ? quoteListRef.current.scrollHeight > (isPc ? 50 : 55) : true; - const sourceList = useMemo(() => { - return Object.values( + const citationRenderList: CitationRenderItem[] = useMemo(() => { + // Dataset citations + const datasetItems = Object.values( quoteList.reduce((acc: Record, cur) => { if (!acc[cur.collectionId]) { acc[cur.collectionId] = [cur]; @@ -79,27 +89,41 @@ const ResponseTags = ({ ) .flat() .map((item) => ({ - sourceName: item.sourceName, - sourceId: item.sourceId, + type: 'dataset' as const, + key: item.collectionId, + displayText: item.sourceName, icon: item.imageId ? 'core/dataset/imageFill' : getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }), - collectionId: item.collectionId, - datasetId: item.datasetId + onClick: () => { + onOpenCiteModal({ + collectionId: item.collectionId, + sourceId: item.sourceId, + sourceName: item.sourceName, + datasetId: item.datasetId + }); + } })); - }, [quoteList]); - const notEmptyTags = - quoteList.length > 0 || - (llmModuleAccount === 1 && notSharePage) || - (llmModuleAccount > 1 && notSharePage) || - (isPc && durationSeconds > 0) || - notSharePage; + // Link citations + const linkItems = toolCiteLinks.map((r, index) => ({ + type: 'link' as const, + key: `${r.url}-${index}`, + displayText: r.name, + onClick: () => { + window.open(r.url, '_blank'); + } + })); + + return [...datasetItems, ...linkItems]; + }, [quoteList, toolCiteLinks, onOpenCiteModal]); + + const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0); return !showTags ? null : ( <> {/* quote */} - {sourceList.length > 0 && ( + {citationRenderList.length > 0 && ( <> @@ -143,9 +167,9 @@ const ResponseTags = ({ : {} } > - {sourceList.map((item, index) => { + {citationRenderList.map((item, index) => { return ( - + { e.stopPropagation(); - onOpenCiteModal(item); + item.onClick?.(); }} height={6} > @@ -184,7 +208,7 @@ const ResponseTags = ({ flex={'1 0 0'} fontSize={'mini'} > - {item.sourceName} + {item.displayText} diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx new file mode 100644 index 000000000..005f4ddb7 --- /dev/null +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox.tsx @@ -0,0 +1,23 @@ +import { ChatBoxContext } from '@/components/core/chat/ChatContainer/ChatBox/Provider'; +import { DEFAULT_LOGO_BANNER_URL } from '@/pageComponents/chat/constants'; +import { Box, Flex, Image } from '@chakra-ui/react'; +import { useContextSelector } from 'use-context-selector'; + +const WelcomeHomeBox = () => { + const wideLogo = useContextSelector(ChatBoxContext, (v) => v.wideLogo); + const slogan = useContextSelector(ChatBoxContext, (v) => v.slogan); + + return ( + + fastgpt logo + {slogan} + + ); +}; + +export default WelcomeHomeBox; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/constants.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/constants.ts index 3b3ab5f46..89f224048 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/constants.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/constants.ts @@ -22,5 +22,6 @@ export enum ChatTypeEnum { chat = 'chat', log = 'log', share = 'share', - team = 'team' + team = 'team', + home = 'home' } diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index ef8127797..47a8d6795 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -14,7 +14,7 @@ import type { } from '@fastgpt/global/core/chat/type.d'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { Box, Checkbox } from '@chakra-ui/react'; +import { Box, Checkbox, Flex, Image } from '@chakra-ui/react'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { useForm } from 'react-hook-form'; @@ -67,6 +67,7 @@ import TimeBox from './components/TimeBox'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; +import WelcomeHomeBox from '@/components/core/chat/ChatContainer/ChatBox/components/WelcomeHomeBox'; const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal')); @@ -819,8 +820,14 @@ const ChatBox = ({ }; }); + const showHomeWelcome = useMemo( + () => chatRecords.length === 0 && chatType === ChatTypeEnum.home, + [chatRecords.length, chatType] + ); + const showEmpty = useMemo( () => + chatType !== ChatTypeEnum.home && feConfigs?.show_emptyChat && showEmptyIntro && chatRecords.length === 0 && @@ -828,9 +835,10 @@ const ChatBox = ({ !externalVariableList?.length && !welcomeText, [ - chatRecords.length, + chatType, feConfigs?.show_emptyChat, showEmptyIntro, + chatRecords.length, variableList?.length, externalVariableList?.length, welcomeText @@ -958,7 +966,7 @@ const ChatBox = ({ return ( {/* chat header */} + {showHomeWelcome && } {showEmpty && } {!!welcomeText && } {/* variable input */} @@ -1084,6 +1093,7 @@ const ChatBox = ({ questionGuides, retryInput, showEmpty, + showHomeWelcome, showMarkIcon, showVoiceIcon, statusBoxData, diff --git a/projects/app/src/components/core/dataset/SelectModal.tsx b/projects/app/src/components/core/dataset/SelectModal.tsx index d5e10f0bf..d83a60ddb 100644 --- a/projects/app/src/components/core/dataset/SelectModal.tsx +++ b/projects/app/src/components/core/dataset/SelectModal.tsx @@ -69,27 +69,42 @@ const DatasetSelectContainer = ({ }; export function useDatasetSelect() { - const [parentId, setParentId] = useState(''); + const [parentId, setParentId] = useState(''); + const [searchKey, setSearchKey] = useState(''); - const { data, loading: isFetching } = useRequest2( - () => - Promise.all([ - getDatasets({ parentId }), - getDatasetPaths({ sourceId: parentId, type: 'current' }) - ]), + const { + data = { + datasets: [], + paths: [] + }, + loading: isFetching + } = useRequest2( + async () => { + const result = await Promise.all([ + getDatasets({ parentId, searchKey }), + // Only get paths when not searching + searchKey.trim() + ? Promise.resolve([]) + : getDatasetPaths({ sourceId: parentId, type: 'current' }) + ]); + return { + datasets: result[0], + paths: result[1] + }; + }, { manual: false, - refreshDeps: [parentId] + refreshDeps: [parentId, searchKey] } ); - const paths = useMemo(() => [...(data?.[1] || [])], [data]); - return { parentId, setParentId, - datasets: data?.[0] || [], - paths, + searchKey, + setSearchKey, + datasets: data.datasets, + paths: data.paths, isFetching }; } diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index 99f204b1a..6d4e29700 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -153,6 +153,7 @@ function RoleSelect({ zIndex={99} overflowY={'auto'} whiteSpace={'pre-wrap'} + userSelect={'none'} > {/* The list of single select permissions */} {roleOptions.singleOptions.map((item) => { @@ -216,8 +217,8 @@ function RoleSelect({ : {})} {...MenuStyle} > - - + + {t(item.name as any)} {t(item.description as any)} diff --git a/projects/app/src/global/aiproxy/constants.ts b/projects/app/src/global/aiproxy/constants.ts index d14444335..a34382432 100644 --- a/projects/app/src/global/aiproxy/constants.ts +++ b/projects/app/src/global/aiproxy/constants.ts @@ -53,6 +53,11 @@ export const aiproxyIdMap: Record< label: i18nT('account_model:azure'), provider: 'OpenAI' }, + 4: { + avatar: 'model/azure', + label: `azure (model name support contain '.')`, + provider: 'Other' + }, 14: { label: 'Anthropic', provider: 'Claude' @@ -151,5 +156,39 @@ export const aiproxyIdMap: Record< 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/global/core/chat/constants.ts b/projects/app/src/global/core/chat/constants.ts index fc2b26805..dfca254b8 100644 --- a/projects/app/src/global/core/chat/constants.ts +++ b/projects/app/src/global/core/chat/constants.ts @@ -19,5 +19,6 @@ export const defaultChatData: InitChatResponse = { export enum GetChatTypeEnum { normal = 'normal', outLink = 'outLink', - team = 'team' + team = 'team', + home = 'home' } diff --git a/projects/app/src/global/core/chat/utils.ts b/projects/app/src/global/core/chat/utils.ts index e50ac347a..5f3cc2385 100644 --- a/projects/app/src/global/core/chat/utils.ts +++ b/projects/app/src/global/core/chat/utils.ts @@ -2,6 +2,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { type ChatHistoryItemResType, type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { type ToolCiteLinksType } from '@fastgpt/global/core/chat/type'; export const isLLMNode = (item: ChatHistoryItemResType) => item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.agent; @@ -33,20 +34,61 @@ export const getFlatAppResponses = (res: ChatHistoryItemResType[]): ChatHistoryI }; export function addStatisticalDataToHistoryItem(historyItem: ChatItemType) { if (historyItem.obj !== ChatRoleEnum.AI) return historyItem; - if (historyItem.totalQuoteList !== undefined) return historyItem; + if (historyItem.totalQuoteList !== undefined || historyItem.toolCiteLinks !== undefined) + return historyItem; if (!historyItem.responseData) return historyItem; // Flat children const flatResData = getFlatAppResponses(historyItem.responseData || []); + // get llm module account and history preview length and total quote list and external link list + const { llmModuleAccount, historyPreviewLength, totalQuoteList, toolCiteLinks } = + flatResData.reduce( + (acc, item) => { + // LLM + if (isLLMNode(item)) { + acc.llmModuleAccount = acc.llmModuleAccount + 1; + if (acc.historyPreviewLength === undefined) { + acc.historyPreviewLength = item.historyPreview?.length; + } + } + // Dataset search result + if (item.moduleType === FlowNodeTypeEnum.datasetSearchNode && item.quoteList) { + acc.totalQuoteList.push(...item.quoteList.filter(Boolean)); + } + + // Tool call + if (item.moduleType === FlowNodeTypeEnum.tool) { + const citeLinks = item?.toolRes?.citeLinks; + if (citeLinks && Array.isArray(citeLinks)) { + citeLinks.forEach(({ name = '', url = '' }: ToolCiteLinksType) => { + if (url) { + const key = `${name}::${url}`; + if (!acc.linkDedupe.has(key)) { + acc.linkDedupe.add(key); + acc.toolCiteLinks.push({ name, url }); + } + } + }); + } + } + + return acc; + }, + { + llmModuleAccount: 0, + historyPreviewLength: undefined as number | undefined, + totalQuoteList: [] as SearchDataResponseItemType[], + toolCiteLinks: [] as ToolCiteLinksType[], + linkDedupe: new Set() + } + ); + return { ...historyItem, - llmModuleAccount: flatResData.filter(isLLMNode).length, - totalQuoteList: flatResData - .filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode) - .map((item) => item.quoteList) - .flat() - .filter(Boolean) as SearchDataResponseItemType[], - historyPreviewLength: flatResData.find(isLLMNode)?.historyPreview?.length + llmModuleAccount, + totalQuoteList, + historyPreviewLength, + ...(toolCiteLinks.length ? { toolCiteLinks } : {}) }; } diff --git a/projects/app/src/pageComponents/account/info/RedeemCouponModal.tsx b/projects/app/src/pageComponents/account/info/RedeemCouponModal.tsx index bfeeffb4e..74e889da4 100644 --- a/projects/app/src/pageComponents/account/info/RedeemCouponModal.tsx +++ b/projects/app/src/pageComponents/account/info/RedeemCouponModal.tsx @@ -3,7 +3,7 @@ import { Button, Input, VStack, Text, ModalBody, Box, ModalFooter } from '@chakr import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; const RedeemCouponModal = ({ onClose, diff --git a/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx b/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx index 51e5ed504..88fb7610a 100644 --- a/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx +++ b/projects/app/src/pageComponents/account/model/ModelDashboard/index.tsx @@ -14,7 +14,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect'; import { getChannelList, getDashboardV2 } from '@/web/core/ai/channel'; import { getSystemModelList } from '@/web/core/ai/config'; import { getModelProvider } from '@fastgpt/global/core/ai/provider'; -import LineChartComponent from '@fastgpt/web/components/common/charts/LineChartComponent'; +import AreaChartComponent from '@fastgpt/web/components/common/charts/AreaChartComponent'; import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import DataTableComponent from './DataTableComponent'; @@ -444,7 +444,7 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => { dashboardData.length > 0 && ( <> - { - { /> - { - { {feConfigs?.isPlus && ( - { - { /> - { {filterProps?.model && ( - { /> - void; + isSelectAllSource: boolean; + setIsSelectAllSource: React.Dispatch>; + dateRange: DateRangeType; + setDateRange: (value: DateRangeType) => void; +}; + +const chartBoxStyles = { + px: 5, + pt: 4, + pb: 8, + h: '300px', + border: 'base', + borderRadius: 'md', + overflow: 'hidden', + bg: 'white' +}; + +const formatWeekDate = (date: Date) => { + const weekStart = startOfWeek(date, { weekStartsOn: 1 }); + const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); + + const startStr = dayjs(weekStart).format('MM/DD'); + const endStr = dayjs(weekEnd).format('MM/DD'); + + return { + date: `${startStr}-${endStr}`, + xLabel: `${startStr}-${endStr}` + }; +}; + +const generateCompleteTimeSeries = ( + dateRange: DateRangeType, + timespan: AppLogTimespanEnum +): string[] => { + if (!dateRange.from || !dateRange.to) return []; + + const start = startOfDay(new Date(dateRange.from)); + const end = startOfDay(new Date(dateRange.to)); + + const timespanConfig = { + [AppLogTimespanEnum.day]: { + count: differenceInDays(end, start) + 1, + addUnit: (date: Date, i: number) => date.setDate(date.getDate() + i), + format: (date: Date) => formatDateByTimespan(date.getTime(), timespan) + }, + [AppLogTimespanEnum.week]: { + dates: eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }), + format: (date: Date) => formatWeekDate(date) + }, + [AppLogTimespanEnum.month]: { + count: differenceInMonths(end, start) + 1, + addUnit: (date: Date, i: number) => date.setMonth(date.getMonth() + i), + format: (date: Date) => formatDateByTimespan(date.getTime(), timespan) + }, + [AppLogTimespanEnum.quarter]: { + count: differenceInQuarters(end, start) + 1, + addUnit: (date: Date, i: number) => date.setMonth(date.getMonth() + i * 3), + format: (date: Date) => formatDateByTimespan(date.getTime(), timespan) + } + }; + + const config = timespanConfig[timespan]; + const dates: string[] = []; + + if ('dates' in config) { + config.dates.forEach((date) => { + const { date: formattedDate } = config.format(date); + dates.push(formattedDate); + }); + } else { + for (let i = 0; i < config.count; i++) { + const date = new Date(start); + config.addUnit(date, i); + const { date: formattedDate } = config.format(date); + dates.push(formattedDate); + } + } + + return [...new Set(dates)]; +}; + +const LogChart = ({ + appId, + chatSources, + setChatSources, + isSelectAllSource, + setIsSelectAllSource, + dateRange, + setDateRange, + showSourceSelector = true +}: HeaderControlProps) => { + const { t } = useTranslation(); + + const { feConfigs } = useSystemStore(); + + const [userTimespan, setUserTimespan] = useState(AppLogTimespanEnum.day); + const [chatTimespan, setChatTimespan] = useState(AppLogTimespanEnum.day); + const [appTimespan, setAppTimespan] = useState(AppLogTimespanEnum.day); + + const [offset, setOffset] = useState(offsetOptions[0].value); + + const { data: chartData } = useRequest2( + async () => { + return getAppChartData({ + appId, + dateStart: dateRange.from || new Date(), + dateEnd: addDays(dateRange.to || new Date(), 1), + offset: parseInt(offset), + source: chatSources, + userTimespan, + chatTimespan, + appTimespan + }); + }, + { + manual: !feConfigs?.isPlus, + refreshDeps: [ + appId, + dateRange.from, + dateRange.to, + offset, + chatSources, + userTimespan, + chatTimespan, + appTimespan + ] + } + ); + + const formatChartData = useMemo(() => { + if (!feConfigs?.isPlus) return fakeChartData; + + const formatTimestamp = (timestamp: number, timespan: AppLogTimespanEnum) => { + return timespan === AppLogTimespanEnum.week + ? formatWeekDate(new Date(timestamp)) + : formatDateByTimespan(timestamp, timespan); + }; + + const processChartData = >( + rawData: any[] | undefined, + timespan: AppLogTimespanEnum | undefined, + mapper: (item: any, dateInfo: { date: string; xLabel: string }) => Omit, + defaultValues: Omit + ): T[] => { + if (!timespan) return []; + + const data = rawData || []; + const completeDates = generateCompleteTimeSeries(dateRange, timespan); + + const dataMap = new Map(); + data.forEach((item) => { + const dateInfo = formatTimestamp(item.timestamp, timespan); + const mappedItem = { + x: dateInfo.date, + xLabel: dateInfo.xLabel, + ...mapper(item, dateInfo) + } as unknown as T; + dataMap.set(dateInfo.date, mappedItem); + }); + + return completeDates.map( + (date) => dataMap.get(date) || ({ x: date, xLabel: date, ...defaultValues } as unknown as T) + ); + }; + + const createDefaultValues = (keys: string[], specialValues: Record = {}) => { + return keys.reduce((acc, key) => ({ ...acc, [key]: specialValues[key] || 0 }), {}); + }; + + const user = processChartData( + chartData?.userData, + userTimespan, + (item) => ({ + userCount: item.summary.userCount, + newUserCount: + userTimespan === 'day' && item.summary.retentionUserCount > 0 + ? item.summary.newUserCount - item.summary.retentionUserCount + : item.summary.newUserCount, + retentionUserCount: item.summary.retentionUserCount, + points: item.summary.points, + sourceCountMap: item.summary.sourceCountMap + }), + createDefaultValues( + ['userCount', 'newUserCount', 'retentionUserCount', 'points', 'sourceCountMap'], + { + sourceCountMap: Object.keys(ChatSourceMap).reduce( + (acc, key) => ({ ...acc, [key]: 0 }), + {} + ) + } + ) + ); + + const chat = processChartData( + chartData?.chatData, + chatTimespan, + (item) => { + const pointsPerChat = + item.summary.chatCount > 0 + ? Number((item.summary.points / item.summary.chatCount).toFixed(2)) + : 0; + return { + chatItemCount: item.summary.chatItemCount, + chatCount: item.summary.chatCount, + pointsPerChat, + errorCount: item.summary.errorCount, + errorRate: item.summary.chatItemCount + ? Number((item.summary.errorCount / item.summary.chatItemCount).toFixed(2)) + : 0 + }; + }, + createDefaultValues([ + 'chatItemCount', + 'chatCount', + 'pointsPerChat', + 'errorCount', + 'errorRate' + ]) + ); + + const app = processChartData( + chartData?.appData, + appTimespan, + (item) => ({ + goodFeedBackCount: item.summary.goodFeedBackCount, + badFeedBackCount: item.summary.badFeedBackCount, + avgDuration: item.summary.totalResponseTime / item.summary.chatCount + }), + createDefaultValues(['goodFeedBackCount', 'badFeedBackCount', 'avgDuration']) + ); + + const calculateStats = ( + data: Record[], + metrics: { [key: string]: 'sum' | 'avg' } + ) => { + return Object.entries(metrics).reduce( + (acc, [key, type]) => { + const values = data.map((item) => item[key] || 0); + acc[key] = + type === 'sum' + ? values.reduce((sum, val) => sum + val, 0) + : values.length > 0 + ? values.reduce((sum, val) => sum + val, 0) / values.length + : 0; + return acc; + }, + {} as Record + ); + }; + + const cumulative = { + ...calculateStats(user, { userCount: 'sum', points: 'sum' }), + ...calculateStats(chat, { + chatItemCount: 'sum', + chatCount: 'sum', + pointsPerChat: 'avg', + errorCount: 'sum', + errorRate: 'avg' + }), + ...calculateStats(app, { + goodFeedBackCount: 'sum', + badFeedBackCount: 'sum', + avgDuration: 'avg' + }) + }; + + return { user, chat, app, cumulative }; + }, [ + feConfigs?.isPlus, + chartData?.userData, + chartData?.chatData, + chartData?.appData, + userTimespan, + chatTimespan, + appTimespan, + dateRange + ]); + + return ( + + + + + + + + + {t('app:logs_user_data')} + + ({ + label: t(`app:logs_timespan_${option}`), + value: option + }))} + value={userTimespan} + onChange={(value) => { + setUserTimespan(value); + setOffset(offsetOptions[0].value); + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + + + + + + {t('app:logs_total')}: {formatChartData.cumulative.userCount} + + } + blur={!feConfigs?.isPlus} + /> + + + data.newUserCount + data.retentionUserCount + }, + { + label: t('app:logs_user_retention'), + dataKey: 'retentionUserCount', + color: theme.colors.primary['400'] + } + ]} + HeaderRightChildren={ + { + setOffset(value); + }} + /> + } + blur={!feConfigs?.isPlus} + /> + + + + {t('app:logs_total')}: {formatChartData.cumulative.points} + + } + blur={!feConfigs?.isPlus} + /> + + + ({ + dataKey: `sourceCountMap.${key}`, + name: t(value.name as any), + color: value.color + }))} + tooltipItems={Object.entries(ChatSourceMap).map(([key, value]) => ({ + dataKey: `sourceCountMap.${key}`, + label: t(value.name as any), + color: value.color, + customValue: (data) => data.sourceCountMap[key as ChatSourceEnum] + }))} + blur={!feConfigs?.isPlus} + /> + + + + + + + + {t('app:logs_chat_data')} + + ({ + label: t(`app:logs_timespan_${option}`), + value: option + }))} + value={chatTimespan} + onChange={(value) => { + setChatTimespan(value); + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + + + + + + {t('app:logs_total')}: {formatChartData.cumulative.chatItemCount} + + } + blur={!feConfigs?.isPlus} + /> + + + + {t('app:logs_total')}: {formatChartData.cumulative.chatCount} + + } + blur={!feConfigs?.isPlus} + /> + + + + {t('app:logs_total_error', { + count: formatChartData.cumulative.errorCount, + rate: formatChartData.cumulative.errorRate.toFixed(2) + })} + + } + blur={!feConfigs?.isPlus} + /> + + + + {`${t('app:logs_total_avg_points')}: ${formatChartData.cumulative.pointsPerChat.toFixed(2)}`} + + } + blur={!feConfigs?.isPlus} + /> + + + + + + + + {t('app:logs_app_result')} + + ({ + label: t(`app:logs_timespan_${option}`), + value: option + }))} + value={appTimespan} + onChange={(value) => { + setAppTimespan(value); + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + + + + + + {t('app:logs_total_feedback', { + goodFeedBack: + formatChartData.cumulative.goodFeedBackCount?.toLocaleString() || 0, + badFeedBack: + formatChartData.cumulative.badFeedBackCount?.toLocaleString() || 0 + })} + + } + blur={!feConfigs?.isPlus} + /> + + + + {`${t('app:logs_total_avg_duration')}: ${formatChartData.cumulative.avgDuration.toFixed(2)}s`} + + } + blur={!feConfigs?.isPlus} + /> + + + + + + + + ); +}; + +export default React.memo(LogChart); + +const HeaderControl = ({ + chatSources, + setChatSources, + isSelectAllSource, + setIsSelectAllSource, + dateRange, + setDateRange, + showSourceSelector = true +}: HeaderControlProps) => { + const { t } = useTranslation(); + + const sourceList = useMemo( + () => + Object.entries(ChatSourceMap).map(([key, value]) => ({ + label: t(value.name as any), + value: key as ChatSourceEnum + })), + [t] + ); + + console.log(showSourceSelector); + return ( + + {showSourceSelector && ( + + + list={sourceList} + value={chatSources} + onSelect={setChatSources} + isSelectAll={isSelectAllSource} + setIsSelectAll={setIsSelectAllSource} + h={10} + w={'226px'} + bg={'white'} + rounded={'8px'} + tagStyle={{ + px: 1, + py: 1, + borderRadius: 'sm', + bg: 'myGray.100', + color: 'myGray.900' + }} + borderColor={'myGray.200'} + formLabel={t('app:logs_source')} + formLabelFontSize={'sm'} + /> + + )} + + { + setDateRange(date); + }} + bg={'white'} + h={10} + w={'240px'} + rounded={'8px'} + borderColor={'myGray.200'} + formLabel={t('app:logs_date')} + _hover={{ + borderColor: 'primary.300' + }} + /> + + + ); +}; + +const TotalData = ({ appId }: { appId: string }) => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const { + data: totalData = { + totalUsers: 0, + totalChats: 0, + totalPoints: 0 + } + } = useRequest2( + async () => { + if (feConfigs?.isPlus) { + return await getAppTotalData({ appId }); + } + return { + totalUsers: 455, + totalChats: 22112, + totalPoints: 112233 + }; + }, + { + manual: false, + refreshDeps: [appId, feConfigs?.isPlus] + } + ); + + const totalDataArray = useMemo(() => { + return [ + { + label: t('app:logs_total_users'), + icon: 'support/user/usersLight', + colorSchema: { + icon: 'primary.600', + border: 'primary.200', + bg: 'primary.50' + }, + value: totalData.totalUsers + }, + { + label: t('app:logs_total_chat'), + icon: 'core/chat/chatLight', + colorSchema: { + icon: 'green.600', + border: 'green.200', + bg: 'green.50' + }, + value: totalData.totalChats + }, + { + label: t('app:logs_total_points'), + icon: 'support/bill/payRecordLight', + colorSchema: { + icon: 'yellow.600', + border: 'yellow.200', + bg: 'yellow.50' + }, + value: totalData.totalPoints + } + ]; + }, [t, totalData.totalChats, totalData.totalPoints, totalData.totalUsers]); + + return ( + <> + + {totalDataArray.map((item, index) => ( + + + + {item.label} + + + {item.value.toLocaleString()} + + + + + + + ))} + + + + + {t('app:logs_total_tips')} + + + + ); +}; diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx new file mode 100644 index 000000000..8d780039e --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx @@ -0,0 +1,517 @@ +import { + Box, + Button, + Flex, + HStack, + Input, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatSourceMap } from '@fastgpt/global/core/chat/constants'; +import MultipleSelect, { + useMultipleSelect +} from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import DateRangePicker from '@fastgpt/web/components/common/DateRangePicker'; +import { addDays } from 'date-fns'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; +import { getTeamMembers } from '@/web/support/user/team/api'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useLocalStorageState } from 'ahooks'; +import { getLogKeys } from '@/web/core/app/api/log'; +import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + AppLogKeysEnum, + AppLogKeysEnumMap, + DefaultAppLogKeys +} from '@fastgpt/global/core/app/logs/constants'; +import { isEqual } from 'lodash'; +import SyncLogKeysPopover from './SyncLogKeysPopover'; +import LogKeysConfigPopover from './LogKeysConfigPopover'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import { downloadFetch } from '@/web/common/system/utils'; +import { usePagination } from '@fastgpt/web/hooks/usePagination'; +import { getAppChatLogs } from '@/web/core/app/api/log'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import type { AppLogsListItemType } from '@/types/app'; +import dayjs from 'dayjs'; +import UserBox from '@fastgpt/web/components/common/UserBox'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import dynamic from 'next/dynamic'; +import type { HeaderControlProps } from './LogChart'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; + +const DetailLogsModal = dynamic(() => import('./DetailLogsModal')); + +const LogTable = ({ + appId, + chatSources, + setChatSources, + isSelectAllSource, + setIsSelectAllSource, + dateRange, + setDateRange, + showSourceSelector = true +}: HeaderControlProps) => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const [detailLogsId, setDetailLogsId] = useState(); + + // source + const sourceList = useMemo( + () => + Object.entries(ChatSourceMap).map(([key, value]) => ({ + label: t(value.name as any), + value: key as ChatSourceEnum + })), + [t] + ); + + // member + const [tmbInputValue, setTmbInputValue] = useState(''); + const { data: members, ScrollData: TmbScrollData } = useScrollPagination(getTeamMembers, { + params: { searchKey: tmbInputValue }, + refreshDeps: [tmbInputValue], + disabled: !feConfigs?.isPlus + }); + const tmbList = useMemo( + () => + members.map((item) => ({ + label: ( + + + + {item.memberName} + + + ), + value: item.tmbId + })), + [members] + ); + const { + value: selectTmbIds, + setValue: setSelectTmbIds, + isSelectAll: isSelectAllTmb, + setIsSelectAll: setIsSelectAllTmb + } = useMultipleSelect([], true); + + // chat + const [chatSearch, setChatSearch] = useState(''); + + // log keys + const [logKeys = DefaultAppLogKeys, setLogKeys] = useLocalStorageState( + `app_log_keys_${appId}` + ); + const { runAsync: fetchLogKeys, data: teamLogKeys } = useRequest2( + async () => { + return getLogKeys({ appId }); + }, + { + manual: false, + refreshDeps: [appId], + onSuccess: (res) => { + if (logKeys.length > 0) return; + if (res.logKeys.length > 0) { + setLogKeys(res.logKeys); + } else if (res.logKeys.length === 0) { + setLogKeys(DefaultAppLogKeys); + } + } + } + ); + const showSyncPopover = useMemo(() => { + const teamLogKeysList = ( + teamLogKeys?.logKeys?.length ? teamLogKeys?.logKeys : DefaultAppLogKeys + ).filter((item) => item.enable); + const personalLogKeysList = logKeys.filter((item) => item.enable); + return !isEqual(teamLogKeysList, personalLogKeysList); + }, [teamLogKeys, logKeys]); + + const { runAsync: exportLogs } = useRequest2( + async () => { + const enabledKeys = logKeys.filter((item) => item.enable).map((item) => item.key); + const headerTitle = enabledKeys.map((k) => t(AppLogKeysEnumMap[k])).join(','); + await downloadFetch({ + url: '/api/core/app/exportChatLogs', + filename: 'chat_logs.csv', + body: { + appId, + dateStart: dateRange.from || new Date(), + dateEnd: addDays(dateRange.to || new Date(), 1), + sources: isSelectAllSource ? undefined : chatSources, + tmbIds: isSelectAllTmb ? undefined : selectTmbIds, + chatSearch, + + title: headerTitle, + logKeys: enabledKeys, + sourcesMap: Object.fromEntries( + Object.entries(ChatSourceMap).map(([key, config]) => [ + key, + { + label: t(config.name as any) + } + ]) + ) + } + }); + }, + { + refreshDeps: [chatSources] + } + ); + const params = useMemo( + () => ({ + appId, + dateStart: dateRange.from!, + dateEnd: dateRange.to!, + sources: isSelectAllSource ? undefined : chatSources, + tmbIds: isSelectAllTmb ? undefined : selectTmbIds, + chatSearch + }), + [ + appId, + chatSources, + dateRange.from, + dateRange.to, + isSelectAllSource, + selectTmbIds, + isSelectAllTmb, + chatSearch + ] + ); + const { + data: logs, + isLoading, + Pagination, + getData, + pageNum, + total + } = usePagination(getAppChatLogs, { + pageSize: 20, + params, + refreshDeps: [params] + }); + + const HeaderRenderMap = useMemo( + () => ({ + [AppLogKeysEnum.SOURCE]: {t('app:logs_keys_source')}, + [AppLogKeysEnum.CREATED_TIME]: ( + {t('app:logs_keys_createdTime')} + ), + [AppLogKeysEnum.LAST_CONVERSATION_TIME]: ( + + {t('app:logs_keys_lastConversationTime')} + + ), + [AppLogKeysEnum.USER]: {t('app:logs_chat_user')}, + [AppLogKeysEnum.TITLE]: {t('app:logs_title')}, + [AppLogKeysEnum.SESSION_ID]: ( + {t('app:logs_keys_sessionId')} + ), + [AppLogKeysEnum.MESSAGE_COUNT]: ( + {t('app:logs_message_total')} + ), + [AppLogKeysEnum.FEEDBACK]: {t('app:feedback_count')}, + [AppLogKeysEnum.CUSTOM_FEEDBACK]: ( + + {t('common:core.app.feedback.Custom feedback')} + + ), + [AppLogKeysEnum.ANNOTATED_COUNT]: ( + + + {t('app:mark_count')} + + + + ), + [AppLogKeysEnum.RESPONSE_TIME]: ( + {t('app:logs_response_time')} + ), + [AppLogKeysEnum.ERROR_COUNT]: ( + {t('app:logs_error_count')} + ), + [AppLogKeysEnum.POINTS]: {t('app:logs_points')} + }), + [t] + ); + + const getCellRenderMap = (item: AppLogsListItemType) => ({ + [AppLogKeysEnum.SOURCE]: ( + + {/* @ts-ignore */} + {item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source} + + ), + [AppLogKeysEnum.CREATED_TIME]: ( + {dayjs(item.createTime).format('YYYY/MM/DD HH:mm')} + ), + [AppLogKeysEnum.LAST_CONVERSATION_TIME]: ( + + {dayjs(item.updateTime).format('YYYY/MM/DD HH:mm')} + + ), + [AppLogKeysEnum.USER]: ( + + + {!!item.outLinkUid ? item.outLinkUid : } + + + ), + [AppLogKeysEnum.TITLE]: ( + + {item.customTitle || item.title} + + ), + [AppLogKeysEnum.SESSION_ID]: ( + + {item.id || '-'} + + ), + [AppLogKeysEnum.MESSAGE_COUNT]: {item.messageCount}, + [AppLogKeysEnum.FEEDBACK]: ( + + {!!item?.userGoodFeedbackCount && ( + + + {item.userGoodFeedbackCount} + + )} + {!!item?.userBadFeedbackCount && ( + + + {item.userBadFeedbackCount} + + )} + {!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-} + + ), + [AppLogKeysEnum.CUSTOM_FEEDBACK]: ( + {item.customFeedbacksCount || '-'} + ), + [AppLogKeysEnum.ANNOTATED_COUNT]: ( + {item.markCount} + ), + [AppLogKeysEnum.RESPONSE_TIME]: ( + + {item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}s` : '-'} + + ), + [AppLogKeysEnum.ERROR_COUNT]: ( + {item.errorCount || '-'} + ), + [AppLogKeysEnum.POINTS]: ( + + {item.totalPoints ? `${item.totalPoints.toFixed(2)}` : '-'} + + ) + }); + + return ( + + + {showSourceSelector && ( + + + list={sourceList} + value={chatSources} + onSelect={setChatSources} + isSelectAll={isSelectAllSource} + setIsSelectAll={setIsSelectAllSource} + h={10} + w={'200px'} + rounded={'8px'} + tagStyle={{ + px: 1, + py: 1, + borderRadius: 'sm', + bg: 'myGray.100', + color: 'myGray.900' + }} + borderColor={'myGray.200'} + formLabel={t('app:logs_source')} + formLabelFontSize={'sm'} + /> + + )} + + { + setDateRange(date); + }} + bg={'myGray.25'} + h={10} + flex={'0 1 250px'} + rounded={'8px'} + borderColor={'myGray.200'} + formLabel={t('app:logs_date')} + _hover={{ + borderColor: 'primary.300' + }} + /> + + {feConfigs?.isPlus && ( + + + list={tmbList} + value={selectTmbIds} + onSelect={(val) => { + setSelectTmbIds(val as string[]); + }} + ScrollData={TmbScrollData} + isSelectAll={isSelectAllTmb} + setIsSelectAll={setIsSelectAllTmb} + h={10} + w={' 226px'} + rounded={'8px'} + formLabelFontSize={'sm'} + formLabel={t('common:member')} + tagStyle={{ + px: 1, + borderRadius: 'sm', + bg: 'myGray.100', + w: '76px' + }} + inputValue={tmbInputValue} + setInputValue={setTmbInputValue} + /> + + )} + + + {t('common:chat')} + + + setChatSearch(e.target.value)} + fontSize={'sm'} + border={'none'} + pl={0} + _focus={{ + boxShadow: 'none' + }} + _placeholder={{ + fontSize: 'sm' + }} + /> + + + {showSyncPopover && ( + + )} + { + if (item.key === AppLogKeysEnum.SOURCE && !showSourceSelector) return false; + return true; + })} + setLogKeysList={setLogKeys} + /> + + {t('common:Export')}} + showCancel + content={t('app:logs_export_confirm_tip', { total })} + onConfirm={exportLogs} + /> + + + + + + + {logKeys + .filter((logKey) => logKey.enable) + .map((logKey) => HeaderRenderMap[logKey.key])} + + + + {logs.map((item) => { + const cellRenderMap = getCellRenderMap(item); + return ( + setDetailLogsId(item.id)} + > + {logKeys + .filter((logKey) => logKey.enable) + .map((logKey) => cellRenderMap[logKey.key])} + + ); + })} + +
+ {logs.length === 0 && !isLoading && } +
+ + + + + + {!!detailLogsId && ( + { + setDetailLogsId(undefined); + getData(pageNum); + }} + /> + )} +
+ ); +}; + +export default React.memo(LogTable); diff --git a/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx b/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx index 7a9d8379c..b16a85cf0 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/SyncLogKeysPopover.tsx @@ -9,6 +9,7 @@ import { updateLogKeys } from '@/web/core/app/api/log'; 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'; const SyncLogKeysPopover = ({ logKeys, @@ -19,7 +20,7 @@ const SyncLogKeysPopover = ({ logKeys: AppLogKeysType[]; setLogKeys: (logKeys: AppLogKeysType[]) => void; teamLogKeys: AppLogKeysType[]; - fetchLogKeys: () => Promise; + fetchLogKeys: () => Promise; }) => { const { t } = useTranslation(); const appId = useContextSelector(AppContext, (v) => v.appId); diff --git a/projects/app/src/pageComponents/app/detail/Logs/index.tsx b/projects/app/src/pageComponents/app/detail/Logs/index.tsx index 34c55c14c..222e508ce 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/index.tsx @@ -1,59 +1,23 @@ -import React, { useMemo, useState } from 'react'; -import { - Flex, - Box, - TableContainer, - Table, - Thead, - Tr, - Th, - Td, - Tbody, - HStack, - Button, - Input -} from '@chakra-ui/react'; -import UserBox from '@fastgpt/web/components/common/UserBox'; +import React, { useState } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import LogTable from './LogTable'; +import LogChart from './LogChart'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'next-i18next'; -import { getAppChatLogs } from '@/web/core/app/api/log'; -import dayjs from 'dayjs'; -import { ChatSourceEnum, ChatSourceMap } from '@fastgpt/global/core/chat/constants'; +import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; import { addDays } from 'date-fns'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import DateRangePicker, { - type DateRangeType -} from '@fastgpt/web/components/common/DateRangePicker'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import ProTag from '@/components/ProTip/Tag'; +import ProText from '@/components/ProTip/ProText'; import { useContextSelector } from 'use-context-selector'; -import { AppContext } from '../context'; -import { cardStyles } from '../constants'; -import dynamic from 'next/dynamic'; -import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import MultipleSelect, { - useMultipleSelect -} from '@fastgpt/web/components/common/MySelect/MultipleSelect'; -import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { downloadFetch } from '@/web/common/system/utils'; -import LogKeysConfigPopover from './LogKeysConfigPopover'; -import { getLogKeys } from '@/web/core/app/api/log'; -import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants'; -import { DefaultAppLogKeys } from '@fastgpt/global/core/app/logs/constants'; -import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; -import { getTeamMembers } from '@/web/support/user/team/api'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useLocalStorageState } from 'ahooks'; -import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; -import type { AppLogsListItemType } from '@/types/app'; -import SyncLogKeysPopover from './SyncLogKeysPopover'; -import { isEqual } from 'lodash'; - -const DetailLogsModal = dynamic(() => import('./DetailLogsModal')); +import { AppContext } from '@/pageComponents/app/detail/context'; const Logs = () => { const { t } = useTranslation(); - + const { feConfigs } = useSystemStore(); + const [viewMode, setViewMode] = useState<'chart' | 'table'>(feConfigs.isPlus ? 'chart' : 'table'); const appId = useContextSelector(AppContext, (v) => v.appId); const [dateRange, setDateRange] = useState({ @@ -61,107 +25,6 @@ const Logs = () => { to: new Date(new Date().setHours(23, 59, 59, 999)) }); - const [detailLogsId, setDetailLogsId] = useState(); - const [tmbInputValue, setTmbInputValue] = useState(''); - const [chatSearch, setChatSearch] = useState(''); - - const getCellRenderMap = (item: AppLogsListItemType) => ({ - [AppLogKeysEnum.SOURCE]: ( - - {/* @ts-ignore */} - {item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source} - - ), - [AppLogKeysEnum.CREATED_TIME]: ( - {dayjs(item.createTime).format('YYYY/MM/DD HH:mm')} - ), - [AppLogKeysEnum.LAST_CONVERSATION_TIME]: ( - - {dayjs(item.updateTime).format('YYYY/MM/DD HH:mm')} - - ), - [AppLogKeysEnum.USER]: ( - - - {!!item.outLinkUid ? item.outLinkUid : } - - - ), - [AppLogKeysEnum.TITLE]: ( - - {item.customTitle || item.title} - - ), - [AppLogKeysEnum.SESSION_ID]: ( - - {item.id || '-'} - - ), - [AppLogKeysEnum.MESSAGE_COUNT]: {item.messageCount}, - [AppLogKeysEnum.FEEDBACK]: ( - - {!!item?.userGoodFeedbackCount && ( - - - {item.userGoodFeedbackCount} - - )} - {!!item?.userBadFeedbackCount && ( - - - {item.userBadFeedbackCount} - - )} - {!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-} - - ), - [AppLogKeysEnum.CUSTOM_FEEDBACK]: ( - {item.customFeedbacksCount || '-'} - ), - [AppLogKeysEnum.ANNOTATED_COUNT]: ( - {item.markCount} - ), - [AppLogKeysEnum.RESPONSE_TIME]: ( - - {item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}s` : '-'} - - ), - [AppLogKeysEnum.ERROR_COUNT]: ( - {item.errorCount || '-'} - ), - [AppLogKeysEnum.POINTS]: ( - - {item.totalPoints ? `${item.totalPoints.toFixed(2)}` : '-'} - - ) - }); - - const { - value: selectTmbIds, - setValue: setSelectTmbIds, - isSelectAll: isSelectAllTmb, - setIsSelectAll: setIsSelectAllTmb - } = useMultipleSelect([], true); - const { value: chatSources, setValue: setChatSources, @@ -169,334 +32,88 @@ const Logs = () => { setIsSelectAll: setIsSelectAllSource } = useMultipleSelect(Object.values(ChatSourceEnum), true); - const sourceList = useMemo( - () => - Object.entries(ChatSourceMap).map(([key, value]) => ({ - label: t(value.name as any), - value: key as ChatSourceEnum - })), - [t] - ); - - const params = useMemo( - () => ({ - appId, - dateStart: dateRange.from!, - dateEnd: dateRange.to!, - sources: isSelectAllSource ? undefined : chatSources, - tmbIds: isSelectAllTmb ? undefined : selectTmbIds, - chatSearch - }), - [ - appId, - chatSources, - dateRange.from, - dateRange.to, - isSelectAllSource, - selectTmbIds, - isSelectAllTmb, - chatSearch - ] - ); - const { - data: logs, - isLoading, - Pagination, - getData, - pageNum, - total - } = usePagination(getAppChatLogs, { - pageSize: 20, - params, - refreshDeps: [params] - }); - - const [logKeys = DefaultAppLogKeys, setLogKeys] = useLocalStorageState( - `app_log_keys_${appId}` - ); - const { runAsync: fetchLogKeys, data: teamLogKeys = [] } = useRequest2( - async () => { - const res = await getLogKeys({ appId }); - const keys = res.logKeys.length > 0 ? res.logKeys : DefaultAppLogKeys; - setLogKeys(keys); - return keys; - }, - { - manual: false, - refreshDeps: [appId] - } - ); - - const HeaderRenderMap = useMemo( - () => ({ - [AppLogKeysEnum.SOURCE]: {t('app:logs_keys_source')}, - [AppLogKeysEnum.CREATED_TIME]: ( - {t('app:logs_keys_createdTime')} - ), - [AppLogKeysEnum.LAST_CONVERSATION_TIME]: ( - - {t('app:logs_keys_lastConversationTime')} - - ), - [AppLogKeysEnum.USER]: {t('app:logs_chat_user')}, - [AppLogKeysEnum.TITLE]: {t('app:logs_title')}, - [AppLogKeysEnum.SESSION_ID]: ( - {t('app:logs_keys_sessionId')} - ), - [AppLogKeysEnum.MESSAGE_COUNT]: ( - {t('app:logs_message_total')} - ), - [AppLogKeysEnum.FEEDBACK]: {t('app:feedback_count')}, - [AppLogKeysEnum.CUSTOM_FEEDBACK]: ( - - {t('common:core.app.feedback.Custom feedback')} - - ), - [AppLogKeysEnum.ANNOTATED_COUNT]: ( - - - {t('app:mark_count')} - - - - ), - [AppLogKeysEnum.RESPONSE_TIME]: ( - {t('app:logs_response_time')} - ), - [AppLogKeysEnum.ERROR_COUNT]: ( - {t('app:logs_error_count')} - ), - [AppLogKeysEnum.POINTS]: {t('app:logs_points')} - }), - [t] - ); - - const { runAsync: exportLogs } = useRequest2( - async () => { - await downloadFetch({ - url: '/api/core/app/exportChatLogs', - filename: 'chat_logs.csv', - body: { - appId, - dateStart: dateRange.from || new Date(), - dateEnd: addDays(dateRange.to || new Date(), 1), - sources: isSelectAllSource ? undefined : chatSources, - tmbIds: isSelectAllTmb ? undefined : selectTmbIds, - chatSearch, - - title: t('app:logs_export_title'), - sourcesMap: Object.fromEntries( - Object.entries(ChatSourceMap).map(([key, config]) => [ - key, - { - label: t(config.name as any) - } - ]) - ) - } - }); - }, - { - refreshDeps: [chatSources] - } - ); - - const { data: members, ScrollData: TmbScrollData } = useScrollPagination(getTeamMembers, { - params: { searchKey: tmbInputValue }, - refreshDeps: [tmbInputValue] - }); - const tmbList = useMemo( - () => - members.map((item) => ({ - label: ( - - - - {item.memberName} - - - ), - value: item.tmbId - })), - [members] - ); - - const showSyncPopover = useMemo(() => { - const teamLogKeysList = teamLogKeys.filter((item) => item.enable); - const personalLogKeysList = logKeys.filter((item) => item.enable); - return !isEqual(teamLogKeysList, personalLogKeysList); - }, [teamLogKeys, logKeys]); - return ( - - - - list={sourceList} - value={chatSources} - onSelect={setChatSources} - isSelectAll={isSelectAllSource} - setIsSelectAll={setIsSelectAllSource} - h={9} - w={'226px'} - rounded={'8px'} - tagStyle={{ - px: 1, - py: 1, - borderRadius: 'sm', - bg: 'myGray.100', - color: 'myGray.900' - }} - borderColor={'myGray.200'} - formLabel={t('app:logs_source')} - /> - - - { - setDateRange(date); - }} - bg={'white'} - h={9} - w={'240px'} - rounded={'8px'} - borderColor={'myGray.200'} - formLabel={t('app:logs_date')} - _hover={{ - borderColor: 'primary.300' - }} - /> - - - - list={tmbList} - value={selectTmbIds} - onSelect={(val) => { - setSelectTmbIds(val as string[]); - }} - ScrollData={TmbScrollData} - isSelectAll={isSelectAllTmb} - setIsSelectAll={setIsSelectAllTmb} - h={9} - w={'226px'} - rounded={'8px'} - formLabel={t('common:member')} - tagStyle={{ - px: 1, - borderRadius: 'sm', - bg: 'myGray.100', - w: '76px' - }} - inputValue={tmbInputValue} - setInputValue={setTmbInputValue} - /> - + - - {t('common:chat')} - - - setChatSearch(e.target.value)} - fontSize={'sm'} - border={'none'} - pl={0} - _focus={{ - boxShadow: 'none' - }} - _placeholder={{ - fontSize: 'sm' - }} - /> + + setViewMode('chart')} + borderRadius={'8px'} + bg={viewMode === 'chart' ? 'myGray.05' : 'transparent'} + _hover={{ bg: 'myGray.05' }} + > + + + {t('app:logs_app_data')} + + + + setViewMode('table')} + gap={2} + borderRadius={'8px'} + bg={viewMode === 'table' ? 'myGray.05' : 'transparent'} + _hover={{ bg: 'myGray.05' }} + > + + {t('app:log_detail')} + + + {viewMode === 'chart' && !feConfigs.isPlus && ( + + + + {t('common:upgrade')} + + + + + )} - - {showSyncPopover && ( - - )} - - - {t('common:Export')}} - showCancel - content={t('app:logs_export_confirm_tip', { total })} - onConfirm={exportLogs} - /> - - - - - - {(logKeys || DefaultAppLogKeys) - .filter((logKey) => logKey.enable) - .map((logKey) => HeaderRenderMap[logKey.key])} - - - - {logs.map((item) => { - const cellRenderMap = getCellRenderMap(item); - return ( - setDetailLogsId(item.id)} - > - {(logKeys || DefaultAppLogKeys) - .filter((logKey) => logKey.enable) - .map((logKey) => cellRenderMap[logKey.key])} - - ); - })} - -
- {logs.length === 0 && !isLoading && } -
- - - - - - {!!detailLogsId && ( - { - setDetailLogsId(undefined); - getData(pageNum); - }} + chatSources={chatSources} + setChatSources={setChatSources} + isSelectAllSource={isSelectAllSource} + setIsSelectAllSource={setIsSelectAllSource} + dateRange={dateRange} + setDateRange={setDateRange} + /> + ) : ( + )}
diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx index 7608cd94e..bba85b73b 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { AppContext } from '../context'; import { useContextSelector } from 'use-context-selector'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { type AppSchema } from '@fastgpt/global/core/app/type'; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx index 295c32876..65d4685dd 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx @@ -6,7 +6,7 @@ import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext'; import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext'; import { Box, Button, Flex, HStack } from '@chakra-ui/react'; import { cardStyles } from '../constants'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; import { useForm } from 'react-hook-form'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx index 8fe41de43..172e002fa 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx @@ -2,7 +2,7 @@ import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/rea import React, { useState } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { AppContext } from '../context'; import { useContextSelector } from 'use-context-selector'; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx index 3e82d9d79..fa944f022 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx @@ -1,6 +1,6 @@ import { Box, Button, Flex } from '@chakra-ui/react'; import FolderPath from '@/components/common/folder/Path'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useContextSelector } from 'use-context-selector'; import { AppContext } from '../context'; diff --git a/projects/app/src/pageComponents/app/detail/RouteTab.tsx b/projects/app/src/pageComponents/app/detail/RouteTab.tsx index 5f1d34eda..1a7fdf1ca 100644 --- a/projects/app/src/pageComponents/app/detail/RouteTab.tsx +++ b/projects/app/src/pageComponents/app/detail/RouteTab.tsx @@ -25,24 +25,36 @@ const RouteTab = () => { const tabList = useMemo( () => [ - { - label: - appDetail.type === AppTypeEnum.plugin ? t('app:setting_plugin') : t('app:setting_app'), - id: TabEnum.appEdit - }, + ...(appDetail.permission.hasWritePer + ? [ + { + label: + appDetail.type === AppTypeEnum.plugin + ? t('app:setting_plugin') + : t('app:setting_app'), + id: TabEnum.appEdit + } + ] + : []), ...(appDetail.permission.hasManagePer ? [ { label: t('app:publish_channel'), id: TabEnum.publish - }, - ...(appDetail.permission.hasReadChatLogPer - ? [{ label: t('app:chat_logs'), id: TabEnum.logs }] - : []) + } ] + : []), + ...(appDetail.permission.hasReadChatLogPer + ? [{ label: t('app:chat_logs'), id: TabEnum.logs }] : []) ], - [appDetail.permission.hasManagePer, appDetail.type] + [ + appDetail.permission.hasManagePer, + appDetail.permission.hasReadChatLogPer, + appDetail.permission.hasWritePer, + appDetail.type, + t + ] ); return ( diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx index f2cafa545..8cc73d1ba 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx @@ -120,13 +120,15 @@ const ConfigToolModal = ({ {isOpenSecretModal && ( { onChange(data); diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx index ccda4ea86..213b4897f 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx @@ -47,6 +47,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { workflowStartNodeId } from '@/web/core/app/constants'; import ConfigToolModal from './ConfigToolModal'; +import CostTooltip from '@/components/core/app/plugin/CostTooltip'; type Props = { selectedTools: FlowNodeTemplateType[]; @@ -177,7 +178,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) onChange={(e) => setSearchKey(e.target.value)} placeholder={ templateType === TemplateTypeEnum.systemPlugin - ? t('common:plugin.Search plugin') + ? t('common:search_tool') : t('app:search_app') } /> diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/index.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/index.tsx index 773f5a218..0737a1a78 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/index.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/index.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils'; import Header from './Header'; -import Edit from './Edit'; import { useContextSelector } from 'use-context-selector'; import { AppContext, TabEnum } from '../context'; import dynamic from 'next/dynamic'; @@ -13,6 +12,7 @@ import { useDebounceEffect, useMount } from 'ahooks'; import { v1Workflow2V2 } from '@/web/core/workflow/adapt'; import { getAppConfigByDiff } from '@/web/core/app/diff'; +const Edit = dynamic(() => import('./Edit')); const Logs = dynamic(() => import('../Logs/index')); const PublishChannel = dynamic(() => import('../Publish')); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx index f686df642..8716f6343 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement } from '@chakra-ui/react'; import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useRouter } from 'next/router'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; @@ -141,7 +141,7 @@ const NodeTemplateListHeader = ({ placeholder={ templateType === TemplateTypeEnum.teamPlugin ? t('common:plugin.Search_app') - : t('common:plugin.Search plugin') + : t('common:search_tool') } value={searchKey} onChange={(e) => setSearchKey(e.target.value)} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx index 684a5f58a..58baf6a29 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx @@ -11,7 +11,7 @@ import { HStack, css } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { getPluginGroups, getPreviewPluginNode } from '@/web/core/app/api/plugin'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; @@ -72,6 +72,7 @@ const NodeTemplateListItem = ({ const { t } = useTranslation(); const { screenToFlowPosition } = useReactFlow(); const handleParams = useContextSelector(WorkflowEventContext, (v) => v.handleParams); + const isToolHandle = handleParams?.handleId === 'selectedTools'; return ( {t(template.intro as any) || t('common:core.workflow.Not intro')} - {/* {templateType === TemplateTypeEnum.systemPlugin && ( - - )} */} + } shouldWrapChildren={false} @@ -127,6 +130,16 @@ const NodeTemplateListItem = ({ }); }} onClick={() => { + // Not tool handle, cannot add toolset + if (!isToolHandle && template.flowNodeType === FlowNodeTypeEnum.toolSet) { + onUpdateParentId(template.id); + return; + } + // Team folder + if (template.isFolder && template.flowNodeType === FlowNodeTypeEnum.pluginModule) { + onUpdateParentId(template.id); + return; + } const position = isPopover && handleParams ? handleParams.addNodePosition @@ -152,7 +165,6 @@ const NodeTemplateListItem = ({ > {t(template.name as any)} - {/* Folder right arrow */} {template.isFolder && ( )} - {/* Author */} {!isPopover && template.authorAvatar && template.author && ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx index 0fbaef78e..85199e848 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx @@ -61,10 +61,12 @@ const ToolConfig = ({ nodeId, inputs }: { nodeId?: string; inputs?: FlowNodeInpu {isOpen && ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeTool.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeTool.tsx index 4b071a2f9..dbdc8b43c 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeTool.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeTool.tsx @@ -4,7 +4,7 @@ import { type NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import IOTitle from '../components/IOTitle'; import Container from '../components/Container'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import RenderOutput from './render/RenderOutput'; import RenderInput from './render/RenderInput'; import { useContextSelector } from 'use-context-selector'; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx index 86230a3f0..c8b9a9d81 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx @@ -4,7 +4,7 @@ import { type NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import IOTitle from '../components/IOTitle'; import Container from '../components/Container'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { Box, Flex } from '@chakra-ui/react'; const NodeToolSet = ({ data, selected }: NodeProps) => { @@ -12,6 +12,7 @@ const NodeToolSet = ({ data, selected }: NodeProps) => { const { toolConfig } = data; const toolList = toolConfig?.mcpToolSet?.toolList ?? toolConfig?.systemToolSet?.toolList ?? []; + return ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/Handle/index.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/Handle/index.tsx index 2565baccf..244ee9c5c 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/Handle/index.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/Handle/index.tsx @@ -10,7 +10,7 @@ import { } from '../../../../context/workflowInitContext'; import { WorkflowEventContext } from '../../../../context/workflowEventContext'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { Box, Flex } from '@chakra-ui/react'; const handleSizeConnected = 16; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 5a92fd124..eef2fee21 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -113,6 +113,7 @@ const NodeCard = (props: Props) => { return { node, parentNode }; }, [nodeList, nodeId]); + const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType]; const showVersion = useMemo(() => { // 1. MCP tool set do not have version @@ -409,6 +410,7 @@ const NodeCard = (props: Props) => { {inputConfig && isOpenToolParamConfigModal && ( { onChangeNode({ @@ -425,7 +427,8 @@ const NodeCard = (props: Props) => { courseUrl={node?.courseUrl} inputConfig={inputConfig} hasSystemSecret={node?.hasSystemSecret} - secretCost={node?.currentCost} + parentId={node?.pluginId} + secretCost={node?.systemKeyCost} /> )}
diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 57da4a556..e4121b88f 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -275,24 +275,30 @@ const MultipleReferenceSelector = ({ 0 ? ( - + {invalidList.map(({ nodeName, outputName }, index) => { return ( - + {nodeName} { const { data: appLatestVersion, run: reloadAppLatestVersion } = useRequest2( () => getAppLatestVersion({ appId }), { - manual: false + manual: !appDetail?.permission?.hasWritePer, + refreshDeps: [appDetail?.permission?.hasWritePer] } ); @@ -161,6 +162,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => { const { runAsync: onSaveApp } = useRequest2(async (data: PostPublishAppProps) => { try { + if (!appDetail.permission.hasWritePer) return; await postPublishApp(appId, data); setAppDetail((state) => ({ ...state, diff --git a/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx b/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx index 922cb92d0..5f69d986b 100644 --- a/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx +++ b/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx @@ -1,9 +1,18 @@ -import { Box, Button, Flex, HStack, Input, ModalBody, ModalFooter } from '@chakra-ui/react'; +import { + Box, + Button, + Flex, + HStack, + Input, + ModalBody, + ModalFooter, + useDisclosure +} from '@chakra-ui/react'; import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio'; import { useTranslation } from 'next-i18next'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import type { FlowNodeInputItemType, InputConfigType } from '@fastgpt/global/core/workflow/type/io'; @@ -13,6 +22,9 @@ import IconButton from '@/pageComponents/account/team/OrgManage/IconButton'; import MyModal from '@fastgpt/web/components/common/MyModal'; import InputRender from '@/components/core/app/formRender'; import { secretInputTypeToInputType } from '@/components/core/app/formRender/utils'; +import { getSystemPlugTemplates } from '@/web/core/app/api/plugin'; +import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; export type ToolParamsFormType = { type: SystemToolInputTypeEnum; @@ -20,13 +32,17 @@ export type ToolParamsFormType = { }; const SecretInputModal = ({ + parentId, hasSystemSecret, secretCost = 0, + isFolder, inputConfig, courseUrl, onClose, onSubmit }: { + parentId?: string; + isFolder?: boolean; inputConfig: FlowNodeInputItemType; hasSystemSecret?: boolean; secretCost?: number; @@ -36,6 +52,9 @@ const SecretInputModal = ({ }) => { const { t } = useTranslation(); const [editIndex, setEditIndex] = useState(); + const { isOpen: isSystemCostOpen, onToggle: onToggleSystemCost } = useDisclosure({ + defaultIsOpen: false + }); const inputList = inputConfig?.inputList || []; const { register, watch, setValue, getValues, handleSubmit, control } = @@ -59,6 +78,24 @@ const SecretInputModal = ({ }); const configType = watch('type'); + const { data: childTools = [] } = useRequest2( + async () => { + if (!isFolder) return []; + return getSystemPlugTemplates({ parentId }); + }, + { + manual: false, + refreshDeps: [isFolder, parentId] + } + ); + + const hasCost = useMemo(() => { + if (isFolder) { + return childTools.some((item) => (item.systemKeyCost || 0) > 0); + } + return secretCost > 0; + }, [isFolder, childTools, secretCost]); + return ( - - - {t('app:tool_active_system_config_price_desc', { - price: secretCost || 0 - })} - - + configType === SystemToolInputTypeEnum.system && hasCost ? ( + + {isFolder ? ( + <> + + + + {t('app:tool_active_system_config_price_desc_folder')} + + + + {isSystemCostOpen && ( + + {childTools.map((tool) => ( + + {t(tool.name as any)}: {tool.systemKeyCost || 0} 积分/次 + + ))} + + )} + + ) : ( + + + + {t('app:tool_active_system_config_price_desc', { + price: secretCost + })} + + + )} + ) : null } ] diff --git a/projects/app/src/pageComponents/chat/ChatHeader.tsx b/projects/app/src/pageComponents/chat/ChatHeader.tsx index 8ac25e39b..528a5edd3 100644 --- a/projects/app/src/pageComponents/chat/ChatHeader.tsx +++ b/projects/app/src/pageComponents/chat/ChatHeader.tsx @@ -24,6 +24,12 @@ import SelectOneResource from '@/components/common/folder/SelectOneResource'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { + ChatSidebarPaneEnum, + DEFAULT_LOGO_BANNER_COLLAPSED_URL +} from '@/pageComponents/chat/constants'; +import { useChatStore } from '@/web/core/chat/context/useChatStore'; const ChatHeader = ({ history, @@ -41,6 +47,10 @@ const ChatHeader = ({ const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); + + const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const isPlugin = chatData.app.type === AppTypeEnum.plugin; const router = useRouter(); const isChat = router.pathname === '/chat'; @@ -68,8 +78,16 @@ const ChatHeader = ({ )} @@ -98,8 +116,9 @@ const MobileDrawer = ({ app = 'app' } const { t } = useTranslation(); - const router = useRouter(); - const isTeamChat = router.pathname === '/chat/team'; + + const { setChatId } = useChatStore(); + const [currentTab, setCurrentTab] = useState(TabEnum.recently); const getAppList = useCallback(async ({ parentId }: GetResourceFolderListProps) => { @@ -114,9 +133,13 @@ const MobileDrawer = ({ }, []); const { onChangeAppId } = useContextSelector(ChatContext, (v) => v); + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const onclickApp = (id: string) => { + handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS); onChangeAppId(id); onCloseDrawer(); + setChatId(); }; return ( @@ -147,12 +170,8 @@ const MobileDrawer = ({ px: 2 }} list={[ - ...(isTeamChat - ? [{ label: t('app:all_apps'), value: TabEnum.recently }] - : [ - { label: t('common:core.chat.Recent use'), value: TabEnum.recently }, - { label: t('app:all_apps'), value: TabEnum.app } - ]) + { label: t('common:core.chat.Recent use'), value: TabEnum.recently }, + { label: t('app:all_apps'), value: TabEnum.app } ]} value={currentTab} onChange={setCurrentTab} @@ -236,14 +255,22 @@ const MobileHeader = ({ return ( <> {showHistory && ( - + )} + {name} + {isShareChat ? null : ( + {isOpenDrawer && !isShareChat && ( )} diff --git a/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx b/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx index 831d93034..eacc0baff 100644 --- a/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx +++ b/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx @@ -1,12 +1,10 @@ import React, { useMemo } from 'react'; -import { Box, Button, Flex, useTheme, IconButton } from '@chakra-ui/react'; +import { Grid, Image, Box, Button, Flex, useTheme, IconButton, GridItem } from '@chakra-ui/react'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useEditTitle } from '@/web/common/hooks/useEditTitle'; -import { useRouter } from 'next/router'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'next-i18next'; -import { useUserStore } from '@/web/support/user/useUserStore'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -15,6 +13,12 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatSidebarPaneEnum, DEFAULT_LOGO_BANNER_URL } from '@/pageComponents/chat/constants'; +import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { useMemoizedFn } from 'ahooks'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import UserAvatarPopover from '@/pageComponents/chat/UserAvatarPopover'; type HistoryItemType = { id: string; @@ -24,25 +28,38 @@ type HistoryItemType = { updateTime: Date; }; -const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) => { +const ChatHistorySlider = ({ + confirmClearText, + customSliderTitle +}: { + confirmClearText: string; + customSliderTitle?: string; +}) => { const theme = useTheme(); - const { t } = useTranslation(); - const { isPc } = useSystem(); - const { appId, chatId: activeChatId } = useChatStore(); + const { userInfo } = useUserStore(); + + const { chatId: activeChatId, setChatId } = useChatStore(); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const ScrollData = useContextSelector(ChatContext, (v) => v.ScrollData); const histories = useContextSelector(ChatContext, (v) => v.histories); const onDelHistory = useContextSelector(ChatContext, (v) => v.onDelHistory); const onClearHistory = useContextSelector(ChatContext, (v) => v.onClearHistories); const onUpdateHistory = useContextSelector(ChatContext, (v) => v.onUpdateHistory); + const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name); const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); + const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + + const isActivePane = useMemoizedFn((active: ChatSidebarPaneEnum) => active === pane); + const concatHistory = useMemo(() => { const formatHistories: HistoryItemType[] = histories.map((item) => { return { @@ -80,14 +97,102 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) = whiteSpace={'nowrap'} > {isPc && ( - - - - {appName} + + {!customSliderTitle && } + + + {customSliderTitle || appName} )} + {!isPc && ( + <> + + banner + + + + + + { + handlePaneChange(ChatSidebarPaneEnum.HOME); + onCloseSlider(); + setChatId(); + }} + > + + + + {t('chat:sidebar.home')} + + + + + { + handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); + onCloseSlider(); + }} + > + + + + {t('chat:sidebar.team_apps')} + + + + + + + + )} + {/* menu */} + {!isPc && ( + + + + + + {userInfo?.username} + + + + + { + handlePaneChange(ChatSidebarPaneEnum.SETTING); + onCloseSlider(); + }} + > + + + + )} +
); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx new file mode 100644 index 000000000..4f70f74eb --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx @@ -0,0 +1,56 @@ +import LogChart from '@/pageComponents/app/detail/Logs/LogChart'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { Flex } from '@chakra-ui/react'; +import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; +import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import { addDays } from 'date-fns'; +import React, { useState } from 'react'; +import { useContextSelector } from 'use-context-selector'; + +type Props = { + Header: React.FC<{ children?: React.ReactNode }>; +}; + +const LogDetails = ({ Header }: Props) => { + const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + + const [dateRange, setDateRange] = useState({ + from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), + to: new Date(new Date().setHours(23, 59, 59, 999)) + }); + + const { + value: chatSources, + setValue: setChatSources, + isSelectAll: isSelectAllSource, + setIsSelectAll: setIsSelectAllSource + } = useMultipleSelect(Object.values(ChatSourceEnum), true); + + return ( + +
+ + + + ); +}; + +export default React.memo(LogDetails); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/DiagramModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/DiagramModal.tsx new file mode 100644 index 000000000..024be4614 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/DiagramModal.tsx @@ -0,0 +1,32 @@ +import { Flex, Image } from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useTranslation } from 'react-i18next'; + +type Props = { + show: boolean; + onShow: (show: boolean) => void; +}; + +const DiagramModal = ({ show, onShow }: Props) => { + const { t } = useTranslation(); + + return ( + onShow(false)} + > + + style diagram + + + ); +}; + +export default DiagramModal; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx new file mode 100644 index 000000000..c643d914e --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting.tsx @@ -0,0 +1,399 @@ +import { Box, Button, Flex, Grid, Input } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import MyInput from '@/components/MyInput'; +import { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { updateChatSetting } from '@/web/core/chat/api'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import ImageUpload from '@/pageComponents/chat/ChatSetting/ImageUpload'; +import type { + ChatSettingSchema, + ChatSettingUpdateParams +} 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'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { useMount } from 'ahooks'; +import { useContextSelector } from 'use-context-selector'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { + DEFAULT_LOGO_BANNER_COLLAPSED_URL, + DEFAULT_LOGO_BANNER_URL +} from '@/pageComponents/chat/constants'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; + +type Props = { + Header: React.FC<{ children?: React.ReactNode }>; + onDiagramShow: (show: boolean) => void; +}; + +type FormValues = Omit & { + selectedTools: ChatSettingSchema['selectedTools']; +}; + +const HomepageSetting = ({ Header, onDiagramShow }: Props) => { + const { isPc } = useSystem(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + + const chatSettings2Form = useCallback( + (data?: ChatSettingSchema) => { + 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 + }; + }, + [t] + ); + + const { register, handleSubmit, reset, setValue, watch } = useForm({ + defaultValues: chatSettings2Form(chatSettings) + }); + + const wideLogoUrl = watch('wideLogoUrl'); + const squareLogoUrl = watch('squareLogoUrl'); + + useMount(async () => { + reset(chatSettings2Form(await refreshChatSetting())); + }); + + const [toolSelectModalOpen, setToolSelectModalOpen] = useState(false); + const selectedTools = watch('selectedTools'); + + const handleAddTool = useCallback( + async (tool: FlowNodeTemplateType) => { + if (!selectedTools.some((t) => t.pluginId === tool.pluginId)) { + const next = [ + ...selectedTools, + { + name: tool.name, + pluginId: tool.pluginId || '', + avatar: tool.avatar || '', + inputs: tool.inputs?.reduce( + (acc, input) => { + acc[input.key] = input.value; + return acc; + }, + {} as Record<`${NodeInputKeyEnum}` | string, any> + ) + } + ]; + setValue('selectedTools', next); + } + }, + [selectedTools, setValue] + ); + const handleRemoveToolById = useCallback( + (toolId?: string) => { + if (!toolId) return; + const next = selectedTools.filter((t) => t.pluginId !== toolId); + setValue('selectedTools', next); + }, + [selectedTools, setValue] + ); + + const { runAsync: onSubmit, loading: isSaving } = useRequest2( + async (values: FormValues) => { + return updateChatSetting({ + ...values, + selectedTools: values.selectedTools.map((tool) => ({ + pluginId: tool.pluginId, + inputs: tool.inputs + })) + }); + }, + { + onSuccess() { + refreshChatSetting(); + }, + successToast: t('chat:setting.save_success') + } + ); + + return ( + +
+ +
+ + + + + {/* AVAILABLE TOOLS */} + + + {t('chat:setting.home.available_tools')} + + {selectedTools.length > 0 && ( + + )} + + + {selectedTools.length === 0 && ( + setToolSelectModalOpen(true)} + > + + {t('chat:setting.home.available_tools.add')} + + )} + + {selectedTools.length > 0 && ( + + {selectedTools.map((tool) => ( + + + + {tool.name} + + handleRemoveToolById(tool.pluginId)} + /> + + ))} + + )} + + {toolSelectModalOpen && ( + handleRemoveToolById(tool.id)} + onClose={() => setToolSelectModalOpen(false)} + /> + )} + + + {/* SLOGAN */} + + + {t('chat:setting.home.slogan')} + + + + + + + + + + {/* DIALOGUE TIPS */} + + + {t('chat:setting.home.dialogue_tips')} + + + + + + + + + + {/* COPYRIGHT */} + {!feConfigs.hideChatCopyrightSetting && ( + <> + + + {t('chat:setting.copyright.copyright_configuration')} + + + + + + {t('chat:setting.home.home_tab_title')} + + + + + + + + + + {/* LOGO */} + + + {t('chat:setting.copyright.logo')} + + + + + + setValue('wideLogoUrl', url)} + /> + + {isPc && ( + + )} + + setValue('squareLogoUrl', url)} + /> + + + + )} + + + +
+ ); +}; + +export default HomepageSetting; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/hooks/useImageUpload.tsx b/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/hooks/useImageUpload.tsx new file mode 100644 index 000000000..d2470cce6 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/hooks/useImageUpload.tsx @@ -0,0 +1,124 @@ +import { useState, useRef } from 'react'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useTranslation } from 'next-i18next'; +import { useMemoizedFn } from 'ahooks'; +import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; +import { formatFileSize } from '@fastgpt/global/common/file/tools'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; + +export type UploadedFileItem = { + url: string; + file: File; +}; + +type UseImageUploadProps = { + maxSize?: number; // MB + onFileSelect: (url: string) => void; +}; + +export const useImageUpload = ({ maxSize, onFileSelect }: UseImageUploadProps) => { + const { toast } = useToast(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const [isDragging, setIsDragging] = useState(false); + const dragCounter = useRef(0); + + // use system config max size, but cap to match server limit + // server validates the base64 string length (12MB), the original file should be smaller because the base64 encoding will increase the size by about 33% + const configMaxSize = maxSize || feConfigs?.uploadFileMaxSize || 100; // MB + const serverLimitMB = 12; // server base64 limit + const clientLimitMB = Math.floor(serverLimitMB * 0.75); // considering the base64 encoding overhead, the client limit is set to 9MB + const finalMaxSize = Math.min(configMaxSize, clientLimitMB); + const maxSizeBytes = finalMaxSize * 1024 * 1024; + + const { + File: SelectFileComponent, + onOpen: onOpenSelectFile, + onSelectImage, + loading + } = useSelectFile({ + fileType: 'image/*', + multiple: false, + maxCount: 1 + }); + + // validate file size + const validateFile = useMemoizedFn((file: File): string | null => { + if (file.size > maxSizeBytes) { + return t('chat:setting.copyright.file_size_exceeds_limit', { + maxSize: formatFileSize(maxSizeBytes) + }); + } + return null; + }); + + // handle file select - immediate upload if enabled + const handleFileSelect = useMemoizedFn(async (files: File[]) => { + const file = files[0]; + + const validationError = validateFile(file); + if (validationError) { + toast({ + status: 'warning', + title: validationError + }); + } + + try { + // 立即上传文件,带TTL + const url = await onSelectImage([file], { maxW: 1000, maxH: 1000 }); + onFileSelect(url); + } catch (error) { + console.error('Failed to upload file:', error); + } + }); + + // 拖拽处理 + const handleDragEnter = useMemoizedFn((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current++; + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragging(true); + } + }); + + const handleDragLeave = useMemoizedFn((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current--; + if (dragCounter.current === 0) { + setIsDragging(false); + } + }); + + const handleDragOver = useMemoizedFn((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }); + + const handleDrop = useMemoizedFn(async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + dragCounter.current = 0; + + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + const files = Array.from(e.dataTransfer.files); + await handleFileSelect(files); + } + }); + + return { + SelectFileComponent, + onOpenSelectFile, + onSelectFile: handleFileSelect, + isDragging, + handleDragEnter, + handleDragLeave, + handleDragOver, + handleDrop, + loading + }; +}; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/index.tsx new file mode 100644 index 000000000..bf652689a --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/ImageUpload/index.tsx @@ -0,0 +1,124 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Image, Flex } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useImageUpload } from './hooks/useImageUpload'; +import { useMemoizedFn } from 'ahooks'; +import MyLoading from '@fastgpt/web/components/common/MyLoading'; + +type Props = { + imageSrc: string; + onFileSelect: (url: string) => void; + tips?: string; + maxSize?: number; // MB + width?: string | number; + height?: string | number; + aspectRatio?: number; + borderRadius?: string | number; + disabled?: boolean; +}; + +const ImageUpload = ({ + imageSrc, + tips, + maxSize, + width, + height, + aspectRatio = 2.84 / 1, + borderRadius = 'md', + disabled = false, + onFileSelect +}: Props) => { + const [isHovered, setIsHovered] = useState(false); + const [imageLoadError, setImageLoadError] = useState(false); + + // reset image load error when imageSrc changes + useEffect(() => { + setImageLoadError(false); + }, [imageSrc]); + + const { + SelectFileComponent, + onOpenSelectFile, + onSelectFile, + isDragging, + handleDragEnter, + handleDragLeave, + handleDragOver, + handleDrop, + loading + } = useImageUpload({ + maxSize, + onFileSelect + }); + + const handleClick = useMemoizedFn(() => { + if (!disabled && !loading) { + onOpenSelectFile(); + } + }); + + const renderUploadArea = () => { + if (loading) { + return ; + } + if (isHovered && !isDragging) { + return ; + } + + // show uploaded image + return ( + Uploaded image + ); + }; + + return ( + + + + !disabled && setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onDragEnter={!disabled ? handleDragEnter : undefined} + onDragLeave={!disabled ? handleDragLeave : undefined} + onDragOver={!disabled ? handleDragOver : undefined} + onDrop={!disabled ? handleDrop : undefined} + opacity={disabled ? 0.6 : 1} + > + + {renderUploadArea()} + + + + {tips && ( + + {tips} + + )} + + ); +}; + +export default ImageUpload; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx new file mode 100644 index 000000000..b17673ba4 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx @@ -0,0 +1,56 @@ +import LogTable from '@/pageComponents/app/detail/Logs/LogTable'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { Flex } from '@chakra-ui/react'; +import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; +import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import { addDays } from 'date-fns'; +import React, { useState } from 'react'; +import { useContextSelector } from 'use-context-selector'; + +type Props = { + Header: React.FC<{ children?: React.ReactNode }>; +}; + +const LogDetails = ({ Header }: Props) => { + const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + + const [dateRange, setDateRange] = useState({ + from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), + to: new Date(new Date().setHours(23, 59, 59, 999)) + }); + + const { + value: chatSources, + setValue: setChatSources, + isSelectAll: isSelectAllSource, + setIsSelectAll: setIsSelectAllSource + } = useMultipleSelect(Object.values(ChatSourceEnum), true); + + return ( + +
+ + + + ); +}; + +export default React.memo(LogDetails); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/SettingTabs.tsx b/projects/app/src/pageComponents/chat/ChatSetting/SettingTabs.tsx new file mode 100644 index 000000000..1ba8075c8 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/SettingTabs.tsx @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; +import { useTranslation } from 'react-i18next'; +import { ChatSettingTabOptionEnum } from '@/pageComponents/chat/constants'; +import { Flex } from '@chakra-ui/react'; + +type Props = { + tab: `${ChatSettingTabOptionEnum}`; + onChange: (tab: `${ChatSettingTabOptionEnum}`) => void; + children?: React.ReactNode; +}; + +const SettingTabs = ({ tab, children, onChange }: 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] + ); + + return ( + + + + {children} + + ); +}; + +export default SettingTabs; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx new file mode 100644 index 000000000..6892613c7 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx @@ -0,0 +1,479 @@ +import React, { useCallback, useMemo, useState } from 'react'; + +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useTranslation } from 'next-i18next'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Button, + css, + Flex, + Grid +} from '@chakra-ui/react'; +import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { + type FlowNodeTemplateType, + type NodeTemplateListItemType, + type NodeTemplateListType +} from '@fastgpt/global/core/workflow/type/node.d'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { + getPluginGroups, + getPreviewPluginNode, + getSystemPlugTemplates, + getSystemPluginPaths +} from '@/web/core/app/api/plugin'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import FolderPath from '@/components/common/folder/Path'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { useMemoizedFn } from 'ahooks'; +import MyAvatar from '@fastgpt/web/components/common/Avatar'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { type AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { workflowStartNodeId } from '@/web/core/app/constants'; +import ConfigToolModal from '@/pageComponents/app/detail/SimpleApp/components/ConfigToolModal'; +import type { ChatSettingSchema } from '@fastgpt/global/core/chat/setting/type'; + +type Props = { + selectedTools: ChatSettingSchema['selectedTools']; + chatConfig?: AppSimpleEditFormType['chatConfig']; + onAddTool: (tool: FlowNodeTemplateType) => void; + onRemoveTool: (tool: NodeTemplateListItemType) => void; +}; + +export const childAppSystemKey: string[] = [ + NodeInputKeyEnum.forbidStream, + NodeInputKeyEnum.history, + NodeInputKeyEnum.historyMaxAmount, + NodeInputKeyEnum.userChatInput +]; + +const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => { + const { t } = useTranslation(); + const [parentId, setParentId] = useState(''); + const [searchKey, setSearchKey] = useState(''); + + const { + data: templates = [], + runAsync: loadTemplates, + loading: isLoading + } = useRequest2( + async ({ + parentId = '', + searchVal = searchKey + }: { + parentId?: ParentIdType; + searchVal?: string; + }) => { + return getSystemPlugTemplates({ parentId, searchKey: searchVal }); + }, + { + onSuccess(_, [{ parentId = '' }]) { + setParentId(parentId); + }, + refreshDeps: [searchKey, parentId], + errorToast: t('common:core.module.templates.Load plugin error') + } + ); + + const { data: paths = [] } = useRequest2( + () => { + return getSystemPluginPaths({ sourceId: parentId, type: 'current' }); + }, + { + manual: false, + refreshDeps: [parentId] + } + ); + + const onUpdateParentId = useCallback( + (parentId: ParentIdType) => { + loadTemplates({ + parentId + }); + }, + [loadTemplates] + ); + + useRequest2(() => loadTemplates({ searchVal: searchKey }), { + manual: false, + throttleWait: 300, + refreshDeps: [searchKey] + }); + + return ( + + {/* Header: search */} + + + setSearchKey(e.target.value)} + placeholder={t('common:search_tool')} + /> + + + {/* route components */} + {!searchKey && parentId && ( + + + + )} + + + + + + + ); +}; + +export default React.memo(ToolSelectModal); + +const RenderList = React.memo(function RenderList({ + templates, + onAddTool, + onRemoveTool, + setParentId, + selectedTools, + chatConfig = {} +}: Props & { + templates: NodeTemplateListItemType[]; + setParentId: (parentId: ParentIdType) => any; +}) { + const { t } = useTranslation(); + const [configTool, setConfigTool] = useState(); + const onCloseConfigTool = useCallback(() => setConfigTool(undefined), []); + const { toast } = useToast(); + + const { runAsync: onClickAdd, loading: isLoading } = useRequest2( + async (template: NodeTemplateListItemType) => { + const res = await getPreviewPluginNode({ appId: template.id }); + + /* Invalid plugin check + 1. Reference type. but not tool description; + 2. Has dataset select + 3. Has dynamic external data + */ + const oneFileInput = + res.inputs.filter((input) => + input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) + ).length === 1; + const canUploadFile = + chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg; + const invalidFileInput = oneFileInput && !!canUploadFile; + if ( + res.inputs.some( + (input) => + (input.renderTypeList.length === 1 && + input.renderTypeList[0] === FlowNodeInputTypeEnum.reference && + !input.toolDescription) || + input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) || + input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) || + (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !invalidFileInput) + ) + ) { + return toast({ + title: t('app:simple_tool_tips'), + status: 'warning' + }); + } + + // 判断是否可以直接添加工具,满足以下任一条件: + // 1. 有工具描述 + // 2. 是模型选择类型 + // 3. 是文件上传类型且:已开启文件上传、非必填、只有一个文件上传输入 + const hasInputForm = + res.inputs.length > 0 && + res.inputs.some((input) => { + if (input.toolDescription) { + return false; + } + if (input.key === NodeInputKeyEnum.forbidStream) { + return false; + } + if (input.key === NodeInputKeyEnum.systemInputConfig) { + return true; + } + + // Check if input has any of the form render types + const formRenderTypes = [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.textarea, + FlowNodeInputTypeEnum.numberInput, + FlowNodeInputTypeEnum.switch, + FlowNodeInputTypeEnum.select, + FlowNodeInputTypeEnum.JSONEditor + ]; + + return formRenderTypes.some((type) => input.renderTypeList.includes(type)); + }); + + // 构建默认表单数据 + const defaultForm = { + ...res, + inputs: res.inputs.map((input) => { + // 如果是文件上传类型,设置为从工作流开始节点获取用户文件 + if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) { + return { + ...input, + value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]] + }; + } + return input; + }) + }; + + if (hasInputForm) { + setConfigTool(defaultForm); + } else { + onAddTool(defaultForm); + } + }, + { + errorToast: t('common:core.module.templates.Load plugin error') + } + ); + + const { data: pluginGroups = [] } = useRequest2(getPluginGroups, { + manual: false + }); + + const formatTemplatesArray = useMemo(() => { + const data = pluginGroups.map((group) => { + const copy: NodeTemplateListType = group.groupTypes.map((type) => ({ + list: [], + type: type.typeId, + label: type.typeName + })); + templates.forEach((item) => { + const index = copy.findIndex((template) => template.type === item.templateType); + if (index === -1) return; + copy[index].list.push(item); + }); + return { + label: group.groupName, + list: copy.filter((item) => item.list.length > 0) + }; + }); + + return data.filter(({ list }) => list.length > 0); + }, [pluginGroups, templates]); + + const gridStyle = { + gridTemplateColumns: ['1fr', '1fr 1fr'], + py: 3, + avatarSize: '1.75rem' + }; + + const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => { + return ( + <> + {list.map((item, i) => { + return ( + + + + {t(item.label as any)} + + + + {item.list.map((template) => { + const selected = selectedTools.some((tool) => tool.pluginId === template.id); + + return ( + + + + + {t(template.name as any)} + + + + {t(template.intro as any) || t('common:core.workflow.Not intro')} + + {/* {type === TemplateTypeEnum.systemPlugin && ( + + )} */} + + } + > + + + + {t(template.name as any)} + + + {selected ? ( + + ) : template.flowNodeType === 'toolSet' ? ( + + + + + ) : template.isFolder ? ( + + ) : ( + + )} + + + ); + })} + + + ); + })} + + ); + }); + + return templates.length === 0 ? ( + + ) : ( + <> + + {formatTemplatesArray.length > 1 ? ( + <> + {formatTemplatesArray.map(({ list, label }, index) => ( + + + {t(label as any)} + + + + + + + ))} + + ) : ( + + )} + + + {!!configTool && ( + + )} + + ); +}); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx new file mode 100644 index 000000000..b9dfc7cea --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx @@ -0,0 +1,90 @@ +import DiagramModal from '@/pageComponents/chat/ChatSetting/DiagramModal'; +import { useCallback, useState } from 'react'; +import { ChatSettingTabOptionEnum } from '@/pageComponents/chat/constants'; +import dynamic from 'next/dynamic'; +import SettingTabs from '@/pageComponents/chat/ChatSetting/SettingTabs'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { Drawer, DrawerContent, DrawerOverlay, Flex } from '@chakra-ui/react'; +import { useContextSelector } from 'use-context-selector'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import ChatHistorySlider from '@/pageComponents/chat/ChatHistorySlider'; +import { useTranslation } from 'react-i18next'; +import { ChatContext } from '@/web/core/chat/context/chatContext'; +import NextHead from '@/components/common/NextHead'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; + +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 ChatSetting = () => { + const { t } = useTranslation(); + const { isPc } = useSystem(); + + const [isOpenDiagram, setIsOpenDiagram] = useState(false); + const [tab, setTab] = useState<`${ChatSettingTabOptionEnum}`>('home'); + + const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider); + const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); + const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + + const SettingHeader = useCallback( + ({ children }: { children?: React.ReactNode }) => ( + <> + + {children} + + + ), + [tab, setTab] + ); + + return ( + <> + + + {!isPc && ( + + + + + + + + + + + )} + + {/* homepage setting */} + {tab === ChatSettingTabOptionEnum.HOME && ( + + )} + + {/* data dashboard */} + {tab === ChatSettingTabOptionEnum.DATA_DASHBOARD && } + + {/* log details */} + {tab === ChatSettingTabOptionEnum.LOG_DETAILS && } + + + + ); +}; + +export default ChatSetting; diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx new file mode 100644 index 000000000..b9e4e1e1d --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { Box, Grid, HStack } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useTranslation } from 'next-i18next'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useContextSelector } from 'use-context-selector'; +import { AppListContext } from '@/pageComponents/dashboard/apps/context'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import AppTypeTag from '@/pageComponents/chat/ChatTeamApp/TypeTag'; + +import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import UserBox from '@fastgpt/web/components/common/UserBox'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; + +const ListItem = ({ appType }: { appType: AppTypeEnum | 'all' }) => { + const { t } = useTranslation(); + const router = useRouter(); + const { isPc } = useSystem(); + + const myApps = useContextSelector(AppListContext, (v) => + v.myApps.filter( + (app) => + appType === app.type || + app.type === AppTypeEnum.folder || + (appType === 'all' && + [ + AppTypeEnum.folder, + AppTypeEnum.simple, + AppTypeEnum.workflow, + AppTypeEnum.plugin + ].includes(app.type)) + ) + ); + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + + return ( + <> + + {myApps.map((app) => { + return ( + + { + if (app.type === AppTypeEnum.folder) { + router.push({ + query: { + ...router.query, + parentId: app._id + } + }); + } else { + router.push({ + query: { + ...router.query, + appId: app._id + } + }); + handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS); + } + }} + > + + + + {app.name} + + + + + + + + {app.intro || t('common:no_intro')} + + + + + + + + {isPc && ( + + + + {t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')} + + + )} + + + + + ); + })} + + {myApps.length === 0 && } + + ); +}; +export default ListItem; diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/TypeTag.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/TypeTag.tsx new file mode 100644 index 000000000..9b6dec5a4 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/TypeTag.tsx @@ -0,0 +1,66 @@ +import React, { useRef } from 'react'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +const AppTypeTag = ({ type }: { type: AppTypeEnum }) => { + const { t } = useTranslation(); + + const map = useRef({ + [AppTypeEnum.simple]: { + label: t('app:type.Simple bot'), + icon: 'core/app/type/simple', + bg: '#DBF3FF', + color: '#0884DD' + }, + [AppTypeEnum.workflow]: { + label: t('app:type.Workflow bot'), + icon: 'core/app/type/workflow', + bg: '#E4E1FC', + color: '#6F5DD7' + }, + [AppTypeEnum.plugin]: { + label: t('app:type.Plugin'), + icon: 'core/app/type/plugin', + bg: '#D0F5EE', + color: '#007E7C' + }, + [AppTypeEnum.httpPlugin]: { + label: t('app:type.Http plugin'), + icon: 'core/app/type/httpPlugin', + bg: '#FFE4EE', + color: '#E82F72' + }, + [AppTypeEnum.toolSet]: { + label: t('app:type.MCP tools'), + icon: 'core/app/type/mcpTools', + bg: '', + color: '' + }, + [AppTypeEnum.tool]: undefined, + [AppTypeEnum.folder]: undefined, + [AppTypeEnum.hidden]: undefined + }); + + const data = map.current[type]; + + return data ? ( + + + + {data.label} + + + ) : null; +}; + +export default AppTypeTag; diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx new file mode 100644 index 000000000..3e8f8e86d --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx @@ -0,0 +1,173 @@ +'use client'; + +import React, { useMemo, useState } from 'react'; +import { Box, Flex, Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useContextSelector } from 'use-context-selector'; +import AppListContextProvider, { AppListContext } from '@/pageComponents/dashboard/apps/context'; +import FolderPath from '@/components/common/folder/Path'; +import { useRouter } from 'next/router'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import List from '@/pageComponents/chat/ChatTeamApp/List'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { Drawer, DrawerContent, DrawerOverlay } from '@chakra-ui/react'; +import ChatHistorySlider from '@/pageComponents/chat/ChatHistorySlider'; +import { ChatContext } from '@/web/core/chat/context/chatContext'; +import NextHead from '@/components/common/NextHead'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; + +const MyApps = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { isPc } = useSystem(); + const { paths, myApps, isFetchingApps, setSearchKey } = useContextSelector( + AppListContext, + (v) => v + ); + + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + + const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); + const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider); + const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); + + const map = useMemo( + () => + ({ + all: t('common:core.module.template.all_team_app'), + [AppTypeEnum.simple]: t('app:type.Simple bot'), + [AppTypeEnum.workflow]: t('app:type.Workflow bot'), + [AppTypeEnum.plugin]: t('app:type.Plugin'), + [AppTypeEnum.httpPlugin]: t('app:type.Http plugin'), + [AppTypeEnum.folder]: t('common:Folder'), + [AppTypeEnum.toolSet]: t('app:type.MCP tools'), + [AppTypeEnum.tool]: t('app:type.MCP tools'), + [AppTypeEnum.hidden]: t('app:type.hidden') + }) satisfies Record, + [t] + ); + + const [appType, setAppType] = useState('all'); + const tabs = ['all' as const, AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin]; + + return ( + + + + {!isPc && ( + + + + + + + + + + + )} + + {paths.length > 0 && ( + + { + router.push({ + query: { + ...router.query, + parentId + } + }); + }} + /> + + )} + + + 0 ? 3 : [4, 6]} alignItems={'center'} gap={3}> + {isPc && ( + setAppType(tabs[index])}> + + {tabs.map((item, index) => ( + + {map[item]} + + ))} + + + + )} + + + {isPc && ( + setSearchKey(e.target.value)} + placeholder={t('app:search_app')} + maxLength={30} + /> + )} + + + {!isPc && ( + + { + setSearchKey(e.target.value)} + placeholder={t('app:search_app')} + maxLength={30} + /> + } + + )} + + + + + + + + ); +}; + +function ContextRender() { + return ( + + + + ); +} + +export default ContextRender; diff --git a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx new file mode 100644 index 000000000..5f95388a8 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx @@ -0,0 +1,176 @@ +import ChatHeader from '@/pageComponents/chat/ChatHeader'; +import ChatBox from '@/components/core/chat/ChatContainer/ChatBox'; +import { Flex, Box, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react'; +import ChatHistorySlider from '@/pageComponents/chat/ChatHistorySlider'; +import { useTranslation } from 'react-i18next'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import SideBar from '@/components/SideBar'; +import { ChatContext } from '@/web/core/chat/context/chatContext'; +import { useContextSelector } from 'use-context-selector'; +import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; +import { type AppListItemType } from '@fastgpt/global/core/app/type'; +import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; +import { useCallback } 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'; +import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; +import { useChatStore } from '@/web/core/chat/context/useChatStore'; +import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getInitChatInfo } from '@/web/core/chat/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useRouter } from 'next/router'; +import NextHead from '@/components/common/NextHead'; + +type Props = { + myApps: AppListItemType[]; +}; + +const AppChatWindow = ({ myApps }: Props) => { + const router = useRouter(); + const { userInfo } = useUserStore(); + const { chatId, appId, outLinkAuthData } = useChatStore(); + + const { t } = useTranslation(); + const { isPc } = useSystem(); + + const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider); + const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat); + const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); + const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); + + const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); + const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); + const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); + const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); + + const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); + const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); + + const { loading } = useRequest2( + async () => { + if (!appId || forbidLoadChat.current) return; + + const res = await getInitChatInfo({ appId, chatId }); + res.userAvatar = userInfo?.avatar; + + setChatBoxData(res); + + resetVariables({ + variables: res.variables, + variableList: res.app?.chatConfig?.variables + }); + }, + { + manual: false, + refreshDeps: [appId, chatId], + errorToast: '', + onError(e: any) { + if (e?.code && e.code >= 502000) { + router.replace({ + query: { + ...router.query, + appId: myApps[0]?._id + } + }); + } + }, + onFinally() { + forbidLoadChat.current = false; + } + } + ); + + const onStartChat = useCallback( + async ({ + messages, + variables, + controller, + responseChatItemId, + generatingMessage + }: StartChatFnProps) => { + const histories = messages.slice(-1); + const { responseText } = await streamFetch({ + data: { + messages: histories, + variables, + responseChatItemId, + appId, + chatId + }, + abortCtrl: controller, + onMessage: generatingMessage + }); + + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]); + + onUpdateHistoryTitle({ chatId, newTitle }); + setChatBoxData((state) => ({ + ...state, + title: newTitle + })); + + return { responseText, isNewChat: forbidLoadChat.current }; + }, + [appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat] + ); + + return ( + + {/* set window title and icon */} + + + {/* show history slider */} + {isPc || !appId ? ( + + + + ) : ( + + + + + + + )} + + {/* chat container */} + + + + + + + + + ); +}; + +export default AppChatWindow; diff --git a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx new file mode 100644 index 000000000..c540b4a78 --- /dev/null +++ b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx @@ -0,0 +1,449 @@ +import ChatBox from '@/components/core/chat/ChatContainer/ChatBox'; +import { + Flex, + Box, + Drawer, + DrawerOverlay, + DrawerContent, + Button, + Menu, + MenuButton, + MenuList, + MenuItem, + Checkbox +} from '@chakra-ui/react'; +import ChatHistorySlider from '@/pageComponents/chat/ChatHistorySlider'; +import { useTranslation } from 'react-i18next'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import SideBar from '@/components/SideBar'; +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 type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type'; +import { streamFetch } from '@/web/common/api/fetch'; +import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils'; +import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; +import { useLocalStorageState, useMemoizedFn } from 'ahooks'; +import { useChatStore } from '@/web/core/chat/context/useChatStore'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getInitChatInfo } from '@/web/core/chat/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import NextHead from '@/components/common/NextHead'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import AIModelSelector from '@/components/Select/AIModelSelector'; +import { form2AppWorkflow } from '@/web/core/app/utils'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { getDefaultAppForm } from '@fastgpt/global/core/app/utils'; +import { getPreviewPluginNode } from '@/web/core/app/api/plugin'; +import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; +import { getWebLLMModel } from '@/web/common/system/utils'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import type { + AppFileSelectConfigType, + AppListItemType, + AppWhisperConfigType +} from '@fastgpt/global/core/app/type'; +import ChatHeader from '@/pageComponents/chat/ChatHeader'; +import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; +import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants'; +import { getModelFromList } from '@fastgpt/global/core/ai/model'; +import MyPopover from '@fastgpt/web/components/common/MyPopover'; + +type Props = { + myApps: AppListItemType[]; +}; + +const defaultFileSelectConfig: AppFileSelectConfigType = { + maxFiles: 20, + canSelectImg: false, + canSelectFile: true +}; + +const defaultWhisperConfig: AppWhisperConfigType = { + open: true, + autoSend: false, + autoTTSResponse: false +}; + +const HomeChatWindow = ({ myApps }: Props) => { + const { t } = useTranslation(); + const { isPc } = useSystem(); + + const { userInfo } = useUserStore(); + const { llmModelList, defaultModels } = useSystemStore(); + const { chatId, appId, outLinkAuthData } = useChatStore(); + + const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider); + const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat); + const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); + const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); + + const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); + const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); + const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); + const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); + + const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + + const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); + const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); + + const availableModels = useMemo( + () => llmModelList.map((model) => ({ value: model.model, label: model.name })), + [llmModelList] + ); + const [selectedModel, setSelectedModel] = useLocalStorageState('chat_home_model', { + defaultValue: defaultModels.llm?.model + }); + const selectedModelAvatar = useMemo(() => { + const modelData = getModelFromList(llmModelList, selectedModel || ''); + return modelData?.avatar || HUGGING_FACE_ICON; + }, [selectedModel, llmModelList]); + + const availableTools = useMemo( + () => chatSettings?.selectedTools || [], + [chatSettings?.selectedTools] + ); + const [selectedToolIds = [], setSelectedToolIds] = useLocalStorageState( + 'chat_home_tools', + { + defaultValue: [] + } + ); + const selectedTools = useMemo(() => { + return availableTools.filter((tool) => selectedToolIds.includes(tool.pluginId)); + }, [availableTools, selectedToolIds]); + // If selected ToolIds not in availableTools, Remove it + useEffect(() => { + if (availableTools.length === 0) return; + setSelectedToolIds( + selectedToolIds.filter((id) => availableTools.some((tool) => tool.pluginId === id)) + ); + }, [availableTools]); + + // 初始化聊天数据 + const { loading } = useRequest2( + async () => { + if (!appId || forbidLoadChat.current) return; + + const modelData = getWebLLMModel(selectedModel); + const res = await getInitChatInfo({ appId, chatId }); + res.userAvatar = userInfo?.avatar; + if (!res.app.chatConfig) { + 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 + }; + } + + setChatBoxData(res); + + resetVariables({ + variables: res.variables, + variableList: res.app?.chatConfig?.variables + }); + }, + { + manual: false, + refreshDeps: [appId, chatId], + errorToast: '', + onFinally() { + forbidLoadChat.current = false; + } + } + ); + + // 使用类似AppChatWindow的对话逻辑 + const onStartChat = useMemoizedFn( + async ({ + messages, + variables, + controller, + responseChatItemId, + generatingMessage + }: StartChatFnProps) => { + 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 }); + node.inputs = node.inputs.map((input) => { + const tool = availableTools.find((tool) => tool.pluginId === toolId); + const value = tool?.inputs?.[input.key]; + return { ...input, value }; + }); + return node; + }) + ); + + const formData = getDefaultAppForm(); + formData.aiSettings.model = selectedModel; + formData.selectedTools = tools; + formData.chatConfig = chatBoxData.app.chatConfig || {}; + + const { responseText } = await streamFetch({ + url: '/api/proApi/core/chat/chatHome', + data: { + messages: histories, + variables, + responseChatItemId, + appId, + appName: t('chat:home.chat_app', { name: 'FastGPT' }), + chatId, + ...form2AppWorkflow(formData, t) + }, + onMessage: generatingMessage, + abortCtrl: controller + }); + + const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]); + + onUpdateHistoryTitle({ chatId, newTitle }); + setChatBoxData((state) => ({ + ...state, + title: newTitle + })); + + return { responseText, isNewChat: forbidLoadChat.current }; + } + ); + + // 自定义按钮组(模型选择和工具选择) + const InputLeftComponent = useMemo( + () => ( + <> + {/* 模型选择 */} + {availableModels.length > 0 && ( + + {isPc && } + {selectedModel} + + } + onChange={async (model) => { + setChatBoxData((state) => ({ + ...state, + app: { + ...state.app, + chatConfig: { + ...state.app.chatConfig, + fileSelectConfig: { + ...defaultFileSelectConfig, + canSelectImg: !!getWebLLMModel(model).vision + } + } + } + })); + setSelectedModel(model); + }} + /> + )} + + {/* 工具选择下拉框 */} + {availableTools.length > 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, + availableTools, + selectedTools.length, + t, + setSelectedModel, + selectedToolIds, + setSelectedToolIds, + setChatBoxData, + isPc, + selectedModelAvatar + ] + ); + + return ( + + {/* set window title and icon */} + + + {/* show history slider */} + {isPc || !appId ? ( + + + + ) : ( + + + + + + + )} + + {/* chat container */} + + {isPc ? ( + chatRecords.length > 0 && ( + + + {chatBoxData?.title} + + } + > + {() => `${t('chat:home.chat_id')}:${chatBoxData?.chatId}`} + + + ) + ) : ( + + )} + + + + + + + ); +}; + +export default HomeChatWindow; diff --git a/projects/app/src/pageComponents/chat/SliderApps.tsx b/projects/app/src/pageComponents/chat/SliderApps.tsx index 371984bd3..1cf0d3e18 100644 --- a/projects/app/src/pageComponents/chat/SliderApps.tsx +++ b/projects/app/src/pageComponents/chat/SliderApps.tsx @@ -1,5 +1,7 @@ import React, { useCallback, useState } from 'react'; +import type { BoxProps } from '@chakra-ui/react'; import { Flex, Box, HStack, Image, Skeleton } 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'; @@ -17,13 +19,451 @@ import type { } 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, + DEFAULT_LOGO_BANNER_URL +} from '@/pageComponents/chat/constants'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useContextSelector } from 'use-context-selector'; +import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; -const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppId: string }) => { +type Props = { + activeAppId: string; + apps: AppListItemType[]; +}; + +const MotionBox = motion(Box); +const MotionFlex = motion(Flex); + +const ANIMATION_DURATION = 0.15; +const ANIMATION_EASE = 'easeInOut'; +const TEXT_DELAY = 0.1; + +const contentVariants = { + show: { + opacity: 1, + transition: { duration: 0.05, delay: 0.02 } + }, + hide: { + opacity: 0, + transition: { duration: 0.05 } + } +}; + +const textVariants = { + show: { + opacity: 1, + x: 0, + transition: { + duration: 0.1, + delay: ANIMATION_DURATION + TEXT_DELAY, + ease: 'easeOut' + } + }, + hide: { + opacity: 0, + x: -10, + transition: { + duration: 0.001, + ease: 'easeIn' + } + } +}; + +// 图标快速动画(无延迟) +const iconVariants = { + show: { + opacity: 1, + scale: 1, + transition: { + duration: 0.1, + delay: 0.05, + ease: 'easeOut' + } + }, + hide: { + opacity: 0, + scale: 0.8, + transition: { + duration: 0.1, + ease: 'easeIn' + } + } +}; + +// 通用动画容器 +const AnimatedSection: React.FC< + { + show: boolean; + children: React.ReactNode; + variant?: 'content' | 'text' | 'icon'; + } & BoxProps +> = ({ show, children, variant = 'content', ...props }) => { + const getVariants = () => { + switch (variant) { + case 'text': + return textVariants; + case 'icon': + return iconVariants; + default: + return contentVariants; + } + }; + + return ( + + {show && ( + + {children} + + )} + + ); +}; + +// 文字动画组件 +type AnimatedTextProps = { + show: boolean; + children: React.ReactNode; + className?: string; + [key: string]: any; +}; + +const AnimatedText: React.FC = ({ show, children, className, ...props }) => ( + + {show && ( + + {children} + + )} + +); + +const LogoSection = () => { + const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const logos = useContextSelector(ChatSettingContext, (v) => v.logos); + const isHomeActive = useContextSelector( + ChatSettingContext, + (v) => v.pane === ChatSidebarPaneEnum.HOME + ); + const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const wideLogoSrc = logos.wideLogoUrl; + const squareLogoSrc = logos.squareLogoUrl; + + return ( + + + FastGPT slogan + + + + + FastGPT logo + + + + + + + + + + ); +}; + +const ActionButton: React.FC<{ + text?: string; + isActive?: boolean; + isCollapsed: boolean; + icon: Parameters[0]['name']; + onClick: () => void; +}> = ({ icon, text, isActive = false, isCollapsed, onClick }) => { + return ( + + + + {text} + + + ); +}; + +const NavigationSection = () => { const { t } = useTranslation(); - const router = useRouter(); - const isTeamChat = router.pathname === '/chat/team'; + const { feConfigs } = useSystemStore(); + const isProVersion = !!feConfigs.isPlus; + + const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const isHomeActive = useContextSelector( + ChatSettingContext, + (v) => v.pane === ChatSidebarPaneEnum.HOME + ); + const isTeamAppsActive = useContextSelector( + ChatSettingContext, + (v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS + ); + const onHomeClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + + return ( + + + + + + {isProVersion && ( + + {isCollapsed ? ( + + + onHomeClick(ChatSidebarPaneEnum.HOME)} + /> + + onHomeClick(ChatSidebarPaneEnum.TEAM_APPS)} + /> + + + ) : ( + + + onHomeClick(ChatSidebarPaneEnum.HOME)} + /> + + onHomeClick(ChatSidebarPaneEnum.TEAM_APPS)} + /> + + + )} + + )} + + ); +}; + +const BottomSection = () => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const isProVersion = !!feConfigs.isPlus; + const { userInfo } = useUserStore(); - const [imageLoaded, setImageLoaded] = useState(false); + const isLoggedIn = !!userInfo; + const avatar = userInfo?.avatar; + const username = userInfo?.username; + const isAdmin = !!userInfo?.team.permission.hasManagePer; + + const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const isSettingActive = useContextSelector( + ChatSettingContext, + (v) => v.pane === ChatSidebarPaneEnum.SETTING + ); + const onSettingClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + + return ( + + + {isAdmin && isProVersion && ( + + onSettingClick(ChatSidebarPaneEnum.SETTING)} + > + + + + )} + + + {isLoggedIn ? ( + + + + + {username} + + + + ) : ( + + + + {t('login:Login')} + + + )} + + + + ); +}; + +const SliderApps = ({ apps, activeAppId }: Props) => { + const router = useRouter(); + const { t } = useTranslation(); + + const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + + const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const getAppList = useCallback(async ({ parentId }: GetResourceFolderListProps) => { return getMyApps({ @@ -39,172 +479,83 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI ); }, []); - const onChangeApp = useCallback( - (appId: string) => { - router.replace({ - query: { - ...router.query, - appId - } - }); + const isRecentlyUsedAppSelected = (id: string) => + pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && id === activeAppId; + + const handleSelectRecentlyUsedApp = useCallback( + (id: string) => { + if (pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && id === activeAppId) return; + handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS); + router.replace({ query: { ...router.query, appId: id } }); }, - [router] + [pane, router, activeAppId, handlePaneChange] ); return ( - - - - + + + + + {/* recently used apps */} + + + + + - FastGPT slogan setImageLoaded(true)} - onError={() => setImageLoaded(true)} - /> - - - + {t('common:core.chat.Recent use')} + + - - - {!isTeamChat && ( - <> - - {t('common:core.chat.Recent use')} - - {t('common:More')} - - - } + + {apps.map((item) => ( + handleSelectRecentlyUsedApp(item._id) + })} > - {({ onClose }) => ( - - { - if (!item) return; - onChangeApp(item.id); - onClose(); - }} - server={getAppList} - /> - - )} - - - - )} - - - {apps.map((item) => ( - onChangeApp(item._id) - })} - > - - - {item.name} - - - ))} - - - - {userInfo ? ( - - - - - {userInfo.username} + + + {item.name} - - ) : ( - - - - {t('login:Login')} - - - )} - - + ))} + + + + + ); }; diff --git a/projects/app/src/pageComponents/chat/UserAvatarPopover.tsx b/projects/app/src/pageComponents/chat/UserAvatarPopover.tsx index c22f13f4f..4fe13d642 100644 --- a/projects/app/src/pageComponents/chat/UserAvatarPopover.tsx +++ b/projects/app/src/pageComponents/chat/UserAvatarPopover.tsx @@ -6,15 +6,22 @@ import { clearToken } from '@/web/support/user/auth'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import MyPopover from '@fastgpt/web/components/common/MyPopover'; import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@fastgpt/web/components/common/Avatar'; type UserAvatarPopoverProps = { + isCollapsed: boolean; children: React.ReactNode; placement?: Parameters[0]['placement']; }; -const UserAvatarPopover = ({ children, placement = 'top-end' }: UserAvatarPopoverProps) => { +const UserAvatarPopover = ({ + isCollapsed, + children, + placement = 'top-end', + ...props +}: UserAvatarPopoverProps) => { const { t } = useTranslation(); - const { setUserInfo } = useUserStore(); + const { setUserInfo, userInfo } = useUserStore(); const { openConfirm, ConfirmModal } = useConfirm({ content: t('common:confirm_logout') }); @@ -30,6 +37,7 @@ const UserAvatarPopover = ({ children, placement = 'top-end' }: UserAvatarPopove trigger="hover" placement={placement} w="160px" + {...props} > {({ onClose }) => { const onLogout = useCallback(() => { @@ -39,6 +47,22 @@ const UserAvatarPopover = ({ children, placement = 'top-end' }: UserAvatarPopove return ( + {!!isCollapsed && ( + + + {userInfo?.username ?? '-'} + + )} + { + const { setSource, setAppId } = useChatStore(); + const { userInfo, initUserInfo } = useUserStore(); + + const [isInitedUser, setIsInitedUser] = useState(false); + + // get app list + const { data: myApps = [] } = useRequest2(() => getRecentlyUsedApps({ getRecentlyChat: true }), { + manual: false, + errorToast: '', + refreshDeps: [userInfo], + pollingInterval: 30000 + }); + + // initialize user info + useMount(async () => { + try { + await initUserInfo(); + } catch (error) { + console.log('User not logged in:', error); + } finally { + setSource('online'); + setIsInitedUser(true); + } + }); + + // watch appId + useEffect(() => { + if (!userInfo || !appId) return; + setAppId(appId); + }, [appId, setAppId, userInfo]); + + return { + isInitedUser, + userInfo, + myApps + }; +}; diff --git a/projects/app/src/pageComponents/dashboard/Container.tsx b/projects/app/src/pageComponents/dashboard/Container.tsx index 1242a4530..2781959ae 100644 --- a/projects/app/src/pageComponents/dashboard/Container.tsx +++ b/projects/app/src/pageComponents/dashboard/Container.tsx @@ -1,6 +1,6 @@ import { Box, Flex, useDisclosure } from '@chakra-ui/react'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useMemo } from 'react'; import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; diff --git a/projects/app/src/pageComponents/dashboard/apps/List.tsx b/projects/app/src/pageComponents/dashboard/apps/List.tsx index 2d21c68b6..530369ac7 100644 --- a/projects/app/src/pageComponents/dashboard/apps/List.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/List.tsx @@ -153,7 +153,7 @@ const ListItem = () => { label={ app.type === AppTypeEnum.folder ? t('common:open_folder') - : app.permission.hasWritePer + : app.permission.hasWritePer || app.permission.hasReadChatLogPer ? t('app:edit_app') : t('app:go_to_chat') } @@ -190,7 +190,7 @@ const ListItem = () => { parentId: app._id } }); - } else if (app.permission.hasWritePer) { + } else if (app.permission.hasWritePer || app.permission.hasReadChatLogPer) { router.push(`/app/detail?appId=${app._id}`); } else { window.open(`/chat?appId=${app._id}`, '_blank'); @@ -249,7 +249,7 @@ const ListItem = () => { )} {(AppFolderTypeList.includes(app.type) ? app.permission.hasManagePer - : app.permission.hasWritePer) && ( + : app.permission.hasWritePer || app.permission.hasReadChatLogPer) && ( { } ] : []), - ...(app.type === AppTypeEnum.toolSet || + ...(!app.permission?.hasWritePer || + app.type === AppTypeEnum.toolSet || app.type === AppTypeEnum.folder || app.type === AppTypeEnum.httpPlugin ? [] diff --git a/projects/app/src/pageComponents/dashboard/apps/MCPToolsEditModal.tsx b/projects/app/src/pageComponents/dashboard/apps/MCPToolsEditModal.tsx index a0eb77b95..306b2efed 100644 --- a/projects/app/src/pageComponents/dashboard/apps/MCPToolsEditModal.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/MCPToolsEditModal.tsx @@ -21,7 +21,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { AppListContext } from './context'; import { useContextSelector } from 'use-context-selector'; import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; diff --git a/projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx b/projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx index be9192138..9b6dec5a4 100644 --- a/projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx @@ -39,7 +39,8 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => { color: '' }, [AppTypeEnum.tool]: undefined, - [AppTypeEnum.folder]: undefined + [AppTypeEnum.folder]: undefined, + [AppTypeEnum.hidden]: undefined }); const data = map.current[type]; diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx index 21898826a..1dde7fe0e 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TrainingStates.tsx @@ -392,7 +392,9 @@ const ErrorView = ({ {item.chunkIndex + 1} {TrainingText[item.mode]} - {t(item.errorMsg)} + + {t(item.errorMsg)} + diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx index 5aeeec4eb..c277ae8f9 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx @@ -44,10 +44,7 @@ import { CollectionPageContext } from './Context'; import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; import MyTag from '@fastgpt/web/components/common/Tag/index'; -import { - checkCollectionIsFolder, - collectionCanSync -} from '@fastgpt/global/core/dataset/collection/utils'; +import { collectionCanSync } from '@fastgpt/global/core/dataset/collection/utils'; import { useFolderDrag } from '@/components/common/folder/useFolderDrag'; import TagsPopOver from './TagsPopOver'; import { useSystemStore } from '@/web/common/system/useSystemStore'; diff --git a/projects/app/src/pageComponents/dataset/list/CreateModal.tsx b/projects/app/src/pageComponents/dataset/list/CreateModal.tsx index d70443ef1..6f1137a04 100644 --- a/projects/app/src/pageComponents/dataset/list/CreateModal.tsx +++ b/projects/app/src/pageComponents/dataset/list/CreateModal.tsx @@ -76,7 +76,7 @@ const CreateModal = ({ }); /* create a new kb and router to it */ - const { run: onclickCreate, loading: creating } = useRequest2( + const { runAsync: onclickCreate, loading: creating } = useRequest2( async (data: CreateDatasetParams) => await postCreateDataset(data), { successToast: t('common:create_success'), diff --git a/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx b/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx index 22e82c3fe..990f432ec 100644 --- a/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx +++ b/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx @@ -1,6 +1,6 @@ import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { AbsoluteCenter, Box, Button, Flex } from '@chakra-ui/react'; +import { AbsoluteCenter, Box, Flex, Grid, IconButton, GridItem, Button } from '@chakra-ui/react'; import { LOGO_ICON } from '@fastgpt/global/common/system/constants'; import { OAuthEnum } from '@fastgpt/global/support/user/constant'; import { useRouter } from 'next/router'; @@ -14,7 +14,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; import Avatar from '@fastgpt/web/components/common/Avatar'; import dynamic from 'next/dynamic'; import { POST } from '@/web/common/api/request'; -import { getBdVId } from '@/web/support/marketing/utils'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; type Props = { children: React.ReactNode; @@ -69,6 +69,16 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => { } ] : []), + ...(pageType !== LoginPageTypeEnum.passwordLogin + ? [ + { + label: t('common:support.user.login.Password login'), + provider: LoginPageTypeEnum.passwordLogin, + icon: 'support/permission/privateLight', + pageType: LoginPageTypeEnum.passwordLogin + } + ] + : []), ...(feConfigs?.oauth?.google ? [ { @@ -100,16 +110,6 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => { redirectUrl: `https://login.microsoftonline.com/${feConfigs?.oauth?.microsoft?.tenantId || 'common'}/oauth2/v2.0/authorize?client_id=${feConfigs?.oauth?.microsoft?.clientId}&response_type=code&redirect_uri=${redirectUri}&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2Fuser.read&state=${state.current}` } ] - : []), - ...(pageType !== LoginPageTypeEnum.passwordLogin - ? [ - { - label: t('common:support.user.login.Password login'), - provider: LoginPageTypeEnum.passwordLogin, - icon: 'support/permission/privateLight', - pageType: LoginPageTypeEnum.passwordLogin - } - ] : []) ], [feConfigs, pageType, redirectUri, t] @@ -159,8 +159,8 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => { return ( - - + + { {children} {show_oauth && ( - + - - - + + + + or - - - - {oAuthList.map((item) => ( - - - - ))} - + + + + + {oAuthList.length > 2 ? ( + + {oAuthList.map((item) => ( + + } + onClick={() => onClickOauth(item)} + /> + + ))} + + ) : ( + + {oAuthList.map((item) => ( + + + + ))} + + )} )} diff --git a/projects/app/src/pageComponents/login/LoginForm/LoginForm.tsx b/projects/app/src/pageComponents/login/LoginForm/LoginForm.tsx index 2298dd099..6021694f1 100644 --- a/projects/app/src/pageComponents/login/LoginForm/LoginForm.tsx +++ b/projects/app/src/pageComponents/login/LoginForm/LoginForm.tsx @@ -1,15 +1,15 @@ import React, { type Dispatch } from 'react'; -import { FormControl, Flex, Input, Button, Box, Link } from '@chakra-ui/react'; +import { FormControl, Flex, Input, Button, Box } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { postLogin, getPreLogin } from '@/web/support/user/api'; import type { ResLogin } from '@/global/support/api/userRes'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { getDocPath } from '@/web/common/system/doc'; import { useTranslation } from 'next-i18next'; import FormLayout from './FormLayout'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import PolicyTip from './PolicyTip'; interface Props { setPageType: Dispatch<`${LoginPageTypeEnum}`>; @@ -74,7 +74,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { return ( { if (e.key === 'Enter' && !e.shiftKey && !requesting) { handleSubmit(onclickLogin)(); @@ -110,37 +110,11 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { })} > - {feConfigs?.docUrl && ( - - {t('login:policy_tip')} - - {t('login:terms')} - - & - - {t('login:privacy')} - - - )} +