Plugin runtime (#2050)
Some checks are pending
Deploy image by kubeconfig / build-fastgpt-docs-images (push) Waiting to run
Deploy image by kubeconfig / update-docs-image (push) Blocked by required conditions
Deploy image to vercel / deploy-production (push) Waiting to run
Build FastGPT images in Personal warehouse / build-fastgpt-images (push) Waiting to run

* feat: plugin run (#1950)

* feat: plugin run

* fix

* ui

* fix

* change user input type

* fix

* fix

* temp

* split out plugin chat

* perf: chatbox

* perf: chatbox

* fix: plugin runtime (#2032)

* fix: plugin runtime

* fix

* fix build

* fix build

* perf: chat send prompt

* perf: chat log ux

* perf: chatbox context and share page plugin runtime

* perf: plugin run time config

* fix: ts

* feat: doc

* perf: isPc check

* perf: variable input render

* feat: app search

* fix: response box height

* fix: phone ui

* perf: lock

* perf: plugin route

* fix: chat (#2049)

---------

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer 2024-07-15 22:50:48 +08:00 committed by GitHub
parent 090c880860
commit b5c98a4f63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
126 changed files with 5012 additions and 4317 deletions

1
.npmrc
View File

@ -1,2 +1,3 @@
public-hoist-pattern[]=*tiktoken* public-hoist-pattern[]=*tiktoken*
public-hoist-pattern[]=*@zilliz/milvus2-sdk-node* public-hoist-pattern[]=*@zilliz/milvus2-sdk-node*
registry=https://registry.npmjs.org/

View File

@ -7,18 +7,22 @@ toc: true
weight: 852 weight: 852
--- ---
## 发起对话
{{% alert icon="🤖 " context="success" %}} {{% alert icon="🤖 " context="success" %}}
该接口的 API Key 需使用`应用特定的 key`,否则会报错。 该接口的 API Key 需使用`应用特定的 key`,否则会报错。
有些包调用时,`BaseUrl`需要添加`v1`路径有些不需要如果出现404情况可补充`v1`重试。 有些包调用时,`BaseUrl`需要添加`v1`路径有些不需要如果出现404情况可补充`v1`重试。
{{% /alert %}} {{% /alert %}}
## 发起对话(简易应用和工作流)
**对话接口兼容`GPT`的接口!如果你的项目使用的是标准的`GPT`官方接口,可以直接通过修改`BaseUrl`和 `Authorization`来访问 FastGpt 应用。** 对话接口兼容`GPT`的接口!如果你的项目使用的是标准的`GPT`官方接口,可以直接通过修改`BaseUrl`和 `Authorization`来访问 FastGpt 应用,不过需要注意下面几个规则:
## 请求 {{% alert icon="🤖 " context="success" %}}
* 传入的`model``temperature`等参数字段均无效,这些字段由编排决定。
* 不会返回实际消耗`Token`值,如果需要,可以设置`detail=true`,并手动计算 `responseData` 里的`tokens`值。
{{% /alert %}}
### 请求
{{< tabs tabTotal="2" >}} {{< tabs tabTotal="2" >}}
{{< tab tabName="请求示例" >}} {{< tab tabName="请求示例" >}}
@ -67,7 +71,7 @@ curl --location --request POST 'https://api.fastgpt.in/api/v1/chat/completions'
{{< /tab >}} {{< /tab >}}
{{< /tabs >}} {{< /tabs >}}
## 响应 ### 响应
{{< tabs tabTotal="5" >}} {{< tabs tabTotal="5" >}}
{{< tab tabName="detail=false,stream=false 响应" >}} {{< tab tabName="detail=false,stream=false 响应" >}}
@ -245,7 +249,7 @@ data: [{"moduleName":"知识库搜索","moduleType":"datasetSearchNode","running
{{< /markdownify >}} {{< /markdownify >}}
{{< /tab >}} {{< /tab >}}
{{< tab tabName="detail=true,stream=true 时,event值" >}} {{< tab tabName="event值" >}}
{{< markdownify >}} {{< markdownify >}}
event取值 event取值
@ -265,6 +269,192 @@ event取值
{{< /tabs >}} {{< /tabs >}}
## 请求插件
插件的接口与对话接口一致,仅请求参数略有区别,有以下规定:
* 调用插件类型的应用时,接口默认为`detail`模式。
* 无需传入 `chatId`,因为插件只能运行一轮。
* 无需传入`messages`。
* 通过传递`variables`来代表插件的输入。
* 通过获取`pluginData`来获取插件输出。
### 请求示例
```bash
curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
--header 'Authorization: Bearer test-xxxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"stream": false,
"chatId": "test",
"variables": {
"query":"你好" # 我的插件输入有一个参数,变量名叫 query
}
}'
```
### 响应示例
{{< tabs tabTotal="3" >}}
{{< tab tabName="detail=true,stream=false 响应" >}}
{{< markdownify >}}
* 插件的输出可以通过查找`responseData`中, `moduleType=pluginOutput`的元素,其`pluginOutput`是插件的输出。
* 流输出,仍可以通过`choices`进行获取。
```json
{
"responseData": [
{
"nodeId": "fdDgXQ6SYn8v",
"moduleName": "AI 对话",
"moduleType": "chatNode",
"totalPoints": 0.685,
"model": "FastAI-3.5",
"tokens": 685,
"query": "你好",
"maxToken": 2000,
"historyPreview": [
{
"obj": "Human",
"value": "你好"
},
{
"obj": "AI",
"value": "你好!有什么可以帮助你的吗?欢迎向我提问。"
}
],
"contextTotalLen": 14,
"runningTime": 1.73
},
{
"nodeId": "pluginOutput",
"moduleName": "自定义插件输出",
"moduleType": "pluginOutput",
"totalPoints": 0,
"pluginOutput": {
"result": "你好!有什么可以帮助你的吗?欢迎向我提问。"
},
"runningTime": 0
}
],
"newVariables": {
"query": "你好"
},
"id": "safsafsa",
"model": "",
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 1
},
"choices": [
{
"message": {
"role": "assistant",
"content": "你好!有什么可以帮助你的吗?欢迎向我提问。"
},
"finish_reason": "stop",
"index": 0
}
]
}
```
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="detail=true,stream=true 响应" >}}
{{< markdownify >}}
* 插件的输出可以通过获取`event=flowResponses`中的字符串,并将其反序列化后得到一个数组。同样的,查找 `moduleType=pluginOutput`的元素,其`pluginOutput`是插件的输出。
* 流输出,仍和对话接口一样获取。
```bash
event: flowNodeStatus
data: {"status":"running","name":"AI 对话"}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"你"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"好"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"有"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"什"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"么"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"可以"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"帮"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"助"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"你"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"的"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"吗"},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}
event: answer
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}
event: answer
data: [DONE]
event: flowResponses
data: [{"nodeId":"fdDgXQ6SYn8v","moduleName":"AI 对话","moduleType":"chatNode","totalPoints":0.033,"model":"FastAI-3.5","tokens":33,"query":"你好","maxToken":2000,"historyPreview":[{"obj":"Human","value":"你好"},{"obj":"AI","value":"你好!有什么可以帮助你的吗?"}],"contextTotalLen":2,"runningTime":1.42},{"nodeId":"pluginOutput","moduleName":"自定义插件输出","moduleType":"pluginOutput","totalPoints":0,"pluginOutput":{"result":"你好!有什么可以帮助你的吗?"},"runningTime":0}]
```
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="输出获取" >}}
{{< markdownify >}}
event取值
- answer: 返回给客户端的文本(最终会算作回答)
- fastAnswer: 指定回复返回给客户端的文本(最终会算作回答)
- toolCall: 执行工具
- toolParams: 工具参数
- toolResponse: 工具返回
- flowNodeStatus: 运行到的节点状态
- flowResponses: 节点完整响应
- updateVariables: 更新变量
- error: 报错
{{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}
## 使用案例 ## 使用案例
- [接入 NextWeb/ChatGPT web 等应用](/docs/use-cases/openapi) - [接入 NextWeb/ChatGPT web 等应用](/docs/use-cases/openapi)

View File

@ -0,0 +1,27 @@
---
title: 'V4.8.7(进行中)'
description: 'FastGPT V4.8.7 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 817
---
## 升级指南
### 1. 做好数据库备份
### 2. 修改镜像
- fastgpt 镜像 tag 修改成 v4.8.7-alpha
- 商业版镜像 tag 修改成 v4.8.7-alpha
-------
## V4.8.7 更新说明
1. 新增 - 插件支持独立运行,发布和日志查看
2. 新增 - 应用搜索
3. 优化 - 对话框代码
4. 优化 - 升级 Dockerfile node 和 pnpm 版本
5. 修复 - 简易模式无法变更全局变量

View File

@ -78,7 +78,7 @@ weight: 404
}, },
{ {
"nodeId": "u6IAOEssxoZT", "nodeId": "u6IAOEssxoZT",
"name": "工具调用(实验)", "name": "工具调用",
"intro": "通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。", "intro": "通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。",
"avatar": "/imgs/workflow/tool.svg", "avatar": "/imgs/workflow/tool.svg",
"flowNodeType": "tools", "flowNodeType": "tools",

View File

@ -26,7 +26,7 @@
"./docSite/**/**/*.md": "npm run format-doc" "./docSite/**/**/*.md": "npm run format-doc"
}, },
"engines": { "engines": {
"node": ">=18.0.0", "node": ">=18.16.0",
"pnpm": ">=8.6.0" "pnpm": ">=9.0.0"
} }
} }

View File

@ -0,0 +1,10 @@
import { StoreNodeItemType } from '../../workflow/type/node';
import { FlowNodeInputItemType } from '../../workflow/type/io';
import { FlowNodeTypeEnum } from '../../workflow/node/constant';
export const getPluginInputsFromStoreNodes = (nodes: StoreNodeItemType[]) => {
return nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || [];
};
export const getPluginRunContent = (e: { pluginInputs: FlowNodeInputItemType[] }) => {
return JSON.stringify(e);
};

View File

@ -21,6 +21,7 @@ export type AppSchema = {
name: string; name: string;
avatar: string; avatar: string;
intro: string; intro: string;
updateTime: Date; updateTime: Date;
modules: StoreNodeItemType[]; modules: StoreNodeItemType[];

View File

@ -56,7 +56,10 @@ export const chats2GPTMessages = ({
text: item.text?.content || '' text: item.text?.content || ''
}; };
} }
if (item.type === 'file' && item.file?.type === ChatFileTypeEnum.image) { if (
item.type === ChatItemValueTypeEnum.file &&
item.file?.type === ChatFileTypeEnum.image
) {
return { return {
type: 'image_url', type: 'image_url',
image_url: { image_url: {
@ -64,7 +67,6 @@ export const chats2GPTMessages = ({
} }
}; };
} }
return;
}) })
.filter(Boolean) as ChatCompletionContentPart[]; .filter(Boolean) as ChatCompletionContentPart[];
@ -166,7 +168,7 @@ export const GPTMessages2Chats = (
} else if (item.type === 'image_url') { } else if (item.type === 'image_url') {
value.push({ value.push({
//@ts-ignore //@ts-ignore
type: 'file', type: ChatItemValueTypeEnum.file,
file: { file: {
type: ChatFileTypeEnum.image, type: ChatFileTypeEnum.image,
name: '', name: '',
@ -175,7 +177,6 @@ export const GPTMessages2Chats = (
}); });
} }
}); });
// @ts-ignore
} }
} else if ( } else if (
obj === ChatRoleEnum.AI && obj === ChatRoleEnum.AI &&

View File

@ -13,8 +13,8 @@ import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import { AppChatConfigType, AppSchema, VariableItemType } from '../app/type'; import { AppChatConfigType, AppSchema, VariableItemType } from '../app/type';
import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d'; import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d';
import { DatasetSearchModeEnum } from '../dataset/constants'; import { DatasetSearchModeEnum } from '../dataset/constants';
import { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type';
import { DispatchNodeResponseType } from '../workflow/runtime/type.d'; import { DispatchNodeResponseType } from '../workflow/runtime/type.d';
import { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type';
export type ChatSchema = { export type ChatSchema = {
_id: string; _id: string;
@ -115,6 +115,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
status: `${ChatStatusEnum}`; status: `${ChatStatusEnum}`;
moduleName?: string; moduleName?: string;
ttsBuffer?: Uint8Array; ttsBuffer?: Uint8Array;
responseData?: ChatHistoryItemResType[];
} & ChatBoxInputType; } & ChatBoxInputType;
/* --------- team chat --------- */ /* --------- team chat --------- */

View File

@ -65,11 +65,12 @@ export const filterPublicNodeResponseData = ({
}: { }: {
flowResponses?: ChatHistoryItemResType[]; flowResponses?: ChatHistoryItemResType[];
}) => { }) => {
const filedList = ['quoteList', 'moduleType']; const filedList = ['quoteList', 'moduleType', 'pluginOutput'];
const filterModuleTypeList: any[] = [ const filterModuleTypeList: any[] = [
FlowNodeTypeEnum.pluginModule, FlowNodeTypeEnum.pluginModule,
FlowNodeTypeEnum.datasetSearchNode, FlowNodeTypeEnum.datasetSearchNode,
FlowNodeTypeEnum.tools FlowNodeTypeEnum.tools,
FlowNodeTypeEnum.pluginOutput
]; ];
return flowResponses return flowResponses
@ -89,14 +90,22 @@ export const filterPublicNodeResponseData = ({
}); });
}; };
export const removeEmptyUserInput = (input: UserChatItemValueItemType[]) => { export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => {
return input.filter((item) => { return (
if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) { input?.filter((item) => {
return false; if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) {
} return false;
if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) { }
return false; if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) {
} return false;
return true; }
}); return true;
}) || []
);
};
export const getPluginOutputsFromChatResponses = (responses: ChatHistoryItemResType[]) => {
const outputs =
responses.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput)?.pluginOutput ?? {};
return outputs;
}; };

View File

