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 000000000..0386ed2ce Binary files /dev/null and b/projects/app/public/imgs/chat/fastgpt_chat_diagram.png differ 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 000000000..3100121b6 Binary files /dev/null and b/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png differ 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 000000000..203186785 Binary files /dev/null and b/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png differ diff --git a/projects/app/public/imgs/fastgpt_slogan.png b/projects/app/public/imgs/fastgpt_slogan.png deleted file mode 100644 index 988e8e7f5..000000000 Binary files a/projects/app/public/imgs/fastgpt_slogan.png and /dev/null differ diff --git a/projects/app/public/imgs/modal/info.svg b/projects/app/public/imgs/modal/info.svg new file mode 100644 index 000000000..ef26d1fdf --- /dev/null +++ b/projects/app/public/imgs/modal/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/app/public/imgs/proModalBg.png b/projects/app/public/imgs/proModalBg.png new file mode 100644 index 000000000..a54fe3d99 Binary files /dev/null and b/projects/app/public/imgs/proModalBg.png differ 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')} - - - )} +