@ -142,6 +142,9 @@ export type DispatchNodeResponseType = {
// code // code
codeLog?: string; codeLog?: string;
// plugin
pluginOutput?: Record<string, any>;
}; };
export type DispatchNodeResultType<T> = { export type DispatchNodeResultType<T> = {

View File

@ -123,6 +123,7 @@ export const checkNodeRunStatus = ({
(item) => item.target === node.nodeId (item) => item.target === node.nodeId
); );
// Entry
if (workflowEdges.length === 0) { if (workflowEdges.length === 0) {
return 'run'; return 'run';
} }

View File

@ -27,7 +27,7 @@ export const ToolModule: FlowNodeTemplateType = {
sourceHandle: getHandleConfig(true, true, false, true), sourceHandle: getHandleConfig(true, true, false, true),
targetHandle: getHandleConfig(true, true, false, true), targetHandle: getHandleConfig(true, true, false, true),
avatar: '/imgs/workflow/tool.svg', avatar: '/imgs/workflow/tool.svg',
name: '工具调用(实验)', name: '工具调用',
intro: '通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。', intro: '通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。',
showStatus: true, showStatus: true,
version: '481', version: '481',

View File

@ -22,6 +22,7 @@ import {
defaultWhisperConfig defaultWhisperConfig
} from '../app/constants'; } from '../app/constants';
import { IfElseResultEnum } from './template/system/ifElse/constant'; import { IfElseResultEnum } from './template/system/ifElse/constant';
import { RuntimeNodeItemType } from './runtime/type';
export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => { export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => {
return `${nodeId}-${type}-${key}`; return `${nodeId}-${type}-${key}`;
@ -190,3 +191,38 @@ export const isReferenceValue = (value: any): boolean => {
export const getElseIFLabel = (i: number) => { export const getElseIFLabel = (i: number) => {
return i === 0 ? IfElseResultEnum.IF : `${IfElseResultEnum.ELSE_IF} ${i}`; return i === 0 ? IfElseResultEnum.IF : `${IfElseResultEnum.ELSE_IF} ${i}`;
}; };
// add value to plugin input node when run plugin
export const updatePluginInputByVariables = (
nodes: RuntimeNodeItemType[],
variables: Record<string, any>
) => {
return nodes.map((node) =>
node.flowNodeType === FlowNodeTypeEnum.pluginInput
? {
...node,
inputs: node.inputs.map((input) => {
const parseValue = (() => {
try {
if (
input.valueType === WorkflowIOValueTypeEnum.string ||
input.valueType === WorkflowIOValueTypeEnum.number ||
input.valueType === WorkflowIOValueTypeEnum.boolean
)
return variables[input.key];
return JSON.parse(variables[input.key]);
} catch (e) {
return variables[input.key];
}
})();
return {
...input,
value: parseValue ?? input.value
};
})
}
: node
);
};

View File

@ -1,5 +1,4 @@
import { SystemPluginResponseType } from '../../type'; import { urlsFetch } from '@fastgpt/service/common/string/cheerio';
import { urlsFetch } from '../../../service/common/string/cheerio';
type Props = { type Props = {
url: string; url: string;

View File

@ -58,6 +58,7 @@ const AppSchema = new Schema({
type: String, type: String,
default: '' default: ''
}, },
updateTime: { updateTime: {
type: Date, type: Date,
default: () => new Date() default: () => new Date()
@ -112,7 +113,7 @@ const AppSchema = new Schema({
...getPermissionSchema(AppDefaultPermissionVal) ...getPermissionSchema(AppDefaultPermissionVal)
}); });
AppSchema.index({ updateTime: -1 }); AppSchema.index({ teamId: 1, updateTime: -1 });
AppSchema.index({ teamId: 1, type: 1 }); AppSchema.index({ teamId: 1, type: 1 });
AppSchema.index({ scheduledTriggerConfig: 1, intervalNextTime: -1 }); AppSchema.index({ scheduledTriggerConfig: 1, intervalNextTime: -1 });

View File

@ -89,7 +89,7 @@ try {
get chat logs; get chat logs;
close custom feedback; close custom feedback;
*/ */
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }, { background: true }); ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }, { background: true, unique: true });
// admin charts // admin charts
ChatItemSchema.index({ time: -1, obj: 1 }, { background: true }); ChatItemSchema.index({ time: -1, obj: 1 }, { background: true });
// timer, clear history // timer, clear history

View File

@ -85,7 +85,7 @@ try {
// get user history // get user history
ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 }, { background: true }); ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 }, { background: true });
// delete by appid; clear history; init chat; update chat; auth chat; get chat; // delete by appid; clear history; init chat; update chat; auth chat; get chat;
ChatSchema.index({ appId: 1, chatId: 1 }, { background: true }); ChatSchema.index({ appId: 1, chatId: 1 }, { background: true, unique: true });
// get chat logs; // get chat logs;
ChatSchema.index({ teamId: 1, appId: 1, updateTime: -1 }, { background: true }); ChatSchema.index({ teamId: 1, appId: 1, updateTime: -1 }, { background: true });

View File

@ -8,6 +8,7 @@ import {
DragStart, DragStart,
DropResult DropResult
} from 'react-beautiful-dnd'; } from 'react-beautiful-dnd';
export * from 'react-beautiful-dnd';
type Props<T = any> = { type Props<T = any> = {
onDragEndCb: (result: T[]) => void; onDragEndCb: (result: T[]) => void;
@ -57,5 +58,3 @@ function DndDrag<T>({ children, renderClone, onDragEndCb, dataList }: Props<T>)
} }
export default DndDrag; export default DndDrag;
export * from 'react-beautiful-dnd';

View File

@ -6,15 +6,15 @@ const CloseIcon = (props: FlexProps) => {
return ( return (
<Flex <Flex
cursor={'pointer'} cursor={'pointer'}
w={'22px'} w={'1.5rem'}
h={'22px'} h={'1.5rem'}
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
borderRadius={'50%'} borderRadius={'50%'}
_hover={{ bg: 'myGray.200' }} _hover={{ bg: 'myGray.200' }}
{...props} {...props}
> >
<MyIcon name={'common/closeLight'} w={'12px'} color={'myGray.500'} /> <MyIcon name={'common/closeLight'} w={'80%'} h={'80%'} color={'myGray.500'} />
</Flex> </Flex>
); );
}; };

View File

@ -7,11 +7,11 @@ import {
ModalCloseButton, ModalCloseButton,
ModalContentProps, ModalContentProps,
Box, Box,
Image, Image
useMediaQuery
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import MyIcon from '../Icon'; import MyIcon from '../Icon';
import MyBox from '../MyBox'; import MyBox from '../MyBox';
import { useSystem } from '../../../hooks/useSystem';
export interface MyModalProps extends ModalContentProps { export interface MyModalProps extends ModalContentProps {
iconSrc?: string; iconSrc?: string;
@ -34,7 +34,7 @@ const MyModal = ({
maxW = ['90vw', '600px'], maxW = ['90vw', '600px'],
...props ...props
}: MyModalProps) => { }: MyModalProps) => {
const [isPc] = useMediaQuery('(min-width: 900px)'); const isPc = useSystem();
return ( return (
<Modal <Modal

View File

@ -81,7 +81,6 @@ const MultipleSelect = <T = any,>({
borderRadius={'md'} borderRadius={'md'}
border={'base'} border={'base'}
userSelect={'none'} userSelect={'none'}
minH={'40px'}
cursor={'pointer'} cursor={'pointer'}
_active={{ _active={{
transform: 'none' transform: 'none'

View File

@ -1,4 +1,11 @@
import React, { useRef, forwardRef, useMemo } from 'react'; import React, {
useRef,
forwardRef,
useMemo,
useEffect,
useImperativeHandle,
ForwardedRef
} from 'react';
import { import {
Menu, Menu,
MenuList, MenuList,
@ -28,17 +35,21 @@ export type SelectProps<T = any> = ButtonProps & {
onchange?: (val: T) => void; onchange?: (val: T) => void;
}; };
const MySelect = <T = any,>({ const MySelect = <T = any,>(
placeholder, {
value, placeholder,
width = '100%', value,
list = [], width = '100%',
onchange, list = [],
isLoading = false, onchange,
...props isLoading = false,
}: SelectProps<T>) => { ...props
const ref = useRef<HTMLButtonElement>(null); }: SelectProps<T>,
const { Loading } = useLoading(); ref: ForwardedRef<{
focus: () => void;
}>
) => {
const ButtonRef = useRef<HTMLButtonElement>(null);
const menuItemStyles: MenuItemProps = { const menuItemStyles: MenuItemProps = {
borderRadius: 'sm', borderRadius: 'sm',
py: 2, py: 2,
@ -54,6 +65,12 @@ const MySelect = <T = any,>({
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]); const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
useImperativeHandle(ref, () => ({
focus() {
onOpen();
}
}));
return ( return (
<Box <Box
css={css({ css={css({
@ -72,7 +89,7 @@ const MySelect = <T = any,>({
> >
<MenuButton <MenuButton
as={Button} as={Button}
ref={ref} ref={ButtonRef}
width={width} width={width}
px={3} px={3}
rightIcon={<ChevronDownIcon />} rightIcon={<ChevronDownIcon />}
@ -98,7 +115,7 @@ const MySelect = <T = any,>({
<MenuList <MenuList
className={props.className} className={props.className}
minW={(() => { minW={(() => {
const w = ref.current?.clientWidth; const w = ButtonRef.current?.clientWidth;
if (w) { if (w) {
return `${w}px !important`; return `${w}px !important`;
} }
@ -152,4 +169,6 @@ const MySelect = <T = any,>({
); );
}; };
export default MySelect; export default forwardRef(MySelect) as <T>(
props: SelectProps<T> & { ref?: React.Ref<HTMLSelectElement> }
) => JSX.Element;

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import { Box, Tooltip, TooltipProps, css, useMediaQuery } from '@chakra-ui/react'; import { Box, Tooltip, TooltipProps } from '@chakra-ui/react';
import { useSystem } from '../../../hooks/useSystem';
interface Props extends TooltipProps { interface Props extends TooltipProps {}
forceShow?: boolean;
}
const MyTooltip = ({ children, forceShow = false, shouldWrapChildren = true, ...props }: Props) => { const MyTooltip = ({ children, shouldWrapChildren = true, ...props }: Props) => {
const [isPc] = useMediaQuery('(min-width: 900px)'); const { isPc } = useSystem();
return isPc || forceShow ? ( return (
<Tooltip <Tooltip
className="chakra-tooltip" className="chakra-tooltip"
bg={'white'} bg={'white'}
@ -27,8 +26,6 @@ const MyTooltip = ({ children, forceShow = false, shouldWrapChildren = true, ...
> >
{children} {children}
</Tooltip> </Tooltip>
) : (
<>{children}</>
); );
}; };

View File

@ -51,6 +51,7 @@ const LightRowTabs = <ValueType = string,>({
borderRadius={'sm'} borderRadius={'sm'}
fontSize={sizeMap.fontSize} fontSize={sizeMap.fontSize}
overflowX={'auto'} overflowX={'auto'}
userSelect={'none'}
{...props} {...props}
> >
{list.map((item) => ( {list.map((item) => (

View File

@ -23,6 +23,8 @@ type Props = Omit<BoxProps, 'resize' | 'onChange'> & {
variables?: EditorVariablePickerType[]; variables?: EditorVariablePickerType[];
defaultHeight?: number; defaultHeight?: number;
placeholder?: string; placeholder?: string;
isDisabled?: boolean;
isInvalid?: boolean;
}; };
const options = { const options = {
@ -55,6 +57,8 @@ const JSONEditor = ({
variables = [], variables = [],
placeholder, placeholder,
defaultHeight = 100, defaultHeight = 100,
isDisabled = false,
isInvalid = false,
...props ...props
}: Props) => { }: Props) => {
const { toast } = useToast(); const { toast } = useToast();
@ -209,9 +213,9 @@ const JSONEditor = ({
return ( return (
<Box <Box
borderWidth={'1px'} borderWidth={isInvalid ? '2px' : '1px'}
borderRadius={'md'} borderRadius={'md'}
borderColor={'myGray.200'} borderColor={isInvalid ? 'red.500' : 'myGray.200'}
py={2} py={2}
height={height} height={height}
position={'relative'} position={'relative'}

View File

@ -26,6 +26,7 @@
"Export Configs": "Export Configs", "Export Configs": "Export Configs",
"Feedback Count": "User Feedback", "Feedback Count": "User Feedback",
"Go to chat": "To chat", "Go to chat": "To chat",
"Go to run": "Run",
"Import Configs": "Import Configs", "Import Configs": "Import Configs",
"Import Configs Failed": "Failed to import configs, please ensure configs are valid!", "Import Configs Failed": "Failed to import configs, please ensure configs are valid!",
"Input Field Settings": "Input Field Settings", "Input Field Settings": "Input Field Settings",
@ -39,8 +40,12 @@
"My Apps": "My Apps", "My Apps": "My Apps",
"Output Field Settings": "Output Field Settings", "Output Field Settings": "Output Field Settings",
"Paste Config": "Paste Config", "Paste Config": "Paste Config",
"Plugin dispatch": "Plugins",
"Plugin dispatch tip": "It is up to the model to decide which plug-ins to add additional capabilities to. If the plug-in is selected, the knowledge base call is also treated as a special plug-in.",
"Publish channel": "Publish channel", "Publish channel": "Publish channel",
"Publish success": "Publish success", "Publish success": "Publish success",
"Run": "Run",
"Search app": "Search app",
"Setting app": "Settings", "Setting app": "Settings",
"Setting plugin": "Setting plugin", "Setting plugin": "Setting plugin",
"To Chat": "Go to Chat", "To Chat": "Go to Chat",

View File

@ -107,8 +107,10 @@
"Rename Success": "Rename Success", "Rename Success": "Rename Success",
"Request Error": "Request Error", "Request Error": "Request Error",
"Require Input": "Required Input", "Require Input": "Required Input",
"Restart": "Restart",
"Role": "Role", "Role": "Role",
"Root folder": "Root folder", "Root folder": "Root folder",
"Run": "Run",
"Save": "Save", "Save": "Save",
"Save Failed": "Save Failed", "Save Failed": "Save Failed",
"Save Success": "Save Success", "Save Success": "Save Success",

View File

@ -25,6 +25,7 @@
"Export Configs": "导出配置", "Export Configs": "导出配置",
"Feedback Count": "用户反馈", "Feedback Count": "用户反馈",
"Go to chat": "去对话", "Go to chat": "去对话",
"Go to run": "去运行",
"Import Configs": "导入配置", "Import Configs": "导入配置",
"Import Configs Failed": "导入配置失败,请确保配置正常!", "Import Configs Failed": "导入配置失败,请确保配置正常!",
"Input Field Settings": "输入字段编辑", "Input Field Settings": "输入字段编辑",
@ -38,8 +39,12 @@
"My Apps": "我的应用", "My Apps": "我的应用",
"Output Field Settings": "输出字段编辑", "Output Field Settings": "输出字段编辑",
"Paste Config": "粘贴配置", "Paste Config": "粘贴配置",
"Plugin dispatch": "插件调用",
"Plugin dispatch tip": "给模型附加额外的能力,具体调用哪些插件,将由模型自主决定。\n若选择了插件知识库调用将自动作为一个特殊的插件。",
"Publish channel": "发布渠道", "Publish channel": "发布渠道",
"Publish success": "发布成功", "Publish success": "发布成功",
"Run": "运行",
"Search app": "搜索应用",
"Setting app": "应用配置", "Setting app": "应用配置",
"Setting plugin": "插件配置", "Setting plugin": "插件配置",
"To Chat": "前去对话", "To Chat": "前去对话",

View File

@ -108,8 +108,10 @@
"Rename Success": "重命名成功", "Rename Success": "重命名成功",
"Request Error": "请求异常", "Request Error": "请求异常",
"Require Input": "必填", "Require Input": "必填",
"Restart": "重新开始",
"Role": "权限", "Role": "权限",
"Root folder": "根目录", "Root folder": "根目录",
"Run": "运行",
"Save": "保存", "Save": "保存",
"Save Failed": "保存失败", "Save Failed": "保存失败",
"Save Success": "保存成功", "Save Success": "保存成功",

View File

@ -38,8 +38,8 @@
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/papaparse": "^5.3.7", "@types/papaparse": "^5.3.7",
"@types/react": "18.3.0", "@types/react": "18.3.1",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.1",
"@types/react-dom": "18.3.0" "@types/react-dom": "18.3.0"
} }
} }

View File

@ -314,7 +314,7 @@ const Input: ComponentStyleConfig = {
}), }),
md: defineStyle({ md: defineStyle({
field: { field: {
h: '40px', h: '34px',
borderRadius: 'md' borderRadius: 'md'
} }
}) })

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
# --------- install dependence ----------- # --------- install dependence -----------
FROM node:18.17-alpine AS mainDeps FROM node:20.14.0-alpine AS mainDeps
WORKDIR /app WORKDIR /app
ARG proxy ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@8.6.0 RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0
# if proxy exists, set proxy # if proxy exists, set proxy
RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npmmirror.com RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npmmirror.com
@ -19,7 +19,7 @@ RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1)
RUN pnpm i RUN pnpm i
# --------- builder ----------- # --------- builder -----------
FROM node:18.17-alpine AS builder FROM node:20.14.0-alpine AS builder
WORKDIR /app WORKDIR /app
ARG proxy ARG proxy
@ -33,13 +33,13 @@ COPY --from=mainDeps /app/projects/app/node_modules ./projects/app/node_modules
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@8.6.0 RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0
ENV NODE_OPTIONS="--max-old-space-size=4096" ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm --filter=app build RUN pnpm --filter=app build
# --------- runner ----------- # --------- runner -----------
FROM node:18.17-alpine AS runner FROM node:20.14.0-alpine AS runner
WORKDIR /app WORKDIR /app
ARG proxy ARG proxy

View File

@ -72,13 +72,13 @@
"@types/jsonwebtoken": "^9.0.3", "@types/jsonwebtoken": "^9.0.3",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/react": "18.3.0", "@types/react": "18.3.1",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "^0.0.37", "@types/request-ip": "^0.0.37",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-next": "14.2.3", "eslint-config-next": "14.2.3",
"nextjs-node-loader": "^1.1.5", "nextjs-node-loader": "^1.1.5",
"typescript": "4.9.5" "typescript": "^5.1.3"
} }
} }

View File

@ -1,262 +0,0 @@
import {
Box,
BoxProps,
Card,
Flex,
useTheme,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Image
} from '@chakra-ui/react';
import React, { useMemo } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { formatChatValue2InputType } from '../utils';
import Markdown, { CodeClassName } from '@/components/Markdown';
import styles from '../index.module.scss';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatStatusEnum
} from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { ChatBoxContext } from '../Provider';
import Avatar from '@/components/Avatar';
import { useContextSelector } from 'use-context-selector';
const colorMap = {
[ChatStatusEnum.loading]: {
bg: 'myGray.100',
color: 'myGray.600'
},
[ChatStatusEnum.running]: {
bg: 'green.50',
color: 'green.700'
},
[ChatStatusEnum.finish]: {
bg: 'green.50',
color: 'green.700'
}
};
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
...chatControllerProps
}: {
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
avatar?: string;
statusBoxData?: {
status: `${ChatStatusEnum}`;
name: string;
};
questionGuides?: string[];
children?: React.ReactNode;
} & ChatControllerProps) => {
const styleMap: BoxProps =
type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const { chat } = chatControllerProps;
const ContentCard = useMemo(() => {
if (type === 'Human') {
const { text, files = [] } = formatChatValue2InputType(chat.value);
return (
<>
{files.length > 0 && <FilesBlock files={files} />}
<Markdown source={text} />
</>
);
}
/* AI */
return (
<Flex flexDirection={'column'} key={chat.dataId} gap={2}>
{chat.value.map((value, i) => {
const key = `${chat.dataId}-ai-${i}`;
if (value.text) {
let source = (value.text?.content || '').trim();
if (!source && chat.value.length > 1) return null;
if (
isLastChild &&
!isChatting &&
questionGuides.length > 0 &&
i === chat.value.length - 1
) {
source = `${source}
\`\`\`${CodeClassName.questionGuide}
${JSON.stringify(questionGuides)}`;
}
return (
<Markdown
key={key}
source={source}
showAnimation={isLastChild && isChatting && i === chat.value.length - 1}
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return (
<Box key={key}>
{value.tools.map((tool) => {
const toolParams = (() => {
try {
return JSON.stringify(JSON.parse(tool.params), null, 2);
} catch (error) {
return tool.params;
}
})();
const toolResponse = (() => {
try {
return JSON.stringify(JSON.parse(tool.response), null, 2);
} catch (error) {
return tool.response;
}
})();
return (
<Box key={tool.id}>
<Accordion allowToggle>
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />
)}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
px={0}
mt={0}
borderRadius={'md'}
overflow={'hidden'}
maxH={'500px'}
overflowY={'auto'}
>
{toolParams && toolParams !== '{}' && (
<Markdown
source={`~~~json#Input
${toolParams}`}
/>
)}
{toolResponse && (
<Markdown
source={`~~~json#Response
${toolResponse}`}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
);
})}
</Box>
);
}
return null;
})}
</Flex>
);
}, [chat.dataId, chat.value, isChatting, isLastChild, questionGuides, type]);
const chatStatusMap = useMemo(() => {
if (!statusBoxData?.status) return;
return colorMap[statusBoxData.status];
}, [statusBoxData?.status]);
return (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
</Box>
)}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}
w="8px"
h="8px"
borderRadius={'50%'}
mt={'1px'}
/>
<Box ml={2} color={'myGray.600'}>
{statusBoxData.name}
</Box>
</Flex>
)}
</Flex>
{/* content */}
<Box mt={['6px', 2]} textAlign={styleMap.textAlign}>
<Card
className="markdown"
{...MessageCardStyle}
bg={styleMap.bg}
borderRadius={styleMap.borderRadius}
textAlign={'left'}
>
{ContentCard}
{children}
</Card>
</Box>
</>
);
};
export default React.memo(ChatItem);

View File

@ -1,325 +0,0 @@
import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image, BoxProps } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Markdown from '../../Markdown';
import { QuoteList } from './QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
function RowRender({
children,
mb,
label,
...props
}: { children: React.ReactNode; label: string } & BoxProps) {
return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
}
function Row({
label,
value,
rawDom
}: {
label: string;
value?: string | number | boolean | object;
rawDom?: React.ReactNode;
}) {
const theme = useTheme();
const val = value || rawDom;
const isObject = typeof value === 'object';
const formatValue = useMemo(() => {
if (isObject) {
return `~~~json\n${JSON.stringify(value, null, 2)}`;
}
return `${value}`;
}, [isObject, value]);
if (rawDom) {
return (
<RowRender label={label} mb={1}>
{rawDom}
</RowRender>
);
}
if (val === undefined || val === '' || val === 'undefined') return null;
return (
<RowRender
label={label}
mb={isObject ? 0 : 1}
{...(isObject
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
<Markdown source={formatValue} />
</RowRender>
);
}
const WholeResponseModal = ({
response,
showDetail,
onClose
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isCentered
isOpen={true}
onClose={onClose}
h={['90vh', '80vh']}
minW={['90vw', '600px']}
iconSrc="/imgs/modal/wholeRecord.svg"
title={
<Flex alignItems={'center'}>
{t('core.chat.response.Complete Response')}
<QuestionTip ml={2} label={'从左往右,为各个模块的响应顺序'}></QuestionTip>
</Flex>
}
>
<Flex h={'100%'} flexDirection={'column'}>
<ResponseBox response={response} showDetail={showDetail} />
</Flex>
</MyModal>
);
};
export default WholeResponseModal;
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
const { workflowT } = useI18n();
const list = useMemo(
() =>
response.map((item, i) => ({
label: (
<Flex alignItems={'center'} justifyContent={'center'} px={2}>
<Image
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
w={['14px', '16px']}
/>
{t(item.moduleName)}
</Flex>
),
value: `${i}`
})),
[response, t]
);
const [currentTab, setCurrentTab] = useState(`0`);
const activeModule = useMemo(() => response[Number(currentTab)], [currentTab, response]);
return (
<>
{!hideTabs && (
<Box>
<LightRowTabs list={list} value={currentTab} onChange={setCurrentTab} />
</Box>
)}
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.totalPoints !== undefined && (
<Row
label={t('support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row
label={t('core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={workflowT('response.Error')} value={activeModule?.error} />
</>
{/* ai chat */}
<>
<Row
label={t('core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row label={t('core.chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={theme.borders.base} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row label={t('core.chat.response.module similarity')} value={activeModule?.similarity} />
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row label={t('core.chat.response.plugin output')} value={activeModule?.pluginOutput} />
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin response detail')}
rawDom={<ResponseBox response={activeModule.pluginDetail} showDetail={showDetail} />}
/>
)}
</>
{/* text output */}
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
{/* tool call */}
{activeModule?.toolDetail && activeModule?.toolDetail.length > 0 && (
<Row
label={t('core.chat.response.Tool call response detail')}
rawDom={<ResponseBox response={activeModule.toolDetail} showDetail={showDetail} />}
/>
)}
{/* code */}
<Row label={workflowT('response.Custom outputs')} value={activeModule?.customOutputs} />
<Row label={workflowT('response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={workflowT('response.Code log')} value={activeModule?.codeLog} />
</Box>
</>
);
});

View File

@ -10,6 +10,7 @@ import { getUnreadCount } from '@/web/support/user/inform/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Auth from './auth'; import Auth from './auth';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Navbar = dynamic(() => import('./navbar')); const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone')); const NavbarPhone = dynamic(() => import('./navbarPhone'));
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal')); const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
@ -43,7 +44,8 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
const Layout = ({ children }: { children: JSX.Element }) => { const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter(); const router = useRouter();
const { Loading } = useLoading(); const { Loading } = useLoading();
const { loading, setScreenWidth, isPc, feConfigs, isNotSufficientModal } = useSystemStore(); const { loading, feConfigs, isNotSufficientModal } = useSystemStore();
const { isPc } = useSystem();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const isChatPage = useMemo( const isChatPage = useMemo(
@ -51,21 +53,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
[router.pathname, router.query] [router.pathname, router.query]
); );
// listen screen width
useEffect(() => {
const resize = throttle(() => {
setScreenWidth(document.documentElement.clientWidth);
}, 300);
window.addEventListener('resize', resize);
resize();
return () => {
window.removeEventListener('resize', resize);
};
}, [setScreenWidth]);
const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, { const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, {
enabled: !!userInfo && !!feConfigs.isPlus, enabled: !!userInfo && !!feConfigs.isPlus,
refetchInterval: 10000 refetchInterval: 10000

View File

@ -32,20 +32,22 @@ export const useFolderDrag = ({
e.preventDefault(); e.preventDefault();
setTargetId(undefined); setTargetId(undefined);
}, },
onDrop: async (e: DragEvent<HTMLDivElement>) => { ...(isFolder && {
e.preventDefault(); onDrop: async (e: DragEvent<HTMLDivElement>) => {
setTrue(); e.preventDefault();
setTrue();
try { try {
if (targetId && dragId && targetId !== dragId) { if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId); await onDrop(dragId, targetId);
} }
} catch (error) {} } catch (error) {}
setTargetId(undefined); setTargetId(undefined);
setDragId(undefined); setDragId(undefined);
setFalse(); setFalse();
}, }
}),
...(activeStyles && ...(activeStyles &&
targetId === dataId && { targetId === dataId && {
...activeStyles ...activeStyles

View File

@ -18,6 +18,7 @@ import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const InputGuideBox = dynamic(() => import('./InputGuideBox')); const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@ -53,7 +54,9 @@ const ChatInput = ({
const { isChatting, whisperConfig, autoTTSResponse, chatInputGuide, outLinkAuthData } = const { isChatting, whisperConfig, autoTTSResponse, chatInputGuide, outLinkAuthData } =
useContextSelector(ChatBoxContext, (v) => v); useContextSelector(ChatBoxContext, (v) => v);
const { isPc, whisperModel } = useSystemStore(); const { whisperModel } = useSystemStore();
const { isPc } = useSystem();
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -15,38 +15,53 @@ import {
defaultWhisperConfig defaultWhisperConfig
} from '@fastgpt/global/core/app/constants'; } from '@fastgpt/global/core/app/constants';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
export type ChatProviderProps = OutLinkChatAuthProps & {
appAvatar?: string;
chatConfig?: AppChatConfigType;
type useChatStoreType = OutLinkChatAuthProps & {
welcomeText: string;
variableList: VariableItemType[];
questionGuide: boolean;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
autoTTSResponse: boolean;
startSegmentedAudio: () => Promise<any>;
splitText2Audio: (text: string, done?: boolean | undefined) => void;
finishSegmentedAudio: () => void;
audioLoading: boolean;
audioPlaying: boolean;
hasAudio: boolean;
playAudioByText: ({
text,
buffer
}: {
text: string;
buffer?: Uint8Array | undefined;
}) => Promise<{
buffer?: Uint8Array | undefined;
}>;
cancelAudio: () => void;
audioPlayingChatId: string | undefined;
setAudioPlayingChatId: React.Dispatch<React.SetStateAction<string | undefined>>;
chatHistories: ChatSiteItemType[]; chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>; setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
isChatting: boolean; variablesForm: UseFormReturn<FieldValues, any>;
chatInputGuide: ChatInputGuideConfigType;
outLinkAuthData: OutLinkChatAuthProps; // not chat test params
chatId?: string;
}; };
type useChatStoreType = OutLinkChatAuthProps &
ChatProviderProps & {
welcomeText: string;
variableList: VariableItemType[];
questionGuide: boolean;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
autoTTSResponse: boolean;
startSegmentedAudio: () => Promise<any>;
splitText2Audio: (text: string, done?: boolean | undefined) => void;
finishSegmentedAudio: () => void;
audioLoading: boolean;
audioPlaying: boolean;
hasAudio: boolean;
playAudioByText: ({
text,
buffer
}: {
text: string;
buffer?: Uint8Array | undefined;
}) => Promise<{
buffer?: Uint8Array | undefined;
}>;
cancelAudio: () => void;
audioPlayingChatId: string | undefined;
setAudioPlayingChatId: React.Dispatch<React.SetStateAction<string | undefined>>;
isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
outLinkAuthData: OutLinkChatAuthProps;
};
export const ChatBoxContext = createContext<useChatStoreType>({ export const ChatBoxContext = createContext<useChatStoreType>({
welcomeText: '', welcomeText: '',
variableList: [], variableList: [],
@ -100,27 +115,27 @@ export const ChatBoxContext = createContext<useChatStoreType>({
open: false, open: false,
customUrl: '' customUrl: ''
}, },
outLinkAuthData: {} outLinkAuthData: {},
// @ts-ignore
variablesForm: undefined
}); });
export type ChatProviderProps = OutLinkChatAuthProps & {
chatConfig?: AppChatConfigType;
// not chat test params
chatId?: string;
children: React.ReactNode;
};
const Provider = ({ const Provider = ({
shareId, shareId,
outLinkUid, outLinkUid,
teamId, teamId,
teamToken, teamToken,
chatConfig = {},
children
}: ChatProviderProps) => {
const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]);
chatHistories,
setChatHistories,
variablesForm,
chatConfig = {},
children,
...props
}: ChatProviderProps & {
children: React.ReactNode;
}) => {
const { const {
welcomeText = '', welcomeText = '',
variables = [], variables = [],
@ -167,12 +182,13 @@ const Provider = ({
); );
const value: useChatStoreType = { const value: useChatStoreType = {
...props,
shareId, shareId,
outLinkUid, outLinkUid,
teamId, teamId,
teamToken, teamToken,
welcomeText, welcomeText,
variableList: variables, variableList: variables.filter((item) => item.type !== VariableInputEnum.custom),
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,
@ -191,7 +207,8 @@ const Provider = ({
setChatHistories, setChatHistories,
isChatting, isChatting,
chatInputGuide, chatInputGuide,
outLinkAuthData outLinkAuthData,
variablesForm
}; };
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>; return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;

View File

@ -0,0 +1,158 @@
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { formatChatValue2InputType } from '../utils';
import Markdown from '@/components/Markdown';
import styles from '../index.module.scss';
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector';
import AIResponseBox from '../../../components/AIResponseBox';
const colorMap = {
[ChatStatusEnum.loading]: {
bg: 'myGray.100',
color: 'myGray.600'
},
[ChatStatusEnum.running]: {
bg: 'green.50',
color: 'green.700'
},
[ChatStatusEnum.finish]: {
bg: 'green.50',
color: 'green.700'
}
};
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
...chatControllerProps
}: {
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
avatar?: string;
statusBoxData?: {
status: `${ChatStatusEnum}`;
name: string;
};
questionGuides?: string[];
children?: React.ReactNode;
} & ChatControllerProps) => {
const styleMap: BoxProps =
type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const { chat } = chatControllerProps;
const ContentCard = useMemo(() => {
if (type === 'Human') {
const { text, files = [] } = formatChatValue2InputType(chat.value);
return (
<>
{files.length > 0 && <FilesBlock files={files} />}
<Markdown source={text} />
</>
);
}
/* AI */
return (
<Flex flexDirection={'column'} key={chat.dataId} gap={2}>
{chat.value.map((value, i) => {
const key = `${chat.dataId}-ai-${i}`;
return (
<AIResponseBox
key={key}
value={value}
index={i}
chat={chat}
isLastChild={isLastChild}
isChatting={isChatting}
questionGuides={questionGuides}
/>
);
})}
</Flex>
);
}, [chat, isChatting, isLastChild, questionGuides, type]);
const chatStatusMap = useMemo(() => {
if (!statusBoxData?.status) return;
return colorMap[statusBoxData.status];
}, [statusBoxData?.status]);
return (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
</Box>
)}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}
w="8px"
h="8px"
borderRadius={'50%'}
mt={'1px'}
/>
<Box ml={2} color={'myGray.600'}>
{statusBoxData.name}
</Box>
</Flex>
)}
</Flex>
{/* content */}
<Box mt={['6px', 2]} textAlign={styleMap.textAlign}>
<Card
className="markdown"
{...MessageCardStyle}
bg={styleMap.bg}
borderRadius={styleMap.borderRadius}
textAlign={'left'}
>
{ContentCard}
{children}
</Card>
</Box>
</>
);
};
export default React.memo(ChatItem);

View File

@ -1,6 +1,6 @@
import { Box, Flex, Grid } from '@chakra-ui/react'; import { Box, Flex, Grid } from '@chakra-ui/react';
import MdImage from '@/components/Markdown/img/Image'; import MdImage from '@/components/Markdown/img/Image';
import { UserInputFileItemType } from '@/components/ChatBox/type'; import { UserInputFileItemType } from '@/components/core/chat/ChatContainer/ChatBox/type';
const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => { const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => {
return ( return (

View File

@ -4,8 +4,8 @@ import { ModalBody, Box, useTheme } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import QuoteItem from '../../core/dataset/QuoteItem'; import QuoteItem from '@/components/core/dataset/QuoteItem';
import RawSourceBox from '../../core/dataset/RawSourceBox'; import RawSourceBox from '@/components/core/dataset/RawSourceBox';
const QuoteModal = ({ const QuoteModal = ({
rawSearch = [], rawSearch = [],

View File

@ -3,7 +3,6 @@ import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d'; import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
import { Flex, useDisclosure, useTheme, Box } from '@chakra-ui/react'; import { Flex, useDisclosure, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
@ -13,10 +12,11 @@ import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider'; import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools'; import { strIsLink } from '@fastgpt/global/common/string/tools';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const QuoteModal = dynamic(() => import('./QuoteModal')); const QuoteModal = dynamic(() => import('./QuoteModal'));
const ContextModal = dynamic(() => import('./ContextModal')); const ContextModal = dynamic(() => import('./ContextModal'));
const WholeResponseModal = dynamic(() => import('./WholeResponseModal')); const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
const isLLMNode = (item: ChatHistoryItemResType) => const isLLMNode = (item: ChatHistoryItemResType) =>
item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.tools; item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.tools;
@ -29,7 +29,7 @@ const ResponseTags = ({
showDetail: boolean; showDetail: boolean;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { t } = useTranslation(); const { t } = useTranslation();
const [quoteModalData, setQuoteModalData] = useState<{ const [quoteModalData, setQuoteModalData] = useState<{
rawSearch: SearchDataResponseItemType[]; rawSearch: SearchDataResponseItemType[];

View File

@ -1,32 +1,27 @@
import { VariableItemType } from '@fastgpt/global/core/app/type.d';
import React from 'react'; import React from 'react';
import { UseFormReturn } from 'react-hook-form'; import { Controller, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react'; import { Box, Button, Card, FormControl, Input, Textarea } from '@chakra-ui/react';
import ChatAvatar from './ChatAvatar'; import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants'; import { MessageCardStyle } from '../constants';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatBoxInputFormType } from '../type.d'; import { ChatBoxInputFormType } from '../type.d';
import { useRefresh } from '@fastgpt/web/hooks/useRefresh'; import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const VariableInput = ({ const VariableInput = ({
appAvatar,
variableList,
chatForm, chatForm,
onSubmitVariables chatStarted
}: { }: {
appAvatar?: string; chatStarted: boolean;
variableList: VariableItemType[];
onSubmitVariables: (e: Record<string, any>) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>; chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
const variables = watch('variables'); const { appAvatar, variableList, variablesForm } = useContextSelector(ChatBoxContext, (v) => v);
const chatStarted = watch('chatStarted'); const { register, getValues, setValue, handleSubmit: handleSubmitChat, control } = variablesForm;
const { refresh } = useRefresh();
return ( return (
<Box py={3}> <Box py={3}>
@ -61,7 +56,7 @@ const VariableInput = ({
{item.type === VariableInputEnum.input && ( {item.type === VariableInputEnum.input && (
<Input <Input
bg={'myWhite.400'} bg={'myWhite.400'}
{...register(`variables.${item.key}`, { {...register(item.key, {
required: item.required required: item.required
})} })}
/> />
@ -69,7 +64,7 @@ const VariableInput = ({
{item.type === VariableInputEnum.textarea && ( {item.type === VariableInputEnum.textarea && (
<Textarea <Textarea
bg={'myWhite.400'} bg={'myWhite.400'}
{...register(`variables.${item.key}`, { {...register(item.key, {
required: item.required required: item.required
})} })}
rows={5} rows={5}
@ -77,19 +72,24 @@ const VariableInput = ({
/> />
)} )}
{item.type === VariableInputEnum.select && ( {item.type === VariableInputEnum.select && (
<MySelect <Controller
width={'100%'} key={item.key}
list={(item.enums || []).map((item) => ({ control={control}
label: item.value, name={item.key}
value: item.value rules={{ required: item.required }}
}))} render={({ field: { ref, value } }) => {
{...register(`variables.${item.key}`, { return (
required: item.required <MySelect
})} ref={ref}
value={variables[item.key]} width={'100%'}
onchange={(e) => { list={(item.enums || []).map((item) => ({
refresh(); label: item.value,
setValue(`variables.${item.key}`, e); value: item.value
}))}
value={value}
onchange={(e) => setValue(item.key, e)}
/>
);
}} }}
/> />
)} )}
@ -100,8 +100,8 @@ const VariableInput = ({
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />} leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
size={'sm'} size={'sm'}
maxW={'100px'} maxW={'100px'}
onClick={handleSubmitChat((data) => { onClick={handleSubmitChat(() => {
onSubmitVariables(data); chatForm.setValue('chatStarted', true);
})} })}
> >
{t('core.chat.Start Chat')} {t('core.chat.Start Chat')}

View File

@ -3,8 +3,12 @@ import React from 'react';
import { MessageCardStyle } from '../constants'; import { MessageCardStyle } from '../constants';
import Markdown from '@/components/Markdown'; import Markdown from '@/components/Markdown';
import ChatAvatar from './ChatAvatar'; import ChatAvatar from './ChatAvatar';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
const appAvatar = useContextSelector(ChatBoxContext, (v) => v.appAvatar);
const WelcomeBox = ({ appAvatar, welcomeText }: { appAvatar?: string; welcomeText: string }) => {
return ( return (
<Box py={3}> <Box py={3}>
{/* avatar */} {/* avatar */}

View File

@ -9,7 +9,6 @@ import React, {
useEffect useEffect
} from 'react'; } from 'react';
import Script from 'next/script'; import Script from 'next/script';
import { throttle } from 'lodash';
import type { import type {
AIChatItemValueItemType, AIChatItemValueItemType,
ChatSiteItemType, ChatSiteItemType,
@ -20,7 +19,6 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Flex, Checkbox } from '@chakra-ui/react'; import { Box, Flex, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
@ -35,30 +33,25 @@ import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api'; import { postQuestionGuide } from '@/web/core/ai/api';
import type { import type { ComponentRef, ChatBoxInputType, ChatBoxInputFormType } from './type.d';
generatingMessageProps, import type { StartChatFnProps, generatingMessageProps } from '../type';
StartChatFnProps,
ComponentRef,
ChatBoxInputType,
ChatBoxInputFormType
} from './type.d';
import ChatInput from './Input/ChatInput'; import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../core/chat/Divider'; import ChatBoxDivider from '../../Divider';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { formatChatValue2InputType } from './utils'; import { formatChatValue2InputType } from './utils';
import { textareaMinH } from './constants'; import { textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import ChatProvider, { ChatBoxContext } from './Provider'; import ChatProvider, { ChatBoxContext, ChatProviderProps } from './Provider';
import ChatItem from './components/ChatItem'; import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useCreation } from 'ahooks';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import type { StreamResponseType } from '@/web/common/api/fetch'; import type { StreamResponseType } from '@/web/common/api/fetch';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useThrottleFn } from 'ahooks';
const ResponseTags = dynamic(() => import('./components/ResponseTags')); const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
@ -74,29 +67,26 @@ enum FeedbackTypeEnum {
hidden = 'hidden' hidden = 'hidden'
} }
type Props = OutLinkChatAuthProps & { type Props = OutLinkChatAuthProps &
feedbackType?: `${FeedbackTypeEnum}`; ChatProviderProps & {
showMarkIcon?: boolean; // admin mark dataset feedbackType?: `${FeedbackTypeEnum}`;
showVoiceIcon?: boolean; showMarkIcon?: boolean; // admin mark dataset
showEmptyIntro?: boolean; showVoiceIcon?: boolean;
appAvatar?: string; showEmptyIntro?: boolean;
userAvatar?: string; userAvatar?: string;
chatConfig?: AppChatConfigType; showFileSelector?: boolean;
showFileSelector?: boolean; active?: boolean; // can use
active?: boolean; // can use appId: string;
appId: string;
// not chat test params // not chat test params
chatId?: string;
onUpdateVariable?: (e: Record<string, any>) => void; onStartChat?: (e: StartChatFnProps) => Promise<
onStartChat?: (e: StartChatFnProps) => Promise< StreamResponseType & {
StreamResponseType & { isNewChat?: boolean;
isNewChat?: boolean; }
} >;
>; onDelMessage?: (e: { contentId: string }) => void;
onDelMessage?: (e: { contentId: string }) => void; };
};
/* /*
The input is divided into sections The input is divided into sections
@ -122,7 +112,6 @@ const ChatBox = (
outLinkUid, outLinkUid,
teamId, teamId,
teamToken, teamToken,
onUpdateVariable,
onStartChat, onStartChat,
onDelMessage onDelMessage
}: Props, }: Props,
@ -132,10 +121,12 @@ const ChatBox = (
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { isPc, setLoading, feConfigs } = useSystemStore(); const { setLoading, feConfigs } = useSystemStore();
const { isPc } = useSystem();
const TextareaDom = useRef<HTMLTextAreaElement>(null); const TextareaDom = useRef<HTMLTextAreaElement>(null);
const chatController = useRef(new AbortController()); const chatController = useRef(new AbortController());
const questionGuideController = useRef(new AbortController()); const questionGuideController = useRef(new AbortController());
const pluginController = useRef(new AbortController());
const isNewChatReplace = useRef(false); const isNewChatReplace = useRef(false);
const [feedbackId, setFeedbackId] = useState<string>(); const [feedbackId, setFeedbackId] = useState<string>();
@ -156,6 +147,7 @@ const ChatBox = (
splitText2Audio, splitText2Audio,
chatHistories, chatHistories,
setChatHistories, setChatHistories,
variablesForm,
isChatting isChatting
} = useContextSelector(ChatBoxContext, (v) => v); } = useContextSelector(ChatBoxContext, (v) => v);
@ -164,41 +156,44 @@ const ChatBox = (
defaultValues: { defaultValues: {
input: '', input: '',
files: [], files: [],
variables: {},
chatStarted: false chatStarted: false
} }
}); });
const { setValue, watch, handleSubmit } = chatForm; const { setValue, watch } = chatForm;
const chatStarted = watch('chatStarted'); const chatStartedWatch = watch('chatStarted');
const chatStarted = chatStartedWatch || chatHistories.length > 0 || variableList.length === 0;
/* variable */
const filterVariableNodes = useCreation(
() => variableList.filter((item) => item.type !== VariableInputEnum.custom),
[variableList]
);
// 滚动到底部 // 滚动到底部
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => { const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
if (!ChatBoxRef.current) return; setTimeout(() => {
ChatBoxRef.current.scrollTo({ if (!ChatBoxRef.current) {
top: ChatBoxRef.current.scrollHeight, setTimeout(() => {
behavior scrollToBottom(behavior);
}); }, 500);
}; } else {
ChatBoxRef.current.scrollTo({
top: ChatBoxRef.current.scrollHeight,
behavior
});
}
}, delay);
}, []);
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部 // 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
const generatingScroll = useCallback( const { run: generatingScroll } = useThrottleFn(
throttle(() => { () => {
if (!ChatBoxRef.current) return; if (!ChatBoxRef.current) return;
const isBottom = const isBottom =
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >= ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
ChatBoxRef.current.scrollHeight; ChatBoxRef.current.scrollHeight;
isBottom && scrollToBottom('auto'); isBottom && scrollToBottom('auto');
}, 100), },
[] {
wait: 100
}
); );
// eslint-disable-next-line react-hooks/exhaustive-deps
const generatingMessage = useCallback( const generatingMessage = useCallback(
({ ({
event, event,
@ -291,7 +286,7 @@ const ChatBox = (
}) })
}; };
} else if (event === SseResponseEventEnum.updateVariables && variables) { } else if (event === SseResponseEventEnum.updateVariables && variables) {
setValue('variables', variables); variablesForm.reset(variables);
} }
return item; return item;
@ -299,7 +294,7 @@ const ChatBox = (
); );
generatingScroll(); generatingScroll();
}, },
[generatingScroll, setChatHistories, setValue, splitText2Audio] [generatingScroll, setChatHistories, splitText2Audio, variablesForm]
); );
// 重置输入内容 // 重置输入内容
@ -347,13 +342,14 @@ const ChatBox = (
} }
} catch (error) {} } catch (error) {}
}, },
[questionGuide, shareId, outLinkUid, teamId, teamToken] [questionGuide, shareId, outLinkUid, teamId, teamToken, scrollToBottom]
); );
/* Abort chat completions, questionGuide */ /* Abort chat completions, questionGuide */
const abortRequest = useCallback(() => { const abortRequest = useCallback(() => {
chatController.current?.abort('stop'); chatController.current?.abort('stop');
questionGuideController.current?.abort('stop'); questionGuideController.current?.abort('stop');
pluginController.current?.abort('stop');
}, []); }, []);
/** /**
@ -369,8 +365,8 @@ const ChatBox = (
autoTTSResponse?: boolean; autoTTSResponse?: boolean;
history?: ChatSiteItemType[]; history?: ChatSiteItemType[];
}) => { }) => {
handleSubmit( variablesForm.handleSubmit(
async ({ variables }) => { async (variables) => {
if (!onStartChat) return; if (!onStartChat) return;
if (isChatting) { if (isChatting) {
toast({ toast({
@ -455,9 +451,7 @@ const ChatBox = (
// 清空输入内容 // 清空输入内容
resetInputVal({}); resetInputVal({});
setQuestionGuide([]); setQuestionGuide([]);
setTimeout(() => { scrollToBottom('smooth', 100);
scrollToBottom();
}, 100);
try { try {
// create abort obj // create abort obj
const abortSignal = new AbortController(); const abortSignal = new AbortController();
@ -470,8 +464,8 @@ const ChatBox = (
responseText, responseText,
isNewChat = false isNewChat = false
} = await onStartChat({ } = await onStartChat({
chatList: newChatList, messages: messages.slice(0, -1),
messages, responseChatItemId: responseChatId,
controller: abortSignal, controller: abortSignal,
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }), generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables: requestVariables variables: requestVariables
@ -542,7 +536,7 @@ const ChatBox = (
autoTTSResponse && finishSegmentedAudio(); autoTTSResponse && finishSegmentedAudio();
}, },
(err) => { (err) => {
console.log(err?.variables); console.log(err);
} }
)(); )();
}, },
@ -553,18 +547,19 @@ const ChatBox = (
finishSegmentedAudio, finishSegmentedAudio,
generatingMessage, generatingMessage,
generatingScroll, generatingScroll,
handleSubmit,
isChatting, isChatting,
isPc, isPc,
onStartChat, onStartChat,
resetInputVal, resetInputVal,
scrollToBottom,
setAudioPlayingChatId, setAudioPlayingChatId,
setChatHistories, setChatHistories,
splitText2Audio, splitText2Audio,
startSegmentedAudio, startSegmentedAudio,
t, t,
toast, toast,
variableList variableList,
variablesForm
] ]
); );
@ -720,7 +715,7 @@ const ChatBox = (
}, },
[appId, chatId, feedbackType, setChatHistories, teamId, teamToken] [appId, chatId, feedbackType, setChatHistories, teamId, teamToken]
); );
const onADdUserDislike = useCallback( const onAddUserDislike = useCallback(
(chat: ChatSiteItemType) => { (chat: ChatSiteItemType) => {
if ( if (
feedbackType !== FeedbackTypeEnum.user || feedbackType !== FeedbackTypeEnum.user ||
@ -797,30 +792,18 @@ const ChatBox = (
[appId, chatId, setChatHistories] [appId, chatId, setChatHistories]
); );
const resetVariables = useCallback(
(e: Record<string, any> = {}) => {
const value: Record<string, any> = { ...e };
filterVariableNodes?.forEach((item) => {
value[item.key] = e[item.key] || '';
});
setValue('variables', value);
},
[filterVariableNodes, setValue]
);
const showEmpty = useMemo( const showEmpty = useMemo(
() => () =>
feConfigs?.show_emptyChat && feConfigs?.show_emptyChat &&
showEmptyIntro && showEmptyIntro &&
chatHistories.length === 0 && chatHistories.length === 0 &&
!filterVariableNodes?.length && !variableList?.length &&
!welcomeText, !welcomeText,
[ [
chatHistories.length, chatHistories.length,
feConfigs?.show_emptyChat, feConfigs?.show_emptyChat,
showEmptyIntro, showEmptyIntro,
filterVariableNodes?.length, variableList?.length,
welcomeText welcomeText
] ]
); );
@ -878,12 +861,10 @@ const ChatBox = (
// output data // output data
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getChatHistories: () => chatHistories, restartChat() {
resetVariables,
resetHistory(e) {
abortRequest(); abortRequest();
setValue('chatStarted', e.length > 0); setValue('chatStarted', false);
setChatHistories(e); scrollToBottom('smooth', 500);
}, },
scrollToBottom, scrollToBottom,
sendPrompt: (question: string) => { sendPrompt: (question: string) => {
@ -900,41 +881,33 @@ const ChatBox = (
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}> <Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}> <Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
{showEmpty && <Empty />} {showEmpty && <Empty />}
{!!welcomeText && <WelcomeBox appAvatar={appAvatar} welcomeText={welcomeText} />} {!!welcomeText && <WelcomeBox welcomeText={welcomeText} />}
{/* variable input */} {/* variable input */}
{!!filterVariableNodes?.length && ( {!!variableList?.length && (
<VariableInput <VariableInput chatStarted={chatStarted} chatForm={chatForm} />
appAvatar={appAvatar}
variableList={filterVariableNodes}
chatForm={chatForm}
onSubmitVariables={(data) => {
setValue('chatStarted', true);
onUpdateVariable?.(data);
}}
/>
)} )}
{/* chat history */} {/* chat history */}
<Box id={'history'}> <Box id={'history'}>
{chatHistories.map((item, index) => ( {chatHistories.map((item, index) => (
<Box key={item.dataId} py={5}> <Box key={item.dataId} py={5}>
{item.obj === 'Human' && ( {item.obj === ChatRoleEnum.Human && (
<ChatItem <ChatItem
type={item.obj} type={item.obj}
avatar={item.obj === 'Human' ? userAvatar : appAvatar} avatar={userAvatar}
chat={item} chat={item}
onRetry={retryInput(item.dataId)} onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)} onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1} isLastChild={index === chatHistories.length - 1}
/> />
)} )}
{item.obj === 'AI' && ( {item.obj === ChatRoleEnum.AI && (
<> <>
<ChatItem <ChatItem
type={item.obj} type={item.obj}
avatar={appAvatar} avatar={appAvatar}
chat={item} chat={item}
isLastChild={index === chatHistories.length - 1} isLastChild={index === chatHistories.length - 1}
{...(item.obj === 'AI' && { {...(item.obj === ChatRoleEnum.AI && {
showVoiceIcon, showVoiceIcon,
shareId, shareId,
outLinkUid, outLinkUid,
@ -948,7 +921,7 @@ const ChatBox = (
), ),
onAddUserLike: onAddUserLike(item), onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item), onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onADdUserDislike(item), onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item) onReadUserDislike: onReadUserDislike(item)
})} })}
> >
@ -997,7 +970,7 @@ const ChatBox = (
</Box> </Box>
</Box> </Box>
{/* message input */} {/* message input */}
{onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && appId && ( {onStartChat && chatStarted && active && appId && (
<ChatInput <ChatInput
onSendMessage={sendPrompt} onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')} onStop={() => chatController.current?.abort('stop')}

View File

@ -7,15 +7,6 @@ import {
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export type generatingMessageProps = {
event: SseResponseEventEnum;
text?: string;
name?: string;
status?: 'running' | 'finish';
tool?: ToolModuleResponseItemType;
variables?: Record<string, any>;
};
export type UserInputFileItemType = { export type UserInputFileItemType = {
id: string; id: string;
rawFile?: File; rawFile?: File;
@ -28,7 +19,6 @@ export type UserInputFileItemType = {
export type ChatBoxInputFormType = { export type ChatBoxInputFormType = {
input: string; input: string;
files: UserInputFileItemType[]; files: UserInputFileItemType[];
variables: Record<string, any>;
chatStarted: boolean; chatStarted: boolean;
}; };
@ -37,18 +27,8 @@ export type ChatBoxInputType = {
files?: UserInputFileItemType[]; files?: UserInputFileItemType[];
}; };
export type StartChatFnProps = {
chatList: ChatSiteItemType[];
messages: ChatCompletionMessageParam[];
controller: AbortController;
variables: Record<string, any>;
generatingMessage: (e: generatingMessageProps) => void;
};
export type ComponentRef = { export type ComponentRef = {
getChatHistories: () => ChatSiteItemType[]; restartChat: () => void;
resetVariables: (data?: Record<string, any>) => void;
resetHistory: (history: ChatSiteItemType[]) => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void; scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
sendPrompt: (question: string) => void; sendPrompt: (question: string) => void;
}; };

View File

@ -2,7 +2,11 @@ import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatBoxInputType, UserInputFileItemType } from './type'; import { ChatBoxInputType, UserInputFileItemType } from './type';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
export const formatChatValue2InputType = (value: ChatItemValueItemType[]): ChatBoxInputType => { export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) {
return { text: '', files: [] };
}
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
console.error('value is error', value); console.error('value is error', value);
return { text: '', files: [] }; return { text: '', files: [] };

View File

@ -0,0 +1,69 @@
import React from 'react';
import { Controller } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
const RenderInput = () => {
const { pluginInputs, variablesForm, histories, onStartChat, onNewChat, onSubmit, isChatting } =
useContextSelector(PluginRunContext, (v) => v);
const { t } = useTranslation();
const {
control,
handleSubmit,
formState: { errors }
} = variablesForm;
const isDisabledInput = histories.length > 0;
return (
<>
{pluginInputs.map((input) => {
return (
<Controller
key={input.key}
control={control}
name={input.key}
rules={{ required: input.required }}
render={({ field: { onChange, value } }) => {
return (
<RenderPluginInput
value={value}
onChange={onChange}
label={input.label}
description={input.description}
isDisabled={isDisabledInput}
valueType={input.valueType}
placeholder={input.placeholder}
required={input.required}
min={input.min}
max={input.max}
isInvalid={errors && Object.keys(errors).includes(input.key)}
/>
);
}}
/>
);
})}
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting}
onClick={() => {
if (histories.length > 0) {
return onNewChat();
}
handleSubmit(onSubmit)();
}}
>
{histories.length > 0 ? t('common.Restart') : t('common.Run')}
</Button>
</Flex>
)}
</>
);
};
export default RenderInput;

View File

@ -0,0 +1,59 @@
import { Box } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import Markdown from '@/components/Markdown';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import AIResponseBox from '../../../components/AIResponseBox';
const RenderOutput = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const pluginOutputs = useMemo(() => {
const pluginOutputs = histories?.[1]?.responseData?.find(
(item) => item.moduleType === FlowNodeTypeEnum.pluginOutput
)?.pluginOutput;
return JSON.stringify(pluginOutputs, null, 2);
}, [histories]);
return (
<>
<Box border={'base'} rounded={'md'} bg={'myGray.25'}>
<Box p={4} color={'myGray.900'}>
<Box color={'myGray.900'} fontWeight={'bold'}>
</Box>
{histories.length > 0 && histories[1]?.value.length > 0 ? (
<Box mt={2}>
{histories[1].value.map((value, i) => {
const key = `${histories[1].dataId}-ai-${i}`;
return (
<AIResponseBox
key={key}
value={value}
index={i}
chat={histories[1]}
isLastChild={true}
isChatting={isChatting}
questionGuides={[]}
/>
);
})}
</Box>
) : null}
</Box>
</Box>
<Box border={'base'} mt={4} rounded={'md'} bg={'myGray.25'}>
<Box p={4} color={'myGray.900'} fontWeight={'bold'}>
<Box></Box>
{histories.length > 0 && histories[1].responseData ? (
<Markdown source={`~~~json\n${pluginOutputs}`} />
) : null}
</Box>
</Box>
</>
);
};
export default RenderOutput;

View File

@ -0,0 +1,21 @@
import { ResponseBox } from '../../../components/WholeResponseModal';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import { Box } from '@chakra-ui/react';
const RenderResponseDetail = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const responseData = histories?.[1]?.responseData || [];
return isChatting ? (
<>{'进行中'}</>
) : (
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ResponseBox response={responseData} showDetail={true} />
</Box>
);
};
export default RenderResponseDetail;

View File

@ -0,0 +1,118 @@
import {
Box,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const RenderPluginInput = ({
value,
onChange,
label,
description,
isDisabled,
valueType,
placeholder,
required,
min,
max,
isInvalid
}: {
value: any;
onChange: () => void;
label: string;
description?: string;
isDisabled?: boolean;
valueType: WorkflowIOValueTypeEnum | undefined;
placeholder?: string;
required?: boolean;
min?: number;
max?: number;
isInvalid: boolean;
}) => {
const { t } = useTranslation();
const render = (() => {
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
value={value}
onChange={onChange}
isDisabled={isDisabled}
placeholder={t(placeholder)}
bg={'myGray.50'}
isInvalid={isInvalid}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput
step={1}
min={min}
max={max}
bg={'myGray.50'}
isDisabled={isDisabled}
isInvalid={isInvalid}
>
<NumberInputField value={value} onChange={onChange} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
isChecked={value}
onChange={onChange}
isDisabled={isDisabled}
isInvalid={isInvalid}
/>
);
}
return (
<JsonEditor
bg={'myGray.50'}
placeholder={t(placeholder || '')}
resize
value={value}
onChange={onChange}
isInvalid={isInvalid}
/>
);
})();
return !!render ? (
<Box _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{label}
</Box>
{description && <QuestionTip ml={2} label={description} />}
</Flex>
{render}
</Box>
) : null;
};
export default RenderPluginInput;

View File

@ -0,0 +1,5 @@
export enum PluginRunBoxTabEnum {
input = 'input',
output = 'output',
detail = 'detail'
}

View File

@ -0,0 +1,234 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import { AIChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { FieldValues } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getPluginRunContent } from '@fastgpt/global/core/app/plugin/utils';
type PluginRunContextType = PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: FieldValues) => Promise<any>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
pluginInputs: [],
//@ts-ignore
variablesForm: undefined,
histories: [],
setHistories: function (value: React.SetStateAction<ChatSiteItemType[]>): void {
throw new Error('Function not implemented.');
},
appId: '',
tab: PluginRunBoxTabEnum.input,
setTab: function (value: React.SetStateAction<PluginRunBoxTabEnum>): void {
throw new Error('Function not implemented.');
},
isChatting: false,
onSubmit: function (e: FieldValues): Promise<any> {
throw new Error('Function not implemented.');
}
});
const PluginRunContextProvider = ({
children,
...props
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const { toast } = useToast();
const chatController = useRef(new AbortController());
/* Abort chat completions, questionGuide */
const abortRequest = useCallback(() => {
chatController.current?.abort('stop');
}, []);
const generatingMessage = useCallback(
({ event, text = '', status, name, tool, variables }: generatingMessageProps) => {
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1 || item.obj !== ChatRoleEnum.AI) return item;
const lastValue: AIChatItemValueItemType = JSON.parse(
JSON.stringify(item.value[item.value.length - 1])
);
if (event === SseResponseEventEnum.flowNodeStatus && status) {
return {
...item,
status,
moduleName: name
};
} else if (
(event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) &&
text
) {
if (!lastValue || !lastValue.text) {
const newValue: AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.text,
text: {
content: text
}
};
return {
...item,
value: item.value.concat(newValue)
};
} else {
lastValue.text.content += text;
return {
...item,
value: item.value.slice(0, -1).concat(lastValue)
};
}
} else if (event === SseResponseEventEnum.toolCall && tool) {
const val: AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.tool,
tools: [tool]
};
return {
...item,
value: item.value.concat(val)
};
} else if (
event === SseResponseEventEnum.toolParams &&
tool &&
lastValue.type === ChatItemValueTypeEnum.tool &&
lastValue?.tools
) {
lastValue.tools = lastValue.tools.map((item) => {
if (item.id === tool.id) {
item.params += tool.params;
}
return item;
});
return {
...item,
value: item.value.slice(0, -1).concat(lastValue)
};
} else if (event === SseResponseEventEnum.toolResponse && tool) {
// replace tool response
return {
...item,
value: item.value.map((val) => {
if (val.type === ChatItemValueTypeEnum.tool && val.tools) {
const tools = val.tools.map((item) =>
item.id === tool.id ? { ...item, response: tool.response } : item
);
return {
...val,
tools
};
}
return val;
})
};
}
return item;
})
);
},
[setHistories]
);
const isChatting = useMemo(
() => histories[histories.length - 1] && histories[histories.length - 1]?.status !== 'finish',
[histories]
);
const { runAsync: onSubmit } = useRequest2(async (e: FieldValues) => {
if (!onStartChat) return;
if (isChatting) {
toast({
title: '正在聊天中...请等待结束',
status: 'warning'
});
return;
}
setTab(PluginRunBoxTabEnum.output);
// reset controller
abortRequest();
const abortSignal = new AbortController();
chatController.current = abortSignal;
setHistories([
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
status: 'finish',
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: getPluginRunContent({
pluginInputs
})
}
}
]
},
{
dataId: getNanoid(24),
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
}
],
status: 'loading'
}
]);
try {
const { responseData } = await onStartChat({
messages: [],
controller: chatController.current,
generatingMessage,
variables: e
});
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
} catch (err: any) {
toast({ title: err.message, status: 'error' });
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}
});
const contextValue: PluginRunContextType = {
...props,
isChatting,
onSubmit
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};
export default PluginRunContextProvider;

View File

@ -0,0 +1,30 @@
import React from 'react';
import { PluginRunBoxTabEnum } from './constants';
import { PluginRunBoxProps } from './type';
import RenderInput from './components/RenderInput';
import PluginRunContextProvider, { PluginRunContext } from './context';
import { useContextSelector } from 'use-context-selector';
import RenderOutput from './components/RenderOutput';
import RenderResponseDetail from './components/RenderResponseDetail';
const PluginRunBox = () => {
const { tab } = useContextSelector(PluginRunContext, (v) => v);
return (
<>
{tab === PluginRunBoxTabEnum.input && <RenderInput />}
{tab === PluginRunBoxTabEnum.output && <RenderOutput />}
{tab === PluginRunBoxTabEnum.detail && <RenderResponseDetail />}
</>
);
};
const Render = (props: PluginRunBoxProps) => {
return (
<PluginRunContextProvider {...props}>
<PluginRunBox />
</PluginRunContextProvider>
);
};
export default Render;

View File

@ -0,0 +1,22 @@
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import React from 'react';
import { onStartChatType } from '../type';
export type PluginRunBoxProps = OutLinkChatAuthProps & {
pluginInputs: FlowNodeInputItemType[];
variablesForm: UseFormReturn<FieldValues, any>;
histories: ChatSiteItemType[]; // chatHistories[1] is the response
setHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
onStartChat?: onStartChatType;
onNewChat?: () => void;
appId: string;
chatId?: string;
tab: PluginRunBoxTabEnum;
setTab: React.Dispatch<React.SetStateAction<PluginRunBoxTabEnum>>;
};

View File

@ -0,0 +1,26 @@
import { StreamResponseType } from '@/web/common/api/fetch';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
export type generatingMessageProps = {
event: SseResponseEventEnum;
text?: string;
name?: string;
status?: 'running' | 'finish';
tool?: ToolModuleResponseItemType;
variables?: Record<string, any>;
};
export type StartChatFnProps = {
messages: ChatCompletionMessageParam[];
responseChatItemId?: string;
controller: AbortController;
variables: Record<string, any>;
generatingMessage: (e: generatingMessageProps) => void;
};
export type onStartChatType = (e: StartChatFnProps) => Promise<
StreamResponseType & {
isNewChat?: boolean;
}
>;

View File

@ -0,0 +1,63 @@
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { useCallback, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './PluginRunBox/constants';
import { ComponentRef as ChatComponentRef } from './ChatBox/type';
export const useChat = () => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const [chatRecords, setChatRecords] = useState<ChatSiteItemType[]>([]);
const variablesForm = useForm();
// plugin
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
const resetChatRecords = useCallback(
(props?: { records?: ChatSiteItemType[]; variables?: Record<string, any> }) => {
const { records = [], variables = {} } = props || {};
setChatRecords(records);
// Reset to empty input
const data = variablesForm.getValues();
for (const key in data) {
data[key] = '';
}
variablesForm.reset({
...data,
...variables
});
setTimeout(
() => {
ChatBoxRef.current?.restartChat?.();
},
ChatBoxRef.current?.restartChat ? 0 : 500
);
},
[variablesForm, setChatRecords]
);
const clearChatRecords = useCallback(() => {
setChatRecords([]);
const data = variablesForm.getValues();
for (const key in data) {
variablesForm.setValue(key, '');
}
console.log(ChatBoxRef.current);
ChatBoxRef.current?.restartChat?.();
}, [variablesForm]);
return {
ChatBoxRef,
chatRecords,
setChatRecords,
variablesForm,
pluginRunTab,
setPluginRunTab,
clearChatRecords,
resetChatRecords
};
};

View File

@ -0,0 +1,127 @@
import Markdown, { CodeClassName } from '@/components/Markdown';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box
} from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import {
AIChatItemValueItemType,
ChatSiteItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
index: number;
chat: ChatSiteItemType;
isLastChild: boolean;
isChatting: boolean;
questionGuides: string[];
};
const AIResponseBox = ({ value, index, chat, isLastChild, isChatting, questionGuides }: props) => {
if (value.text) {
let source = (value.text?.content || '').trim();
// First empty line
if (!source && chat.value.length > 1) return null;
// computed question guide
if (
isLastChild &&
!isChatting &&
questionGuides.length > 0 &&
index === chat.value.length - 1
) {
source = `${source}
\`\`\`${CodeClassName.questionGuide}
${JSON.stringify(questionGuides)}`;
}
return (
<Markdown
source={source}
showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return (
<Box>
{value.tools.map((tool) => {
const toolParams = (() => {
try {
return JSON.stringify(JSON.parse(tool.params), null, 2);
} catch (error) {
return tool.params;
}
})();
const toolResponse = (() => {
try {
return JSON.stringify(JSON.parse(tool.response), null, 2);
} catch (error) {
return tool.response;
}
})();
return (
<Accordion key={tool.id} allowToggle>
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1rem'} h={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
px={0}
mt={0}
borderRadius={'md'}
overflow={'hidden'}
maxH={'500px'}
overflowY={'auto'}
>
{toolParams && toolParams !== '{}' && (
<Markdown
source={`~~~json#Input
${toolParams}`}
/>
)}
{toolResponse && (
<Markdown
source={`~~~json#Response
${toolResponse}`}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
);
})}
</Box>
);
}
return null;
};
export default React.memo(AIResponseBox);

View File

@ -0,0 +1,347 @@
import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image, BoxProps } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Markdown from '@/components/Markdown';
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
function RowRender({
children,
mb,
label,
...props
}: { children: React.ReactNode; label: string } & BoxProps) {
return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
}
function Row({
label,
value,
rawDom
}: {
label: string;
value?: string | number | boolean | object;
rawDom?: React.ReactNode;
}) {
const theme = useTheme();
const val = value || rawDom;
const isObject = typeof value === 'object';
const formatValue = useMemo(() => {
if (isObject) {
return `~~~json\n${JSON.stringify(value, null, 2)}`;
}
return `${value}`;
}, [isObject, value]);
if (rawDom) {
return (
<RowRender label={label} mb={1}>
{rawDom}
</RowRender>
);
}
if (val === undefined || val === '' || val === 'undefined') return null;
return (
<RowRender
label={label}
mb={isObject ? 0 : 1}
{...(isObject
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
<Markdown source={formatValue} />
</RowRender>
);
}
const WholeResponseModal = ({
response,
showDetail,
onClose
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isCentered
isOpen={true}
onClose={onClose}
h={['90vh', '80vh']}
minW={['90vw', '600px']}
iconSrc="/imgs/modal/wholeRecord.svg"
title={
<Flex alignItems={'center'}>
{t('core.chat.response.Complete Response')}
<QuestionTip ml={2} label={'从左往右,为各个模块的响应顺序'}></QuestionTip>
</Flex>
}
>
<ResponseBox response={response} showDetail={showDetail} />
</MyModal>
);
};
export default WholeResponseModal;
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
const { workflowT } = useI18n();
const list = useMemo(
() =>
response.map((item, i) => ({
label: (
<Flex alignItems={'center'} justifyContent={'center'} px={2}>
<Image
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
w={['14px', '16px']}
/>
{t(item.moduleName)}
</Flex>
),
value: `${i}`
})),
[response, t]
);
const [currentTab, setCurrentTab] = useState(`0`);
const activeModule = useMemo(() => response[Number(currentTab)], [currentTab, response]);
return (
<Box
{...(hideTabs ? { overflow: 'auto' } : { display: 'flex', flexDirection: 'column' })}
h={'100%'}
>
{!hideTabs && (
<Box>
<LightRowTabs
list={list}
value={currentTab}
inlineStyles={{ pt: 0 }}
onChange={setCurrentTab}
/>
</Box>
)}
{activeModule && (
<Box
py={2}
px={4}
{...(hideTabs
? {}
: {
flex: '1 0 0',
overflow: 'auto'
})}
>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.totalPoints !== undefined && (
<Row
label={t('support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row
label={t('core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={workflowT('response.Error')} value={activeModule?.error} />
</>
{/* ai chat */}
<>
<Row
label={t('core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row label={t('core.chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={theme.borders.base} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row
label={t('core.chat.response.module similarity')}
value={activeModule?.similarity}
/>
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row label={t('core.chat.response.plugin output')} value={activeModule?.pluginOutput} />
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin response detail')}
rawDom={
<ResponseBox response={activeModule.pluginDetail} showDetail={showDetail} />
}
/>
)}
</>
{/* text output */}
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
{/* tool call */}
{activeModule?.toolDetail && activeModule?.toolDetail.length > 0 && (
<Row
label={t('core.chat.response.Tool call response detail')}
rawDom={<ResponseBox response={activeModule.toolDetail} showDetail={showDetail} />}
/>
)}
{/* code */}
<Row label={workflowT('response.Custom outputs')} value={activeModule?.customOutputs} />
<Row label={workflowT('response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={workflowT('response.Code log')} value={activeModule?.codeLog} />
</Box>
)}
</Box>
);
});

View File

@ -1,6 +1,7 @@
import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d'; import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
import { AdminFbkType, ChatItemType } from '@fastgpt/global/core/chat/type'; import { AdminFbkType, ChatItemType } from '@fastgpt/global/core/chat/type';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d'; import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export type GetChatSpeechProps = { export type GetChatSpeechProps = {
ttsConfig: AppTTSConfigType; ttsConfig: AppTTSConfigType;
@ -39,6 +40,8 @@ export type InitChatResponse = {
avatar: string; avatar: string;
intro: string; intro: string;
canUse?: boolean; canUse?: boolean;
type: `${AppTypeEnum}`;
pluginInputs: FlowNodeInputItemType[];
}; };
}; };

View File

@ -1,3 +1,4 @@
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { InitChatResponse } from './api'; import { InitChatResponse } from './api';
export const defaultChatData: InitChatResponse = { export const defaultChatData: InitChatResponse = {
@ -7,7 +8,9 @@ export const defaultChatData: InitChatResponse = {
name: 'Loading', name: 'Loading',
avatar: '/icon/logo.svg', avatar: '/icon/logo.svg',
intro: '', intro: '',
canUse: false canUse: false,
type: AppTypeEnum.simple,
pluginInputs: []
}, },
title: '新对话', title: '新对话',
variables: {}, variables: {},

View File

@ -1,6 +1,7 @@
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
export type PostWorkflowDebugProps = { export type PostWorkflowDebugProps = {
nodes: RuntimeNodeItemType[]; nodes: RuntimeNodeItemType[];

View File

@ -44,6 +44,7 @@ import {
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList'; import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const StandDetailModal = dynamic(() => import('./standardDetailModal')); const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu')); const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
@ -54,7 +55,7 @@ const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccoun
const CommunityModal = dynamic(() => import('@/components/CommunityModal')); const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const Account = () => { const Account = () => {
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { teamPlanStatus } = useUserStore(); const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants; const standardPlan = teamPlanStatus?.standardConstants;
@ -99,7 +100,7 @@ const MyInfo = () => {
const { reset } = useForm<UserUpdateParams>({ const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType defaultValues: userInfo as UserType
}); });
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { toast } = useToast(); const { toast } = useToast();
const { const {

View File

@ -9,12 +9,14 @@ import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading'; import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const InformTable = () => { const InformTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { Loading } = useLoading(); const { Loading } = useLoading();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { const {
data: informs, data: informs,
isLoading, isLoading,

View File

@ -31,6 +31,7 @@ import Avatar from '@/components/Avatar';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import { formatNumber } from '@fastgpt/global/common/math/tools'; import { formatNumber } from '@fastgpt/global/common/math/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const UsageDetail = dynamic(() => import('./UsageDetail')); const UsageDetail = dynamic(() => import('./UsageDetail'));
const UsageTable = () => { const UsageTable = () => {
@ -41,7 +42,7 @@ const UsageTable = () => {
to: new Date() to: new Date()
}); });
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>(''); const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>(); const [usageDetail, setUsageDetail] = useState<UsageItemType>();

View File

@ -12,6 +12,7 @@ import UserInfo from './components/Info';
import { serviceSideProps } from '@/web/common/utils/i18n'; import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Script from 'next/script'; import Script from 'next/script';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Promotion = dynamic(() => import('./components/Promotion')); const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable')); const UsageTable = dynamic(() => import('./components/UsageTable'));
@ -34,7 +35,8 @@ enum TabEnum {
const Account = ({ currentTab }: { currentTab: TabEnum }) => { const Account = ({ currentTab }: { currentTab: TabEnum }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, setUserInfo } = useUserStore(); const { userInfo, setUserInfo } = useUserStore();
const { feConfigs, isPc, systemVersion } = useSystemStore(); const { feConfigs, systemVersion } = useSystemStore();
const { isPc } = useSystem();
const tabList = [ const tabList = [
{ {

View File

@ -14,6 +14,7 @@ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constan
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
export type ListAppBody = { export type ListAppBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
@ -55,8 +56,8 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
const searchMatch = searchKey const searchMatch = searchKey
? { ? {
$or: [ $or: [
{ name: { $regex: searchKey, $options: 'i' } }, { name: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } },
{ intro: { $regex: searchKey, $options: 'i' } } { intro: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } }
] ]
} }
: {}; : {};
@ -65,7 +66,14 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
return { return {
// get all chat app // get all chat app
teamId, teamId,
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple] }, type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.plugin] },
...searchMatch
};
}
if (searchKey) {
return {
teamId,
...searchMatch ...searchMatch
}; };
} }
@ -74,8 +82,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
teamId, teamId,
...(type && Array.isArray(type) && { type: { $in: type } }), ...(type && Array.isArray(type) && { type: { $in: type } }),
...(type && { type }), ...(type && { type }),
...parseParentIdInMongo(parentId), ...parseParentIdInMongo(parentId)
...searchMatch
}; };
})(); })();

View File

@ -1,15 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { sseErrRes } from '@fastgpt/service/common/response'; import { sseErrRes } from '@fastgpt/service/common/response';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { responseWrite } from '@fastgpt/service/common/response'; import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push'; import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { import type { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
ChatItemType,
ChatItemValueItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
@ -18,10 +13,14 @@ import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import { NextAPI } from '@/service/middleware/entry';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
export type Props = { export type Props = {
history: ChatItemType[]; messages: ChatCompletionMessageParam[];
prompt: UserChatItemValueItemType[];
nodes: RuntimeNodeItemType[]; nodes: RuntimeNodeItemType[];
edges: RuntimeEdgeItemType[]; edges: RuntimeEdgeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
@ -29,7 +28,7 @@ export type Props = {
appName: string; appName: string;
}; };
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => { res.on('close', () => {
res.end(); res.end();
}); });
@ -38,26 +37,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end(); res.end();
}); });
let { let { nodes = [], edges = [], messages = [], variables = {}, appName, appId } = req.body as Props;
nodes = [],
edges = [],
history = [],
prompt,
variables = {},
appName,
appId
} = req.body as Props;
try { try {
await connectToDatabase(); // [histories, user]
if (!history || !nodes || !prompt || prompt.length === 0) { const chatMessages = GPTMessages2Chats(messages);
throw new Error('Prams Error'); const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
}
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
/* user auth */ /* user auth */
const [{ app }, { teamId, tmbId }] = await Promise.all([ const [{ app }, { teamId, tmbId }] = await Promise.all([
@ -67,6 +51,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
authToken: true authToken: true
}) })
]); ]);
const isPlugin = app.type === AppTypeEnum.plugin;
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
// Plugin need to replace inputs
if (isPlugin) {
nodes = updatePluginInputByVariables(nodes, variables);
} else {
if (!userInput) {
throw new Error('Params Error');
}
}
// auth balance // auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId); const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
@ -82,8 +83,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
runtimeNodes: nodes, runtimeNodes: nodes,
runtimeEdges: edges, runtimeEdges: edges,
variables, variables,
query: removeEmptyUserInput(prompt), query: removeEmptyUserInput(userInput),
histories: history, histories: chatMessages,
stream: true, stream: true,
detail: true, detail: true,
maxRunTimes: 200 maxRunTimes: 200
@ -117,6 +118,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
} }
export default NextAPI(handler);
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {

View File

@ -11,6 +11,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -53,6 +54,8 @@ async function handler(
}), }),
getAppLatestVersion(app._id, app) getAppLatestVersion(app._id, app)
]); ]);
const pluginInputs =
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ?? [];
return { return {
chatId, chatId,
@ -72,7 +75,9 @@ async function handler(
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,
intro: app.intro intro: app.intro,
type: app.type,
pluginInputs
} }
}; };
} }

View File

@ -15,6 +15,8 @@ import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
@ -47,7 +49,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatId, chatId,
limit: 30, limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback ${ field: `dataId obj value userGoodFeedback userBadFeedback ${
shareChat.responseDetail shareChat.responseDetail || app.type === AppTypeEnum.plugin
? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` ? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
: '' : ''
} ` } `
@ -56,11 +58,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]); ]);
// pick share response field // pick share response field
histories.forEach((item) => { app.type !== AppTypeEnum.plugin &&
if (item.obj === ChatRoleEnum.AI) { histories.forEach((item) => {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); if (item.obj === ChatRoleEnum.AI) {
} item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}); }
});
jsonRes<InitChatResponse>(res, { jsonRes<InitChatResponse>(res, {
data: { data: {
@ -82,7 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,
intro: app.intro intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
} }
} }
}); });

View File

@ -15,6 +15,8 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils'; import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
@ -58,11 +60,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]); ]);
// pick share response field // pick share response field
histories.forEach((item) => { app.type !== AppTypeEnum.plugin &&
if (item.obj === ChatRoleEnum.AI) { histories.forEach((item) => {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); if (item.obj === ChatRoleEnum.AI) {
} item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}); }
});
jsonRes<InitChatResponse>(res, { jsonRes<InitChatResponse>(res, {
data: { data: {
@ -83,7 +86,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,
intro: app.intro intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
} }
} }
}); });

View File

@ -3,7 +3,11 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response'; import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log'; import { addLog } from '@fastgpt/service/common/system/log';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatSourceEnum
} from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d'; import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';
@ -28,10 +32,10 @@ import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { import {
concatHistories, concatHistories,
filterPublicNodeResponseData, filterPublicNodeResponseData,
getChatTitleFromChatMessage,
removeEmptyUserInput removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils'; } from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools'; import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { connectToDatabase } from '@/service/mongo';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team'; import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
@ -49,9 +53,17 @@ import { setEntryEntries } from '@fastgpt/service/core/workflow/dispatchV1/utils
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import {
getPluginInputsFromStoreNodes,
getPluginRunContent
} from '@fastgpt/global/core/app/plugin/utils';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
type FastGptWebChatProps = { type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
appId?: string; appId?: string;
}; };
@ -59,14 +71,11 @@ export type Props = ChatCompletionCreateParams &
FastGptWebChatProps & FastGptWebChatProps &
OutLinkChatAuthProps & { OutLinkChatAuthProps & {
messages: ChatCompletionMessageParam[]; messages: ChatCompletionMessageParam[];
responseChatItemId?: string;
stream?: boolean; stream?: boolean;
detail?: boolean; detail?: boolean;
variables: Record<string, any>; variables: Record<string, any>; // Global variables or plugin inputs
}; };
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
type AuthResponseType = { type AuthResponseType = {
teamId: string; teamId: string;
@ -89,7 +98,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res.end(); res.end();
}); });
const { let {
chatId, chatId,
appId, appId,
// share chat // share chat
@ -98,41 +107,39 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// team chat // team chat
teamId: spaceTeamId, teamId: spaceTeamId,
teamToken, teamToken,
stream = false, stream = false,
detail = false, detail = false,
messages = [], messages = [],
variables = {} variables = {},
responseChatItemId = getNanoid()
} = req.body as Props; } = req.body as Props;
try {
const originIp = requestIp.getClientIp(req);
await connectToDatabase(); const originIp = requestIp.getClientIp(req);
// body data check
if (!messages) { const startTime = Date.now();
throw new Error('Prams Error');
} try {
if (!Array.isArray(messages)) { if (!Array.isArray(messages)) {
throw new Error('messages is not array'); throw new Error('messages is not array');
} }
if (messages.length === 0) {
throw new Error('messages is empty');
}
let startTime = Date.now(); /*
Web params: chatId + [Human]
// Web chat params: [Human, AI] API params: chatId + [Human]
API params: [histories, Human]
*/
const chatMessages = GPTMessages2Chats(messages); const chatMessages = GPTMessages2Chats(messages);
if (chatMessages[chatMessages.length - 1].obj !== ChatRoleEnum.Human) {
chatMessages.pop();
}
// user question // Computed start hook params
const question = chatMessages.pop() as UserChatItemType; const startHookText = (() => {
if (!question) { // Chat
throw new Error('Question is empty'); const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType | undefined;
} if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text;
const { text, files } = chatValue2RuntimePrompt(question.value); // plugin
return JSON.stringify(variables);
})();
/* /*
1. auth app permission 1. auth app permission
@ -149,7 +156,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
outLinkUid, outLinkUid,
chatId, chatId,
ip: originIp, ip: originIp,
question: text question: startHookText
}); });
} }
// team space chat // team space chat
@ -169,8 +176,45 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId chatId
}); });
})(); })();
const isPlugin = app.type === AppTypeEnum.plugin;
// 1. get and concat history; 2. get app workflow // Check message type
if (isPlugin) {
detail = true;
} else {
if (messages.length === 0) {
throw new Error('messages is empty');
}
}
// Get obj=Human history
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
return {
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: getPluginRunContent({
pluginInputs: getPluginInputsFromStoreNodes(app.modules)
})
}
}
]
};
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;
if (!latestHumanChat) {
throw new Error('User question is empty');
}
return latestHumanChat;
})();
const { text, files } = chatValue2RuntimePrompt(userQuestion.value);
// Get and concat history;
const limit = getMaxHistoryLimitFromNodes(app.modules); const limit = getMaxHistoryLimitFromNodes(app.modules);
const [{ histories }, { nodes, edges, chatConfig }] = await Promise.all([ const [{ histories }, { nodes, edges, chatConfig }] = await Promise.all([
getChatItems({ getChatItems({
@ -182,7 +226,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
getAppLatestVersion(app._id, app) getAppLatestVersion(app._id, app)
]); ]);
const newHistories = concatHistories(histories, chatMessages); const newHistories = concatHistories(histories, chatMessages);
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
// Get runtimeNodes
const runtimeNodes = isPlugin
? updatePluginInputByVariables(
storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
variables
)
: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes));
/* start flow controller */ /* start flow controller */
const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => { const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => {
@ -196,10 +247,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
app, app,
chatId, chatId,
responseChatItemId, responseChatItemId,
runtimeNodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)), runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges), runtimeEdges: initWorkflowEdgeStatus(edges),
variables, variables,
query: removeEmptyUserInput(question.value), query: removeEmptyUserInput(userQuestion.value),
histories: newHistories, histories: newHistories,
stream, stream,
detail, detail,
@ -245,6 +296,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return ChatSourceEnum.online; return ChatSourceEnum.online;
})(); })();
const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone)
: getChatTitleFromChatMessage(userQuestion);
await saveChat({ await saveChat({
chatId, chatId,
appId: app._id, appId: app._id,
@ -254,11 +309,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
newTitle,
shareId, shareId,
outLinkUid: outLinkUserId, outLinkUid: outLinkUserId,
source, source,
content: [ content: [
question, userQuestion,
{ {
dataId: responseChatItemId, dataId: responseChatItemId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
@ -295,7 +351,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}); });
if (detail) { if (detail) {
if (responseDetail) { if (responseDetail || isPlugin) {
responseWrite({ responseWrite({
res, res,
event: SseResponseEventEnum.flowResponses, event: SseResponseEventEnum.flowResponses,
@ -312,6 +368,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return assistantResponses[0].text?.content; return assistantResponses[0].text?.content;
return assistantResponses; return assistantResponses;
})(); })();
res.json({ res.json({
...(detail ? { responseData: feResponseData, newVariables } : {}), ...(detail ? { responseData: feResponseData, newVariables } : {}),
id: chatId || '', id: chatId || '',

View File

@ -0,0 +1,198 @@
import React from 'react';
import { Flex, Box, useTheme } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
import { getInitChatInfo } from '@/web/core/chat/api';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useChat } from '@/components/core/chat/ChatContainer/useChat';
import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useQuery } from '@tanstack/react-query';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const DetailLogsModal = ({
appId,
chatId,
onClose
}: {
appId: string;
chatId: string;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const theme = useTheme();
const {
ChatBoxRef,
chatRecords,
setChatRecords,
variablesForm,
pluginRunTab,
setPluginRunTab,
resetChatRecords
} = useChat();
const { data: chat, isFetching } = useQuery(
['getChatDetail', chatId],
() => getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true }),
{
onSuccess(res) {
const history = res.history.map((item) => ({
...item,
dataId: item.dataId || getNanoid(),
status: 'finish' as any
}));
resetChatRecords({
records: history,
variables: res.variables
});
}
}
);
const title = chat?.title;
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.plugin;
return (
<>
<MyBox
isLoading={isFetching}
display={'flex'}
flexDirection={'column'}
zIndex={3}
position={['fixed', 'absolute']}
top={[0, '2%']}
right={0}
h={['100%', '96%']}
w={'100%'}
maxW={['100%', '600px']}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
{/* Header */}
{isPlugin ? (
<Flex
alignItems={'flex-start'}
justifyContent={'space-between'}
px={3}
pt={3}
bg={'myGray.25'}
borderBottom={'base'}
>
<LightRowTabs<PluginRunBoxTabEnum>
list={[
{ label: t('common.Input'), value: PluginRunBoxTabEnum.input },
...(chatRecords.length > 0
? [
{ label: t('common.Output'), value: PluginRunBoxTabEnum.output },
{ label: '完整结果', value: PluginRunBoxTabEnum.detail }
]
: [])
]}
value={pluginRunTab}
onChange={setPluginRunTab}
inlineStyles={{ px: 0.5, pb: 2 }}
gap={5}
py={0}
fontSize={'sm'}
/>
<CloseIcon onClick={onClose} />
</Flex>
) : (
<Flex
alignItems={'center'}
px={[3, 5]}
h={['46px', '60px']}
borderBottom={theme.borders.base}
borderBottomColor={'gray.200'}
color={'myGray.900'}
>
{isPc ? (
<>
<Box mr={3} color={'myGray.1000'}>
{title}
</Box>
{chatRecords.length > 0 && (
<>
<MyTag colorSchema="blue">
<MyIcon name={'history'} w={'14px'} />
<Box ml={1}>{`${chatRecords.length}条记录`}</Box>
</MyTag>
{!!chatModels && (
<MyTag ml={2} colorSchema={'green'}>
<MyIcon name={'core/chat/chatModelTag'} w={'14px'} />
<Box ml={1}>{chatModels.join(',')}</Box>
</MyTag>
)}
</>
)}
<Box flex={1} />
</>
) : (
<>
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
<Box ml={1} className="textEllipsis">
{title}
</Box>
</Flex>
</>
)}
<CloseIcon onClick={onClose} />
</Flex>
)}
{/* Chat container */}
<Box pt={2} flex={'1 0 0'}>
{isPlugin ? (
<Box px={5} pt={2} h={'100%'}>
<PluginRunBox
pluginInputs={chat?.app.pluginInputs}
variablesForm={variablesForm}
histories={chatRecords}
setHistories={setChatRecords}
appId={chat.appId}
tab={pluginRunTab}
setTab={setPluginRunTab}
/>
</Box>
) : (
<ChatBox
ref={ChatBoxRef}
chatHistories={chatRecords}
setChatHistories={setChatRecords}
variablesForm={variablesForm}
appAvatar={chat?.app.avatar}
userAvatar={HUMAN_ICON}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
/>
)}
</Box>
</MyBox>
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
</>
);
};
export default DetailLogsModal;

View File

@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState } from 'react'; import React, { useState } from 'react';
import { import {
Flex, Flex,
Box, Box,
@ -9,7 +9,6 @@ import {
Th, Th,
Td, Td,
Tbody, Tbody,
useTheme,
useDisclosure, useDisclosure,
ModalBody, ModalBody,
HStack HStack
@ -19,31 +18,26 @@ import { useTranslation } from 'next-i18next';
import { getAppChatLogs } from '@/web/core/app/api'; import { getAppChatLogs } from '@/web/core/app/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ChatSourceMap } from '@fastgpt/global/core/chat/constants'; import { ChatSourceMap } from '@fastgpt/global/core/chat/constants';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
import { AppLogsListItemType } from '@/types/app'; import { AppLogsListItemType } from '@/types/app';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef } from '@/components/ChatBox/type.d';
import { useQuery } from '@tanstack/react-query';
import { getInitChatInfo } from '@/web/core/chat/api';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { usePagination } from '@fastgpt/web/hooks/usePagination'; import { usePagination } from '@fastgpt/web/hooks/usePagination';
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import { formatChatValue2InputType } from '@/components/ChatBox/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context'; import { AppContext } from '../context';
import { cardStyles } from '../constants'; import { cardStyles } from '../constants';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
const Logs = () => { const Logs = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const appId = useContextSelector(AppContext, (v) => v.appId); const appId = useContextSelector(AppContext, (v) => v.appId);
@ -225,131 +219,3 @@ const Logs = () => {
}; };
export default React.memo(Logs); export default React.memo(Logs);
const DetailLogsModal = ({
appId,
chatId,
onClose
}: {
appId: string;
chatId: string;
onClose: () => void;
}) => {
const ChatBoxRef = useRef<ComponentRef>(null);
const { isPc } = useSystemStore();
const theme = useTheme();
const { data: chat, isFetching } = useQuery(
['getChatDetail', chatId],
() => getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true }),
{
onSuccess(res) {
const history = res.history.map((item) => ({
...item,
dataId: item.dataId || getNanoid(),
status: 'finish' as any
}));
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
}
}
);
const history = useMemo(() => (chat?.history ? chat.history : []), [chat]);
const title = useMemo(() => {
const { text } = formatChatValue2InputType(history[history.length - 2]?.value);
return text?.slice(0, 8);
}, [history]);
const chatModels = chat?.app?.chatModels;
return (
<>
<MyBox
isLoading={isFetching}
display={'flex'}
flexDirection={'column'}
zIndex={3}
position={['fixed', 'absolute']}
top={[0, '2%']}
right={0}
h={['100%', '96%']}
w={'100%'}
maxW={['100%', '600px']}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
<Flex
alignItems={'center'}
px={[3, 5]}
h={['46px', '60px']}
borderBottom={theme.borders.base}
borderBottomColor={'gray.200'}
color={'myGray.900'}
>
{isPc ? (
<>
<Box mr={3} color={'myGray.1000'}>
{title}
</Box>
<MyTag colorSchema="blue">
<MyIcon name={'history'} w={'14px'} />
<Box ml={1}>{`${history.length}条记录`}</Box>
</MyTag>
{!!chatModels && chatModels.length > 0 && (
<MyTag ml={2} colorSchema={'green'}>
<MyIcon name={'core/chat/chatModelTag'} w={'14px'} />
<Box ml={1}>{chatModels.join(',')}</Box>
</MyTag>
)}
<Box flex={1} />
</>
) : (
<>
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
<Box ml={1} className="textEllipsis">
{title}
</Box>
</Flex>
</>
)}
<Flex
alignItems={'center'}
justifyContent={'center'}
w={'20px'}
h={'20px'}
borderRadius={'50%'}
cursor={'pointer'}
_hover={{ bg: 'myGray.100' }}
onClick={onClose}
>
<MyIcon name={'common/closeLight'} w={'12px'} h={'12px'} color={'myGray.700'} />
</Flex>
</Flex>
<Box pt={2} flex={'1 0 0'}>
<ChatBox
ref={ChatBoxRef}
appAvatar={chat?.app.avatar}
userAvatar={HUMAN_ICON}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
/>
</Box>
</MyBox>
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
</>
);
};

View File

@ -14,11 +14,15 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard'; import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import RouteTab from '../RouteTab';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider')); const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => { const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { isPc } = useSystem();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v); const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2'; const isV2Workflow = appDetail?.version === 'v2';
@ -27,6 +31,7 @@ const Header = () => {
flowData2StoreDataAndCheck, flowData2StoreDataAndCheck,
onSaveWorkflow, onSaveWorkflow,
setHistoriesDefaultData, setHistoriesDefaultData,
setWorkflowTestData,
historiesDefaultData, historiesDefaultData,
initData initData
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
@ -62,11 +67,11 @@ const Header = () => {
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (
<> <>
{/* {!isPc && ( {!isPc && (
<Flex pt={2} justifyContent={'center'}> <Flex pt={2} justifyContent={'center'}>
<RouteTab /> <RouteTab />
</Flex> </Flex>
)} */} )}
<Flex <Flex
mt={[2, 0]} mt={[2, 0]}
py={3} py={3}
@ -101,18 +106,18 @@ const Header = () => {
/> />
</Box> </Box>
{/* {isPc && ( {isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}> <Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab /> <RouteTab />
</Box> </Box>
)} */} )}
<Box flex={1} /> <Box flex={1} />
{currentTab === TabEnum.appEdit && ( {currentTab === TabEnum.appEdit && (
<> <>
{!historiesDefaultData && ( {!historiesDefaultData && (
<IconButton <IconButton
// mr={[2, 4]} mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />} icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''} aria-label={''}
size={'sm'} size={'sm'}
@ -129,7 +134,7 @@ const Header = () => {
}} }}
/> />
)} )}
{/* <Button <Button
size={'sm'} size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />} leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'} variant={'whitePrimary'}
@ -141,7 +146,7 @@ const Header = () => {
}} }}
> >
{t('core.workflow.Debug')} {t('core.workflow.Debug')}
</Button> */} </Button>
{!historiesDefaultData && ( {!historiesDefaultData && (
<PopoverConfirm <PopoverConfirm
@ -176,12 +181,15 @@ const Header = () => {
}, [ }, [
appDetail.chatConfig, appDetail.chatConfig,
currentTab, currentTab,
flowData2StoreDataAndCheck,
historiesDefaultData, historiesDefaultData,
initData, initData,
isPc,
isV2Workflow, isV2Workflow,
onclickPublish, onclickPublish,
saveAndBack, saveAndBack,
setHistoriesDefaultData, setHistoriesDefaultData,
setWorkflowTestData,
t t
]); ]);

View File

@ -27,7 +27,7 @@ const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
setWorkflowData({ nodes, edges }); setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData]); }, [appForm, setWorkflowData]);
const { resetChatBox, ChatBox } = useChatTest({ const { restartChat, ChatContainer } = useChatTest({
...workflowData, ...workflowData,
chatConfig: appForm.chatConfig chatConfig: appForm.chatConfig
}); });
@ -48,13 +48,13 @@ const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
aria-label={'delete'} aria-label={'delete'}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
resetChatBox(); restartChat();
}} }}
/> />
</MyTooltip> </MyTooltip>
</Flex> </Flex>
<Box flex={1}> <Box flex={1}>
<ChatBox /> <ChatContainer />
</Box> </Box>
</Flex> </Flex>
); );

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset'; import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils'; import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
@ -15,6 +14,7 @@ import { useContextSelector } from 'use-context-selector';
import { cardStyles } from '../constants'; import { cardStyles } from '../constants';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Edit = ({ const Edit = ({
appForm, appForm,
@ -23,7 +23,7 @@ const Edit = ({
appForm: AppSimpleEditFormType; appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>; setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => { }) => {
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { loadAllDatasets } = useDatasetStore(); const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@ -257,8 +257,8 @@ const EditForm = ({
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}> <Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} /> <MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('core.app.Tool call')}()</FormLabel> <FormLabel ml={2}>{appT('Plugin dispatch')}</FormLabel>
<QuestionTip ml={1} label={t('core.app.Tool call tip')} /> <QuestionTip ml={1} label={appT('Plugin dispatch tip')} />
</Flex> </Flex>
<Button <Button
variant={'transparentBase'} variant={'transparentBase'}
@ -315,7 +315,13 @@ const EditForm = ({
<VariableEdit <VariableEdit
variables={appForm.chatConfig.variables} variables={appForm.chatConfig.variables}
onChange={(e) => { onChange={(e) => {
appForm.chatConfig.variables = e; setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
variables: e
}
}));
}} }}
/> />
</Box> </Box>

View File

@ -21,6 +21,7 @@ import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants'; import { publishStatusStyle } from '../constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Header = ({ const Header = ({
appForm, appForm,
@ -30,7 +31,7 @@ const Header = ({
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>; setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const router = useRouter(); const router = useRouter();
const { appId, appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v); const { appId, appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);

View File

@ -17,11 +17,12 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard'; import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider')); const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => { const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const router = useRouter(); const router = useRouter();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v); const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);

View File

@ -1,7 +1,7 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import React, { forwardRef, ForwardedRef } from 'react'; import React from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons'; import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react'; import { Box, Flex, HStack, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@ -10,29 +10,27 @@ import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { useChatTest } from '@/pages/app/detail/components/useChatTest'; import { useChatTest } from '@/pages/app/detail/components/useChatTest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
export type ChatTestComponentRef = { const ChatTest = ({
resetChatTest: () => void; isOpen,
}; nodes = [],
edges = [],
const ChatTest = ( onClose
{ }: {
isOpen, isOpen: boolean;
nodes = [], nodes?: StoreNodeItemType[];
edges = [], edges?: StoreEdgeItemType[];
onClose onClose: () => void;
}: { }) => {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const isPlugin = appDetail.type === AppTypeEnum.plugin;
const { resetChatBox, ChatBox } = useChatTest({ const { restartChat, ChatContainer, pluginRunTab, setPluginRunTab, chatRecords } = useChatTest({
nodes, nodes,
edges, edges,
chatConfig: appDetail.chatConfig chatConfig: appDetail.chatConfig
@ -64,40 +62,76 @@ const ChatTest = (
overflow={'hidden'} overflow={'hidden'}
transition={'.2s ease'} transition={'.2s ease'}
> >
<Flex py={4} px={5} whiteSpace={'nowrap'}> {isPlugin ? (
<Box fontSize={'lg'} fontWeight={'bold'} flex={1}> <Flex
{t('core.chat.Debug test')} alignItems={'flex-start'}
</Box> justifyContent={'space-between'}
<MyTooltip label={t('core.chat.Restart')}> px={3}
<IconButton pt={3}
className="chat" bg={'myGray.25'}
size={'smSquare'} borderBottom={'base'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />} >
variant={'whiteDanger'} <LightRowTabs<PluginRunBoxTabEnum>
borderRadius={'md'} list={[
aria-label={'delete'} { label: t('common.Input'), value: PluginRunBoxTabEnum.input },
onClick={(e) => { ...(chatRecords.length > 0
resetChatBox(); ? [
}} { label: t('common.Output'), value: PluginRunBoxTabEnum.output },
{ label: '完整结果', value: PluginRunBoxTabEnum.detail }
]
: [])
]}
value={pluginRunTab}
onChange={setPluginRunTab}
inlineStyles={{ px: 0.5, pb: 2 }}
gap={5}
py={0}
fontSize={'sm'}
/> />
</MyTooltip>
<MyTooltip label={t('common.Close')}> <CloseIcon mt={1} onClick={onClose} />
<IconButton </Flex>
ml={3} ) : (
icon={<SmallCloseIcon fontSize={'22px'} />} <Flex
variant={'grayBase'} py={4}
size={'smSquare'} px={5}
aria-label={''} whiteSpace={'nowrap'}
onClick={onClose} bg={isPlugin ? 'myGray.25' : ''}
/> borderBottom={isPlugin ? '1px solid #F4F4F7' : ''}
</MyTooltip> >
</Flex> <Box fontSize={'lg'} fontWeight={'bold'} flex={1}>
<Box flex={1}> {t('core.chat.Debug test')}
<ChatBox /> </Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={restartChat}
/>
</MyTooltip>
<MyTooltip label={t('common.Close')}>
<IconButton
ml={4}
icon={<SmallCloseIcon fontSize={'22px'} />}
variant={'grayBase'}
size={'smSquare'}
aria-label={''}
onClick={onClose}
/>
</MyTooltip>
</Flex>
)}
<Box flex={'1 0 0'} overflow={'auto'}>
<ChatContainer />
</Box> </Box>
</Flex> </Flex>
</> </>
); );
}; };
export default React.memo(forwardRef(ChatTest)); export default React.memo(ChatTest);

View File

@ -32,6 +32,7 @@ import { getAppFolderPath } from '@/web/core/app/api/app';
import { useWorkflowUtils } from './hooks/useUtils'; import { useWorkflowUtils } from './hooks/useUtils';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants'; import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
type ModuleTemplateListProps = { type ModuleTemplateListProps = {
isOpen: boolean; isOpen: boolean;
@ -268,7 +269,7 @@ const RenderList = React.memo(function RenderList({
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const { isPc } = useSystemStore(); const { isPc } = useSystem();
const { x, y, zoom } = useViewport(); const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore(); const { setLoading } = useSystemStore();
const { toast } = useToast(); const { toast } = useToast();

View File

@ -138,7 +138,7 @@ export const useDebug = () => {
setRuntimeEdges(undefined); setRuntimeEdges(undefined);
}; };
const onclickRun = (data: Record<string, any>) => { const onClickRun = (data: Record<string, any>) => {
onStartNodeDebug({ onStartNodeDebug({
entryNodeId: runtimeNode.nodeId, entryNodeId: runtimeNode.nodeId,
runtimeNodes: runtimeNodes.map((node) => runtimeNodes: runtimeNodes.map((node) =>
@ -261,7 +261,7 @@ export const useDebug = () => {
})} })}
</Box> </Box>
<Flex py={2} justifyContent={'flex-end'} px={6}> <Flex py={2} justifyContent={'flex-end'} px={6}>
<Button onClick={handleSubmit(onclickRun)}></Button> <Button onClick={handleSubmit(onClickRun)}></Button>
</Flex> </Flex>
</MyRightDrawer> </MyRightDrawer>
); );

View File

@ -13,7 +13,6 @@ import { ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea'; import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle'; import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug'; import { useDebug } from '../../hooks/useDebug';
import { ResponseBox } from '@/components/ChatBox/components/WholeResponseModal';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginNode } from '@/web/core/app/api/plugin'; import { getPreviewPluginNode } from '@/web/core/app/api/plugin';
import { storeNode2FlowNode, getLatestNodeTemplate } from '@/web/core/workflow/utils'; import { storeNode2FlowNode, getLatestNodeTemplate } from '@/web/core/workflow/utils';
@ -26,6 +25,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils'; import { useWorkflowUtils } from '../../hooks/useUtils';
import { ResponseBox } from '@/components/core/chat/components/WholeResponseModal';
type Props = FlowNodeItemType & { type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string; children?: React.ReactNode | React.ReactNode[] | string;
@ -566,11 +566,10 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
w={'420px'} w={'420px'}
maxH={'100%'} maxH={'100%'}
minH={'300px'} minH={'300px'}
overflowY={'auto'}
border={'base'} border={'base'}
> >
{/* Status header */} {/* Status header */}
<Flex px={4} mb={1} py={3} alignItems={'center'} borderBottom={'base'}> <Flex h={'54x'} px={4} mb={1} py={3} alignItems={'center'} borderBottom={'base'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} /> <MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}> <Box fontWeight={'bold'} flex={'1'}>
{t('core.workflow.debug.Run result')} {t('core.workflow.debug.Run result')}
@ -606,7 +605,7 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
)} )}
</Flex> </Flex>
{/* Show result */} {/* Show result */}
<Box maxH={'100%'} overflow={'auto'}> <Box maxH={'calc(100%-54px)'} overflow={'auto'}>
{!debugResult.message && !response && ( {!debugResult.message && !response && (
<EmptyTip text={t('core.workflow.debug.Not result')} pt={2} pb={5} /> <EmptyTip text={t('core.workflow.debug.Not result')} pt={2} pb={5} />
)} )}

View File

@ -44,7 +44,7 @@ import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils'; import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import ChatTest, { type ChatTestComponentRef } from './Flow/ChatTest'; import ChatTest from './Flow/ChatTest';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { uiWorkflow2StoreWorkflow } from './utils'; import { uiWorkflow2StoreWorkflow } from './utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@ -750,7 +750,6 @@ const WorkflowContextProvider = ({
}, [edges, nodes]); }, [edges, nodes]);
/* chat test */ /* chat test */
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure(); const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
const [workflowTestData, setWorkflowTestData] = useState<{ const [workflowTestData, setWorkflowTestData] = useState<{
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
@ -813,7 +812,7 @@ const WorkflowContextProvider = ({
return ( return (
<WorkflowContext.Provider value={value}> <WorkflowContext.Provider value={value}>
{children} {children}
<ChatTest ref={ChatTestRef} isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} /> <ChatTest isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
</WorkflowContext.Provider> </WorkflowContext.Provider>
); );
}; };

View File

@ -1,7 +1,6 @@
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useCallback, useRef } from 'react'; import React from 'react';
import ChatBox from '@/components/ChatBox'; import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { streamFetch } from '@/web/common/api/fetch'; import { streamFetch } from '@/web/common/api/fetch';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils'; import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import { import {
@ -16,6 +15,14 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext } from './context'; import { AppContext } from './context';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import { useChat } from '@/components/core/chat/ChatContainer/useChat';
import { Box } from '@chakra-ui/react';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
export const useChatTest = ({ export const useChatTest = ({
nodes, nodes,
@ -27,22 +34,19 @@ export const useChatTest = ({
chatConfig: AppChatConfigType; chatConfig: AppChatConfigType;
}) => { }) => {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const startChat = useMemoizedFn( const startChat = useMemoizedFn(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => { async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
/* get histories */ /* get histories */
let historyMaxLen = getMaxHistoryLimitFromNodes(nodes); const historyMaxLen = getMaxHistoryLimitFromNodes(nodes);
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据 // 流请求,获取数据
const { responseText, responseData } = await streamFetch({ const { responseText, responseData } = await streamFetch({
url: '/api/core/chat/chatTest', url: '/api/core/chat/chatTest',
data: { data: {
history, // Send histories and user messages
prompt: chatList[chatList.length - 2].value, messages: messages.slice(-historyMaxLen - 2),
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)), nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges), edges: initWorkflowEdgeStatus(edges),
variables, variables,
@ -57,28 +61,57 @@ export const useChatTest = ({
} }
); );
const resetChatBox = useCallback(() => { const pluginInputs =
ChatBoxRef.current?.resetHistory([]); nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || [];
ChatBoxRef.current?.resetVariables(); const {
}, []); ChatBoxRef,
chatRecords,
setChatRecords,
variablesForm,
pluginRunTab,
setPluginRunTab,
clearChatRecords
} = useChat();
const CustomChatBox = useMemoizedFn(() => ( const CustomChatContainer = useMemoizedFn(() =>
<ChatBox appDetail.type === AppTypeEnum.plugin ? (
ref={ChatBoxRef} <Box h={'100%'} p={3}>
appId={appDetail._id} <PluginRunBox
appAvatar={appDetail.avatar} pluginInputs={pluginInputs}
userAvatar={userInfo?.avatar} variablesForm={variablesForm}
showMarkIcon histories={chatRecords}
chatConfig={chatConfig} setHistories={setChatRecords}
showFileSelector={checkChatSupportSelectFileByModules(nodes)} appId={appDetail._id}
onStartChat={startChat} tab={pluginRunTab}
onDelMessage={() => {}} setTab={setPluginRunTab}
/> onNewChat={clearChatRecords}
)); onStartChat={startChat}
/>
</Box>
) : (
<ChatBox
ref={ChatBoxRef}
chatHistories={chatRecords}
setChatHistories={setChatRecords}
variablesForm={variablesForm}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatConfig={chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
)
);
return { return {
resetChatBox, restartChat: clearChatRecords,
ChatBox: CustomChatBox ChatContainer: CustomChatContainer,
chatRecords,
pluginRunTab,
setPluginRunTab
}; };
}; };

Some files were not shown because too many files have changed in this diff Show More