Perf input guide (#1557)

* perf: input guide code

* perf: input guide ui

* Chat input guide api

* Update app chat config store

* perf: app chat config field

* perf: app context

* perf: params

* fix: ts

* perf: filter private config

* perf: filter private config

* perf: import workflow

* perf: limit max tip amount
This commit is contained in:
Archer 2024-05-21 17:52:04 +08:00 committed by GitHub
parent 8e8ceb7439
commit fb368a581c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 2124 additions and 1805 deletions

View File

@ -25,6 +25,7 @@ usageMatchRegex:
- "[^\\w\\d]publishT\\(['\"`]({key})['\"`]" - "[^\\w\\d]publishT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]workflowT\\(['\"`]({key})['\"`]" - "[^\\w\\d]workflowT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]userT\\(['\"`]({key})['\"`]" - "[^\\w\\d]userT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]chatT\\(['\"`]({key})['\"`]"
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys # A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
# and works like how the i18next framework identifies the namespace scope from the # and works like how the i18next framework identifies the namespace scope from the

View File

@ -0,0 +1,48 @@
---
title: "对话问题引导"
description: "FastGPT 对话问题引导"
icon: "code"
draft: false
toc: true
weight: 350
---
![](/imgs/questionGuide.png)
## 什么是自定义问题引导
你可以为你的应用提前预设一些问题,用户在输入时,会根据输入的内容,动态搜索这些问题作为提示,从而引导用户更快的进行提问。
你可以直接在 FastGPT 中配置词库,或者提供自定义词库接口。
## 自定义词库接口
**请求:**
```bash
curl --location --request GET 'http://localhost:3000/api/core/chat/inputGuide/query?appId=663c75302caf8315b1c00194&searchKey=你'
```
**响应**
```json
{
"code": 200,
"statusText": "",
"message": "",
"data": [
"是你",
"你是谁呀",
"你好好呀",
"你好呀",
"你是谁!",
"你好"
]
}
```
**参数说明:**
- appId - 应用ID
- searchKey - 搜索关键字

View File

@ -1,43 +0,0 @@
---
title: "自定义词库地址"
description: "FastGPT 自定义输入提示的接口地址"
icon: "code"
draft: false
toc: true
weight: 350
---
![](/imgs/questionGuide.png)
## 什么是输入提示
可自定义开启或关闭,当输入提示开启,并且词库中存在数据时,用户在输入问题时如果输入命中词库,那么会在输入框上方展示对应的智能推荐数据
用户可配置词库,选择存储在 FastGPT 数据库中,或者提供自定义接口获取词库
## 数据格式
词库的形式为一个字符串数组,定义的词库接口应该有两种方法 —— GET & POST
### GET
对于 GET 方法用于获取词库数据FastGPT 会给接口发送数据 query 为
```
{
appId: 'xxxx'
}
```
返回数据格式应当为
```
{
data: ['xxx', 'xxxx']
}
```
### POST
对于 POST 方法用于更新词库数据FastGPT 会给接口发送数据 body 为
```
{
appId: 'xxxx',
text: ['xxx', 'xxxx']
}
```
接口应当按照获取的数据格式存储相对应的词库数组

View File

@ -1,4 +1,4 @@
import { AppWhisperConfigType } from './type'; import { AppTTSConfigType, AppWhisperConfigType } from './type';
export enum AppTypeEnum { export enum AppTypeEnum {
simple = 'simple', simple = 'simple',
@ -13,14 +13,16 @@ export const AppTypeMap = {
} }
}; };
export const defaultTTSConfig: AppTTSConfigType = { type: 'web' };
export const defaultWhisperConfig: AppWhisperConfigType = { export const defaultWhisperConfig: AppWhisperConfigType = {
open: false, open: false,
autoSend: false, autoSend: false,
autoTTSResponse: false autoTTSResponse: false
}; };
export const defaultQuestionGuideTextConfig = { export const defaultChatInputGuideConfig = {
open: false, open: false,
textList: [], textList: [],
customURL: '' customUrl: ''
}; };

View File

@ -8,7 +8,7 @@ import { DatasetSearchModeEnum } from '../dataset/constants';
import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d';
import { StoreEdgeItemType } from '../workflow/type/edge'; import { StoreEdgeItemType } from '../workflow/type/edge';
export interface AppSchema { export type AppSchema = {
_id: string; _id: string;
teamId: string; teamId: string;
tmbId: string; tmbId: string;
@ -23,13 +23,14 @@ export interface AppSchema {
edges: StoreEdgeItemType[]; edges: StoreEdgeItemType[];
// App system config // App system config
chatConfig: AppChatConfigType;
scheduledTriggerConfig?: AppScheduledTriggerConfigType | null; scheduledTriggerConfig?: AppScheduledTriggerConfigType | null;
scheduledTriggerNextTime?: Date; scheduledTriggerNextTime?: Date;
permission: `${PermissionTypeEnum}`; permission: `${PermissionTypeEnum}`;
inited?: boolean; inited?: boolean;
teamTags: string[]; teamTags: string[];
} };
export type AppListItemType = { export type AppListItemType = {
_id: string; _id: string;
@ -66,33 +67,19 @@ export type AppSimpleEditFormType = {
datasetSearchExtensionBg?: string; datasetSearchExtensionBg?: string;
}; };
selectedTools: FlowNodeTemplateType[]; selectedTools: FlowNodeTemplateType[];
userGuide: { chatConfig: AppChatConfigType;
welcomeText: string;
variables: {
id: string;
key: string;
label: string;
type: `${VariableInputEnum}`;
required: boolean;
maxLen: number;
enums: {
value: string;
}[];
}[];
questionGuide: boolean;
tts: {
type: 'none' | 'web' | 'model';
model?: string | undefined;
voice?: string | undefined;
speed?: number | undefined;
};
whisper: AppWhisperConfigType;
scheduleTrigger: AppScheduledTriggerConfigType | null;
questionGuideText: AppQuestionGuideTextConfigType;
};
}; };
/* app function config */ /* app chat config type */
export type AppChatConfigType = {
welcomeText?: string;
variables?: VariableItemType[];
questionGuide?: boolean;
ttsConfig?: AppTTSConfigType;
whisperConfig?: AppWhisperConfigType;
scheduledTriggerConfig?: AppScheduledTriggerConfigType;
chatInputGuide?: ChatInputGuideConfigType;
};
export type SettingAIDataType = { export type SettingAIDataType = {
model: string; model: string;
temperature: number; temperature: number;
@ -125,10 +112,9 @@ export type AppWhisperConfigType = {
autoTTSResponse: boolean; autoTTSResponse: boolean;
}; };
// question guide text // question guide text
export type AppQuestionGuideTextConfigType = { export type ChatInputGuideConfigType = {
open: boolean; open: boolean;
textList: string[]; customUrl: string;
customURL: string;
}; };
// interval timer // interval timer
export type AppScheduledTriggerConfigType = { export type AppScheduledTriggerConfigType = {

View File

@ -1,50 +1,42 @@
import type { AppSimpleEditFormType } from '../app/type'; import type { AppChatConfigType, AppSimpleEditFormType } from '../app/type';
import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { FlowNodeTypeEnum } from '../workflow/node/constant';
import { NodeInputKeyEnum, FlowNodeTemplateTypeEnum } from '../workflow/constants'; import { NodeInputKeyEnum, FlowNodeTemplateTypeEnum } from '../workflow/constants';
import type { FlowNodeInputItemType } from '../workflow/type/io.d'; import type { FlowNodeInputItemType } from '../workflow/type/io.d';
import { getGuideModule, splitGuideModule } from '../workflow/utils'; import { getAppChatConfig } from '../workflow/utils';
import { StoreNodeItemType } from '../workflow/type'; import { StoreNodeItemType } from '../workflow/type';
import { DatasetSearchModeEnum } from '../dataset/constants'; import { DatasetSearchModeEnum } from '../dataset/constants';
import { defaultQuestionGuideTextConfig, defaultWhisperConfig } from './constants';
export const getDefaultAppForm = (): AppSimpleEditFormType => { export const getDefaultAppForm = (): AppSimpleEditFormType => ({
return { aiSettings: {
aiSettings: { model: 'gpt-3.5-turbo',
model: 'gpt-3.5-turbo', systemPrompt: '',
systemPrompt: '', temperature: 0,
temperature: 0, isResponseAnswerText: true,
isResponseAnswerText: true, maxHistories: 6,
maxHistories: 6, maxToken: 4000
maxToken: 4000 },
}, dataset: {
dataset: { datasets: [],
datasets: [], similarity: 0.4,
similarity: 0.4, limit: 1500,
limit: 1500, searchMode: DatasetSearchModeEnum.embedding,
searchMode: DatasetSearchModeEnum.embedding, usingReRank: false,
usingReRank: false, datasetSearchUsingExtensionQuery: true,
datasetSearchUsingExtensionQuery: true, datasetSearchExtensionBg: ''
datasetSearchExtensionBg: '' },
}, selectedTools: [],
selectedTools: [], chatConfig: {}
userGuide: { });
welcomeText: '',
variables: [],
questionGuide: false,
tts: {
type: 'web'
},
whisper: defaultWhisperConfig,
scheduleTrigger: null,
questionGuideText: defaultQuestionGuideTextConfig
}
};
};
/* format app nodes to edit form */ /* format app nodes to edit form */
export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => { export const appWorkflow2Form = ({
nodes,
chatConfig
}: {
nodes: StoreNodeItemType[];
chatConfig: AppChatConfigType;
}) => {
const defaultAppForm = getDefaultAppForm(); const defaultAppForm = getDefaultAppForm();
const findInputValueByKey = (inputs: FlowNodeInputItemType[], key: string) => { const findInputValueByKey = (inputs: FlowNodeInputItemType[], key: string) => {
return inputs.find((item) => item.key === key)?.value; return inputs.find((item) => item.key === key)?.value;
}; };
@ -103,26 +95,6 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => {
node.inputs, node.inputs,
NodeInputKeyEnum.datasetSearchExtensionBg NodeInputKeyEnum.datasetSearchExtensionBg
); );
} else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) {
const {
welcomeText,
variableNodes,
questionGuide,
ttsConfig,
whisperConfig,
scheduledTriggerConfig,
questionGuideText
} = splitGuideModule(getGuideModule(nodes));
defaultAppForm.userGuide = {
welcomeText: welcomeText,
variables: variableNodes,
questionGuide: questionGuide,
tts: ttsConfig,
whisper: whisperConfig,
scheduleTrigger: scheduledTriggerConfig,
questionGuideText: questionGuideText
};
} else if (node.flowNodeType === FlowNodeTypeEnum.pluginModule) { } else if (node.flowNodeType === FlowNodeTypeEnum.pluginModule) {
if (!node.pluginId) return; if (!node.pluginId) return;
@ -139,6 +111,12 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => {
outputs: node.outputs, outputs: node.outputs,
templateType: FlowNodeTemplateTypeEnum.other templateType: FlowNodeTemplateTypeEnum.other
}); });
} else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) {
defaultAppForm.chatConfig = getAppChatConfig({
chatConfig,
systemConfigNode: node,
isPublicFetch: true
});
} }
}); });

View File

@ -1,5 +1,6 @@
import { StoreNodeItemType } from '../workflow/type'; import { StoreNodeItemType } from '../workflow/type';
import { StoreEdgeItemType } from '../workflow/type/edge'; import { StoreEdgeItemType } from '../workflow/type/edge';
import { AppChatConfigType } from './type';
export type AppVersionSchemaType = { export type AppVersionSchemaType = {
_id: string; _id: string;
@ -7,4 +8,5 @@ export type AppVersionSchemaType = {
time: Date; time: Date;
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[]; edges: StoreEdgeItemType[];
chatConfig: AppChatConfigType;
}; };

View File

@ -0,0 +1,5 @@
export type ChatInputGuideSchemaType = {
_id: string;
appId: string;
text: string;
};

View File

@ -10,7 +10,7 @@ import {
import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { FlowNodeTypeEnum } from '../workflow/node/constant';
import { NodeOutputKeyEnum } from '../workflow/constants'; import { NodeOutputKeyEnum } from '../workflow/constants';
import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import { 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 { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type';

View File

@ -45,7 +45,7 @@ export enum NodeInputKeyEnum {
whisper = 'whisper', whisper = 'whisper',
variables = 'variables', variables = 'variables',
scheduleTrigger = 'scheduleTrigger', scheduleTrigger = 'scheduleTrigger',
questionGuideText = 'questionGuideText', chatInputGuide = 'chatInputGuide',
// entry // entry
userChatInput = 'userChatInput', userChatInput = 'userChatInput',

View File

@ -1,33 +0,0 @@
import { FlowNodeTemplateTypeEnum, WorkflowIOValueTypeEnum } from '../../constants';
import { getHandleConfig } from '../utils';
import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../node/constant';
import { VariableItemType } from '../../../app/type';
import { FlowNodeTemplateType } from '../../type';
export const getGlobalVariableNode = ({
id,
variables
}: {
id: string;
variables: VariableItemType[];
}): FlowNodeTemplateType => {
return {
id,
templateType: FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.systemConfig,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
avatar: '/imgs/workflow/variable.png',
name: '全局变量',
intro: '',
version: '481',
inputs: [],
outputs: variables.map((item) => ({
id: item.key,
key: item.key,
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static,
label: item.label
}))
};
};

View File

@ -1,10 +1,6 @@
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../node/constant'; import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/index.d'; import { FlowNodeTemplateType } from '../../type/index.d';
import { import { FlowNodeTemplateTypeEnum, WorkflowIOValueTypeEnum } from '../../constants';
WorkflowIOValueTypeEnum,
NodeInputKeyEnum,
FlowNodeTemplateTypeEnum
} from '../../constants';
import { getHandleConfig } from '../utils'; import { getHandleConfig } from '../utils';
export const SystemConfigNode: FlowNodeTemplateType = { export const SystemConfigNode: FlowNodeTemplateType = {
@ -19,50 +15,6 @@ export const SystemConfigNode: FlowNodeTemplateType = {
unique: true, unique: true,
forbidDelete: true, forbidDelete: true,
version: '481', version: '481',
inputs: [ inputs: [],
{
key: NodeInputKeyEnum.welcomeText,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.string,
label: 'core.app.Welcome Text'
},
{
key: NodeInputKeyEnum.variables,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: 'core.module.Variable',
value: []
},
{
key: NodeInputKeyEnum.questionGuide,
valueType: WorkflowIOValueTypeEnum.boolean,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: ''
},
{
key: NodeInputKeyEnum.tts,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: ''
},
{
key: NodeInputKeyEnum.whisper,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: ''
},
{
key: NodeInputKeyEnum.scheduleTrigger,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: ''
},
{
key: NodeInputKeyEnum.questionGuideText,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: ''
}
],
outputs: [] outputs: []
}; };

View File

@ -12,10 +12,15 @@ import type {
AppTTSConfigType, AppTTSConfigType,
AppWhisperConfigType, AppWhisperConfigType,
AppScheduledTriggerConfigType, AppScheduledTriggerConfigType,
AppQuestionGuideTextConfigType ChatInputGuideConfigType,
AppChatConfigType
} from '../app/type'; } from '../app/type';
import { EditorVariablePickerType } from '../../../web/components/common/Textarea/PromptEditor/type'; import { EditorVariablePickerType } from '../../../web/components/common/Textarea/PromptEditor/type';
import { defaultWhisperConfig } from '../app/constants'; import {
defaultChatInputGuideConfig,
defaultTTSConfig,
defaultWhisperConfig
} from '../app/constants';
import { IfElseResultEnum } from './template/system/ifElse/constant'; import { IfElseResultEnum } from './template/system/ifElse/constant';
export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => { export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => {
@ -41,70 +46,81 @@ export const splitGuideModule = (guideModules?: StoreNodeItemType) => {
const welcomeText: string = const welcomeText: string =
guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.welcomeText)?.value || ''; guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.welcomeText)?.value || '';
const variableNodes: VariableItemType[] = const variables: VariableItemType[] =
guideModules?.inputs.find((item) => item.key === NodeInputKeyEnum.variables)?.value || []; guideModules?.inputs.find((item) => item.key === NodeInputKeyEnum.variables)?.value || [];
const questionGuide: boolean = const questionGuide: boolean =
!!guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value || !!guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value ||
false; false;
const ttsConfig: AppTTSConfigType = guideModules?.inputs?.find( const ttsConfig: AppTTSConfigType =
(item) => item.key === NodeInputKeyEnum.tts guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.tts)?.value ||
)?.value || { type: 'web' }; defaultTTSConfig;
const whisperConfig: AppWhisperConfigType = const whisperConfig: AppWhisperConfigType =
guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.whisper)?.value || guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.whisper)?.value ||
defaultWhisperConfig; defaultWhisperConfig;
const scheduledTriggerConfig: AppScheduledTriggerConfigType | null = const scheduledTriggerConfig: AppScheduledTriggerConfigType = guideModules?.inputs?.find(
guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.scheduleTrigger)?.value ?? (item) => item.key === NodeInputKeyEnum.scheduleTrigger
null; )?.value;
const questionGuideText: AppQuestionGuideTextConfigType = guideModules?.inputs?.find( const chatInputGuide: ChatInputGuideConfigType =
(item) => item.key === NodeInputKeyEnum.questionGuideText guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.chatInputGuide)?.value ||
)?.value || { defaultChatInputGuideConfig;
open: false
};
return { return {
welcomeText, welcomeText,
variableNodes, variables,
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,
scheduledTriggerConfig, scheduledTriggerConfig,
questionGuideText chatInputGuide
}; };
}; };
export const replaceAppChatConfig = ({ export const getAppChatConfig = ({
node, chatConfig,
variableList, systemConfigNode,
welcomeText storeVariables,
storeWelcomeText,
isPublicFetch = false
}: { }: {
node?: StoreNodeItemType; chatConfig?: AppChatConfigType;
variableList?: VariableItemType[]; systemConfigNode?: StoreNodeItemType;
welcomeText?: string; storeVariables?: VariableItemType[];
}): StoreNodeItemType | undefined => { storeWelcomeText?: string;
if (!node) return; isPublicFetch: boolean;
return { }): AppChatConfigType => {
...node, const {
inputs: node.inputs.map((input) => { welcomeText,
if (input.key === NodeInputKeyEnum.variables && variableList) { variables,
return { questionGuide,
...input, ttsConfig,
value: variableList whisperConfig,
}; scheduledTriggerConfig,
} chatInputGuide
if (input.key === NodeInputKeyEnum.welcomeText && welcomeText) { } = splitGuideModule(systemConfigNode);
return {
...input,
value: welcomeText
};
}
return input; const config: AppChatConfigType = {
}) questionGuide,
ttsConfig,
whisperConfig,
scheduledTriggerConfig,
chatInputGuide,
...chatConfig,
variables: storeVariables ?? chatConfig?.variables ?? variables,
welcomeText: storeWelcomeText ?? chatConfig?.welcomeText ?? welcomeText
}; };
if (!isPublicFetch) {
if (config?.chatInputGuide?.customUrl) {
config.chatInputGuide.customUrl = '';
}
config.scheduledTriggerConfig = undefined;
}
return config;
}; };
export const getOrInitModuleInputValue = (input: FlowNodeInputItemType) => { export const getOrInitModuleInputValue = (input: FlowNodeInputItemType) => {

View File

@ -2,7 +2,7 @@ import { AppSchema } from '@fastgpt/global/core/app/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getLLMModel } from '../ai/model'; import { getLLMModel } from '../ai/model';
import { MongoAppVersion } from './versionSchema'; import { MongoAppVersion } from './version/schema';
export const beforeUpdateAppFormat = <T extends AppSchema['modules'] | undefined>({ export const beforeUpdateAppFormat = <T extends AppSchema['modules'] | undefined>({
nodes nodes
@ -55,11 +55,13 @@ export const getAppLatestVersion = async (appId: string, app?: AppSchema) => {
if (version) { if (version) {
return { return {
nodes: version.nodes, nodes: version.nodes,
edges: version.edges edges: version.edges,
chatConfig: version.chatConfig || app?.chatConfig || {}
}; };
} }
return { return {
nodes: app?.modules || [], nodes: app?.modules || [],
edges: app?.edges || [] edges: app?.edges || [],
chatConfig: app?.chatConfig || {}
}; };
}; };

View File

@ -1,40 +0,0 @@
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
export const AppQGuideCollectionName = 'app_question_guides';
type AppQGuideSchemaType = {
_id: string;
appId: string;
teamId: string;
text: string;
};
const AppQGuideSchema = new Schema({
appId: {
type: Schema.Types.ObjectId,
ref: AppQGuideCollectionName,
required: true
},
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
text: {
type: String,
default: ''
}
});
try {
AppQGuideSchema.index({ appId: 1 });
} catch (error) {
console.log(error);
}
export const MongoAppQGuide: Model<AppQGuideSchemaType> =
models[AppQGuideCollectionName] || model(AppQGuideCollectionName, AppQGuideSchema);
MongoAppQGuide.syncIndexes();

View File

@ -10,6 +10,16 @@ import {
export const AppCollectionName = 'apps'; export const AppCollectionName = 'apps';
export const chatConfigType = {
welcomeText: String,
variables: Array,
questionGuide: Boolean,
ttsConfig: Object,
whisperConfig: Object,
scheduledTriggerConfig: Object,
chatInputGuide: Object
};
const AppSchema = new Schema({ const AppSchema = new Schema({
teamId: { teamId: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@ -47,6 +57,16 @@ const AppSchema = new Schema({
default: () => new Date() default: () => new Date()
}, },
// role and auth
permission: {
type: String,
enum: Object.keys(PermissionTypeMap),
default: PermissionTypeEnum.private
},
teamTags: {
type: [String]
},
// tmp store // tmp store
modules: { modules: {
type: Array, type: Array,
@ -56,6 +76,10 @@ const AppSchema = new Schema({
type: Array, type: Array,
default: [] default: []
}, },
chatConfig: {
type: chatConfigType,
default: {}
},
scheduledTriggerConfig: { scheduledTriggerConfig: {
cronString: { cronString: {
@ -74,14 +98,6 @@ const AppSchema = new Schema({
inited: { inited: {
type: Boolean type: Boolean
},
permission: {
type: String,
enum: Object.keys(PermissionTypeMap),
default: PermissionTypeEnum.private
},
teamTags: {
type: [String]
} }
}); });

View File

@ -1,6 +1,7 @@
import { connectionMongo, type Model } from '../../common/mongo'; import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo; const { Schema, model, models } = connectionMongo;
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import { chatConfigType } from '../schema';
export const AppVersionCollectionName = 'app_versions'; export const AppVersionCollectionName = 'app_versions';
@ -21,6 +22,10 @@ const AppVersionSchema = new Schema({
edges: { edges: {
type: Array, type: Array,
default: [] default: []
},
chatConfig: {
type: chatConfigType,
default: {}
} }
}); });

View File

@ -0,0 +1,29 @@
import { AppCollectionName } from '../../app/schema';
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import type { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type.d';
export const ChatInputGuideCollectionName = 'chat_input_guides';
const ChatInputGuideSchema = new Schema({
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName,
required: true
},
text: {
type: String,
default: ''
}
});
try {
ChatInputGuideSchema.index({ appId: 1, text: 1 }, { unique: true });
} catch (error) {
console.log(error);
}
export const MongoChatInputGuide: Model<ChatInputGuideSchemaType> =
models[ChatInputGuideCollectionName] || model(ChatInputGuideCollectionName, ChatInputGuideSchema);
MongoChatInputGuide.syncIndexes();

View File

@ -168,7 +168,8 @@ export async function pushDataListToTrainingQueue({
indexes: item.indexes indexes: item.indexes
})), })),
{ {
session session,
ordered: false
} }
); );
} catch (error: any) { } catch (error: any) {

View File

@ -44,6 +44,7 @@ import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
import { dispatchSystemConfig } from './init/systemConfig'; import { dispatchSystemConfig } from './init/systemConfig';
import { dispatchUpdateVariable } from './tools/runUpdateVar'; import { dispatchUpdateVariable } from './tools/runUpdateVar';
import { addLog } from '../../../common/system/log';
const callbackMap: Record<FlowNodeTypeEnum, Function> = { const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
@ -137,7 +138,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
} }
if (nodeDispatchUsages) { if (nodeDispatchUsages) {
chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages); chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages);
props.maxRunTimes -= nodeDispatchUsages.length;
} }
if (toolResponses !== undefined) { if (toolResponses !== undefined) {
if (Array.isArray(toolResponses) && toolResponses.length === 0) return; if (Array.isArray(toolResponses) && toolResponses.length === 0) return;
@ -213,9 +213,11 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}); });
if (status === 'run') { if (status === 'run') {
addLog.info(`[dispatchWorkFlow] nodeRunWithActive: ${node.name}`);
return nodeRunWithActive(node); return nodeRunWithActive(node);
} }
if (status === 'skip') { if (status === 'skip') {
addLog.info(`[dispatchWorkFlow] nodeRunWithActive: ${node.name}`);
return nodeRunWithSkip(node); return nodeRunWithSkip(node);
} }
@ -275,6 +277,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}); });
} }
props.maxRunTimes--;
// get node running params // get node running params
const params = getNodeRunParams(node); const params = getNodeRunParams(node);

View File

@ -1,4 +1,4 @@
import { NextApiRequest } from 'next'; import { ApiRequestProps } from '../../type/next';
export type ReqHeaderAuthType = { export type ReqHeaderAuthType = {
cookie?: string; cookie?: string;
@ -9,7 +9,7 @@ export type ReqHeaderAuthType = {
authorization?: string; authorization?: string;
}; };
export type AuthModeType = { export type AuthModeType = {
req: NextApiRequest; req: ApiRequestProps;
authToken?: boolean; authToken?: boolean;
authRoot?: boolean; authRoot?: boolean;
authApiKey?: boolean; authApiKey?: boolean;

View File

@ -45,7 +45,7 @@ const UserSchema = new Schema({
inviterId: { inviterId: {
// 谁邀请注册的 // 谁邀请注册的
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'user' ref: userCollectionName
}, },
promotionRate: { promotionRate: {
type: Number, type: Number,

View File

@ -1,4 +1,5 @@
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import Papa from 'papaparse';
export const loadFile2Buffer = ({ file, onError }: { file: File; onError?: (err: any) => void }) => export const loadFile2Buffer = ({ file, onError }: { file: File; onError?: (err: any) => void }) =>
new Promise<ArrayBuffer>((resolve, reject) => { new Promise<ArrayBuffer>((resolve, reject) => {
@ -29,3 +30,47 @@ export const loadFile2Buffer = ({ file, onError }: { file: File; onError?: (err:
reject('The browser does not support file content reading'); reject('The browser does not support file content reading');
} }
}); });
export const readFileRawText = ({
file,
onError
}: {
file: File;
onError?: (err: any) => void;
}) => {
return new Promise<string>((resolve, reject) => {
try {
let reader = new FileReader();
reader.onload = async ({ target }) => {
if (!target?.result) {
onError?.('Load file error');
return reject('Load file error');
}
try {
resolve(target.result as string);
} catch (err) {
console.log(err, 'Load file error');
onError?.(err);
reject(getErrText(err, 'Load file error'));
}
};
reader.onerror = (err) => {
console.log(err, 'Load file error');
onError?.(err);
reject(getErrText(err, 'Load file error'));
};
reader.readAsText(file);
} catch (error) {
reject('The browser does not support file content reading');
}
});
};
export const readCsvRawText = async ({ file }: { file: File }) => {
const rawText = await readFileRawText({ file });
const csvArr = Papa.parse(rawText).data as string[][];
return csvArr;
};

View File

@ -10,7 +10,7 @@ type Props = FlexProps & {
const EmptyTip = ({ text, ...props }: Props) => { const EmptyTip = ({ text, ...props }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Flex mt={5} flexDirection={'column'} alignItems={'center'} pt={'10vh'} {...props}> <Flex mt={5} flexDirection={'column'} alignItems={'center'} py={'10vh'} {...props}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} /> <MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}> <Box mt={2} color={'myGray.500'}>
{text || t('common.empty.Common Tip')} {text || t('common.empty.Common Tip')}

View File

@ -15,7 +15,7 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
.catch((error) => console.log(error)); .catch((error) => console.log(error));
}, [name]); }, [name]);
return !!name && !!iconPaths[name] ? ( return !!IconComponent ? (
<Icon <Icon
{...IconComponent} {...IconComponent}
w={w} w={w}

View File

@ -9,7 +9,7 @@ type Props = BoxProps & {
const MyBox = ({ text, isLoading, children, ...props }: Props, ref: any) => { const MyBox = ({ text, isLoading, children, ...props }: Props, ref: any) => {
return ( return (
<Box ref={ref} position={'relative'} {...props}> <Box ref={ref} position={isLoading ? 'relative' : 'unset'} {...props}>
{isLoading && <Loading fixed={false} text={text} />} {isLoading && <Loading fixed={false} text={text} />}
{children} {children}
</Box> </Box>

View File

@ -11,11 +11,13 @@ import {
useMediaQuery useMediaQuery
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import MyIcon from '../Icon'; import MyIcon from '../Icon';
import MyBox from '../MyBox';
export interface MyModalProps extends ModalContentProps { export interface MyModalProps extends ModalContentProps {
iconSrc?: string; iconSrc?: string;
title?: any; title?: any;
isCentered?: boolean; isCentered?: boolean;
isLoading?: boolean;
isOpen: boolean; isOpen: boolean;
onClose?: () => void; onClose?: () => void;
} }
@ -27,6 +29,7 @@ const MyModal = ({
title, title,
children, children,
isCentered, isCentered,
isLoading,
w = 'auto', w = 'auto',
maxW = ['90vw', '600px'], maxW = ['90vw', '600px'],
...props ...props
@ -39,6 +42,7 @@ const MyModal = ({
onClose={() => onClose && onClose()} onClose={() => onClose && onClose()}
autoFocus={false} autoFocus={false}
isCentered={isPc ? isCentered : true} isCentered={isPc ? isCentered : true}
blockScrollOnMount={false}
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent <ModalContent
@ -78,14 +82,15 @@ const MyModal = ({
</ModalHeader> </ModalHeader>
)} )}
<Box <MyBox
isLoading={isLoading}
overflow={props.overflow || 'overlay'} overflow={props.overflow || 'overlay'}
h={'100%'} h={'100%'}
display={'flex'} display={'flex'}
flexDirection={'column'} flexDirection={'column'}
> >
{children} {children}
</Box> </MyBox>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );

View File

@ -0,0 +1,40 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
const HighlightText = ({
rawText,
matchText,
color = 'primary.600'
}: {
rawText: string;
matchText: string;
color?: string;
}) => {
const regex = new RegExp(`(${matchText})`, 'gi');
const parts = rawText.split(regex);
return (
<Box>
{parts.map((part, index) => {
let highLight = part.toLowerCase() === matchText.toLowerCase();
if (highLight) {
parts.find((item, i) => {
if (i >= index) return;
if (item.toLowerCase() === matchText.toLowerCase()) {
highLight = false;
}
});
}
return (
<Box as="span" key={index} color={highLight ? color : 'inherit'}>
{part}
</Box>
);
})}
</Box>
);
};
export default HighlightText;

View File

@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptions } from '@tanstack/react-query'; import type { UseMutationOptions } from '@tanstack/react-query';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useRequest as ahooksUseRequest } from 'ahooks';
interface Props extends UseMutationOptions<any, any, any, any> { interface Props extends UseMutationOptions<any, any, any, any> {
successToast?: string | null; successToast?: string | null;
@ -39,3 +40,50 @@ export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...pr
return mutation; return mutation;
}; };
type UseRequestFunProps<TData, TParams extends any[]> = Parameters<
typeof ahooksUseRequest<TData, TParams>
>;
export const useRequest2 = <TData, TParams extends any[]>(
server: UseRequestFunProps<TData, TParams>[0],
options: UseRequestFunProps<TData, TParams>[1] & {
errorToast?: string;
successToast?: string;
} = {},
plugin?: UseRequestFunProps<TData, TParams>[2]
) => {
const { t } = useTranslation();
const { errorToast, successToast, ...rest } = options || {};
const { toast } = useToast();
const res = ahooksUseRequest<TData, TParams>(
server,
{
...rest,
onError: (err, params) => {
rest?.onError?.(err, params);
if (errorToast !== undefined) {
const errText = t(getErrText(err, errorToast || ''));
if (errText) {
toast({
title: errText,
status: 'error'
});
}
}
},
onSuccess: (res, params) => {
rest?.onSuccess?.(res, params);
if (successToast) {
toast({
title: successToast,
status: 'success'
});
}
}
},
plugin
);
return res;
};

View File

@ -1,9 +1,17 @@
import { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { Box, BoxProps } from '@chakra-ui/react'; import { Box, BoxProps } from '@chakra-ui/react';
import { useToast } from './useToast'; import { useToast } from './useToast';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { PaginationProps, PaginationResponse } from '../common/fetch/type'; import { PaginationProps, PaginationResponse } from '../common/fetch/type';
import { useBoolean, useLockFn, useMemoizedFn, useMount, useScroll, useVirtualList } from 'ahooks'; import {
useBoolean,
useLockFn,
useMemoizedFn,
useMount,
useScroll,
useVirtualList,
useRequest
} from 'ahooks';
import MyBox from '../components/common/MyBox'; import MyBox from '../components/common/MyBox';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@ -13,12 +21,19 @@ export function useScrollPagination<
>( >(
api: (data: TParams) => Promise<TData>, api: (data: TParams) => Promise<TData>,
{ {
debounceWait,
throttleWait,
refreshDeps,
itemHeight = 50, itemHeight = 50,
overscan = 10, overscan = 10,
pageSize = 10, pageSize = 10,
defaultParams = {} defaultParams = {}
}: { }: {
debounceWait?: number;
throttleWait?: number;
refreshDeps?: any[];
itemHeight: number; itemHeight: number;
overscan?: number; overscan?: number;
@ -45,7 +60,7 @@ export function useScrollPagination<
}); });
const loadData = useLockFn(async (num: number = current) => { const loadData = useLockFn(async (num: number = current) => {
if (noMore.current) return; if (noMore.current && num !== 1) return;
setTrue(); setTrue();
@ -59,7 +74,7 @@ export function useScrollPagination<
setCurrent(num); setCurrent(num);
if (num === 1) { if (num === 1) {
// reload // init or reload
setData(res.list); setData(res.list);
noMore.current = res.list.length >= res.total; noMore.current = res.list.length >= res.total;
} else { } else {
@ -78,34 +93,48 @@ export function useScrollPagination<
setFalse(); setFalse();
}); });
const scroll2Top = () => {
if (containerRef.current) {
containerRef.current.scrollTop = 0;
}
};
const ScrollList = useMemoizedFn( const ScrollList = useMemoizedFn(
({ ({
children, children,
EmptyChildren,
isLoading, isLoading,
...props ...props
}: { children: React.ReactNode; isLoading?: boolean } & BoxProps) => { }: {
children: React.ReactNode;
EmptyChildren?: React.ReactNode;
isLoading?: boolean;
} & BoxProps) => {
return ( return (
<> <>
<MyBox isLoading={isLoading} ref={containerRef} overflow={'overlay'} {...props}> <MyBox isLoading={isLoading} ref={containerRef} overflow={'overlay'} {...props}>
<Box ref={wrapperRef}>{children}</Box> <Box ref={wrapperRef}>{children}</Box>
{noMore.current && list.length > 0 && (
<Box py={4} textAlign={'center'} color={'myGray.600'} fontSize={'sm'}>
{t('common.No more data')}
</Box>
)}
{list.length === 0 && !isLoading && EmptyChildren && <>{EmptyChildren}</>}
</MyBox> </MyBox>
{noMore.current && (
<Box pb={2} textAlign={'center'} color={'myGray.600'} fontSize={'sm'}>
{t('common.No more data')}
</Box>
)}
</> </>
); );
} }
); );
useMount(() => { useRequest(() => loadData(1), {
loadData(1); refreshDeps,
debounceWait: data.length === 0 ? 0 : debounceWait,
throttleWait
}); });
const scroll = useScroll(containerRef); const scroll = useScroll(containerRef);
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current || list.length === 0) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
@ -118,8 +147,10 @@ export function useScrollPagination<
containerRef, containerRef,
list, list,
data, data,
setData,
isLoading, isLoading,
ScrollList, ScrollList,
fetchData: loadData fetchData: loadData,
scroll2Top
}; };
} }

View File

@ -26,7 +26,6 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next-i18next": "15.2.0", "next-i18next": "15.2.0",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pdfjs-dist": "4.0.269",
"react": "18.3.1", "react": "18.3.1",
"use-context-selector": "^1.4.4", "use-context-selector": "^1.4.4",
"react-day-picker": "^8.7.1", "react-day-picker": "^8.7.1",

3
pnpm-lock.yaml generated
View File

@ -283,9 +283,6 @@ importers:
papaparse: papaparse:
specifier: ^5.4.1 specifier: ^5.4.1
version: 5.4.1 version: 5.4.1
pdfjs-dist:
specifier: 4.0.269
version: 4.0.269(encoding@0.1.13)
react: react:
specifier: 18.3.1 specifier: 18.3.1
version: 18.3.1 version: 18.3.1

View File

@ -47,14 +47,6 @@
"type": "\"{{type}}\" type\n{{description}}" "type": "\"{{type}}\" type\n{{description}}"
}, },
"modules": { "modules": {
"Config Texts": "Config Texts",
"Config question guide": "Config question guide",
"Custom question guide URL": "Custom question guide URL",
"Input Guide": "Input Guide",
"Only support CSV": "Only support CSV",
"Question Guide": "Question guide",
"Question Guide Switch": "Open question guide",
"Question Guide Texts": "Texts",
"Title is required": "Module name cannot be empty" "Title is required": "Module name cannot be empty"
} }
} }

View File

@ -0,0 +1,18 @@
{
"Chat input guide lexicon is empty": "The lexicon has not been configured",
"Config Texts": "Config thesaurus ",
"Config input guide lexicon": "Config",
"Config input guide lexicon title": "Config lexicon",
"Config question guide": "Configuration input Prompt ",
"Csv input lexicon tip": "Only CSV can be imported in batches. Click to download the template",
"Custom input guide url": "Custom lexicon url",
"Custom question guide URL": "Custom lexicon address ",
"Input Guide": "Intelligent Recommendation ",
"Input guide": "Input guide",
"Input guide lexicon": "Lexicon",
"Input guide tip": "You can configure some preset questions. When the user enters a question, the relevant question is retrieved from these preset questions for prompt.",
"Insert input guide, Some data already exists": "Duplicate data, automatically filtered, insert: {{len}} data",
"New input guide lexicon": "New lexicon",
"Only support CSV": "Only support CSV import, click download template ",
"Question Guide Texts": "Lexicon"
}

View File

@ -3,6 +3,7 @@
"App": "App", "App": "App",
"Export": "Export", "Export": "Export",
"Folder": "Folder", "Folder": "Folder",
"Is open": "Opened",
"Login": "Login", "Login": "Login",
"Move": "Move", "Move": "Move",
"Name": "Name", "Name": "Name",
@ -16,6 +17,7 @@
"Action": "Action", "Action": "Action",
"Add": "Add", "Add": "Add",
"Add New": "Add New", "Add New": "Add New",
"Add Success": "Add successfully",
"All": "All", "All": "All",
"Back": "Back", "Back": "Back",
"Beta": "Beta", "Beta": "Beta",
@ -1177,6 +1179,7 @@
} }
}, },
"error": { "error": {
"Create failed": "Create failed",
"fileNotFound": "File not found~", "fileNotFound": "File not found~",
"team": { "team": {
"overSize": "Team members exceed the limit" "overSize": "Team members exceed the limit"

View File

@ -46,14 +46,6 @@
"type": "\"{{type}}\"类型\n{{description}}" "type": "\"{{type}}\"类型\n{{description}}"
}, },
"modules": { "modules": {
"Config Texts": "配置词库",
"Config question guide": "配置输入提示",
"Custom question guide URL": "自定义词库地址",
"Input Guide": "智能推荐",
"Only support CSV": "仅支持 CSV 导入,点击下载模板",
"Question Guide": "输入提示",
"Question Guide Switch": "是否开启",
"Question Guide Texts": "词库",
"Title is required": "模块名不能为空" "Title is required": "模块名不能为空"
} }
} }

View File

@ -0,0 +1,13 @@
{
"Chat input guide lexicon is empty": "还没有配置词库",
"Config input guide": "配置输入引导",
"Config input guide lexicon": "配置词库",
"Config input guide lexicon title": "配置词库",
"Csv input lexicon tip": "仅支持 CSV 批量导入,点击下载模板",
"Custom input guide url": "自定义词库地址",
"Input guide": "输入引导",
"Input guide lexicon": "词库",
"Input guide tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。",
"Insert input guide, Some data already exists": "有重复数据,已自动过滤,共插入: {{len}} 条数据",
"New input guide lexicon": "新词库"
}

View File

@ -3,6 +3,7 @@
"App": "应用", "App": "应用",
"Export": "导出", "Export": "导出",
"Folder": "文件夹", "Folder": "文件夹",
"Is open": "是否开启",
"Login": "登录", "Login": "登录",
"Move": "移动", "Move": "移动",
"Name": "名称", "Name": "名称",
@ -16,6 +17,7 @@
"Action": "操作", "Action": "操作",
"Add": "添加", "Add": "添加",
"Add New": "新增", "Add New": "新增",
"Add Success": "添加成功",
"All": "全部", "All": "全部",
"Back": "返回", "Back": "返回",
"Beta": "实验版", "Beta": "实验版",
@ -1184,6 +1186,7 @@
} }
}, },
"error": { "error": {
"Create failed": "创建失败",
"fileNotFound": "文件找不到了~", "fileNotFound": "文件找不到了~",
"team": { "team": {
"overSize": "团队成员超出上限" "overSize": "团队成员超出上限"

View File

@ -86,7 +86,7 @@ const nextConfig = {
return config; return config;
}, },
transpilePackages: ['@fastgpt/*', 'ahooks', '@chakra-ui/*', 'react'], transpilePackages: ['@fastgpt/*', 'ahooks'],
experimental: { experimental: {
// 指定导出包优化,按需引入包模块 // 指定导出包优化,按需引入包模块
optimizePackageImports: ['mongoose', 'pg'], optimizePackageImports: ['mongoose', 'pg'],

View File

@ -1,6 +1,6 @@
{ {
"name": "app", "name": "app",
"version": "4.8", "version": "4.8.1",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@ -1,9 +1,9 @@
import { useSpeech } from '@/web/common/hooks/useSpeech'; import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react'; import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
import React, { useRef, useEffect, useCallback, useTransition } from 'react'; import React, { useRef, useEffect, useCallback } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyTooltip from '../MyTooltip'; import MyTooltip from '../../MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller'; import { compressImgFileAndUpload } from '@/web/common/file/controller';
@ -12,18 +12,16 @@ import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from './type'; import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type';
import { textareaMinH } from './constants'; import { textareaMinH } from '../constants';
import { UseFormReturn, useFieldArray } from 'react-hook-form'; import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { useChatProviderStore } from './Provider'; import { useChatProviderStore } from '../Provider';
import QuestionGuide from './components/QustionGuide'; import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const MessageInput = ({ const InputGuideBox = dynamic(() => import('./InputGuideBox'));
const ChatInput = ({
onSendMessage, onSendMessage,
onStop, onStop,
TextareaDom, TextareaDom,
@ -38,7 +36,7 @@ const MessageInput = ({
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>; TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
resetInputVal: (val: ChatBoxInputType) => void; resetInputVal: (val: ChatBoxInputType) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>; chatForm: UseFormReturn<ChatBoxInputFormType>;
appId?: string; appId: string;
}) => { }) => {
const { setValue, watch, control } = chatForm; const { setValue, watch, control } = chatForm;
const inputValue = watch('input'); const inputValue = watch('input');
@ -53,12 +51,19 @@ const MessageInput = ({
name: 'files' name: 'files'
}); });
const { shareId, outLinkUid, teamId, teamToken, isChatting, whisperConfig, autoTTSResponse } = const {
useChatProviderStore(); shareId,
outLinkUid,
teamId,
teamToken,
isChatting,
whisperConfig,
autoTTSResponse,
chatInputGuide
} = useChatProviderStore();
const { isPc, whisperModel } = useSystemStore(); const { isPc, whisperModel } = useSystemStore();
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { appDetail } = useAppStore();
const havInput = !!inputValue || fileList.length > 0; const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url); const hasFileUploading = fileList.some((item) => !item.url);
@ -150,9 +155,9 @@ const MessageInput = ({
); );
/* on send */ /* on send */
const handleSend = async () => { const handleSend = async (val?: string) => {
if (!canSendMessage) return; if (!canSendMessage) return;
const textareaValue = TextareaDom.current?.value || ''; const textareaValue = val || TextareaDom.current?.value || '';
onSendMessage({ onSendMessage({
text: textareaValue.trim(), text: textareaValue.trim(),
@ -211,23 +216,6 @@ const MessageInput = ({
startSpeak(finishWhisperTranscription); startSpeak(finishWhisperTranscription);
}, [finishWhisperTranscription, isSpeaking, startSpeak, stopSpeak]); }, [finishWhisperTranscription, isSpeaking, startSpeak, stopSpeak]);
const { data } = useQuery(
[appId, inputValue],
async () => {
if (!appId) return { list: [], total: 0 };
return getMyQuestionGuides({
appId,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 5,
current: 1,
searchKey: inputValue
});
},
{
enabled: !!appId
}
);
return ( return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}> <Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box <Box
@ -248,6 +236,20 @@ const MessageInput = ({
borderTopColor: 'rgba(0,0,0,0.15)' borderTopColor: 'rgba(0,0,0,0.15)'
})} })}
> >
{/* Chat input guide box */}
{chatInputGuide.open && (
<InputGuideBox
appId={appId}
text={inputValue}
onSelect={(e) => {
setValue('input', e);
}}
onSend={(e) => {
handleSend(e);
}}
/>
)}
{/* translate loading */} {/* translate loading */}
<Flex <Flex
position={'absolute'} position={'absolute'}
@ -266,21 +268,6 @@ const MessageInput = ({
{t('core.chat.Converting to text')} {t('core.chat.Converting to text')}
</Flex> </Flex>
{/* popup */}
{havInput && (
<QuestionGuide
guides={data?.list || []}
setDropdownValue={(value) => setValue('input', value)}
bottom={'100%'}
top={'auto'}
left={0}
right={0}
mb={2}
overflowY={'auto'}
boxShadow={'sm'}
/>
)}
{/* file preview */} {/* file preview */}
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}> <Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
{fileList.map((item, index) => ( {fileList.map((item, index) => (
@ -415,12 +402,7 @@ const MessageInput = ({
// @ts-ignore // @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select(); e.key === 'a' && e.ctrlKey && e.target?.select();
if ( if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
(isPc || window !== parent) &&
e.keyCode === 13 &&
!e.shiftKey &&
!(havInput && data?.list.length && data?.list.length > 0)
) {
handleSend(); handleSend();
e.preventDefault(); e.preventDefault();
} }
@ -556,4 +538,4 @@ const MessageInput = ({
); );
}; };
export default React.memo(MessageInput); export default React.memo(ChatInput);

View File

@ -0,0 +1,111 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { queryChatInputGuideList } from '@/web/core/chat/inputGuide/api';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import HighlightText from '@fastgpt/web/components/common/String/HighlightText';
import { useChatProviderStore } from '../Provider';
export default function InputGuideBox({
appId,
text,
onSelect,
onSend
}: {
appId: string;
text: string;
onSelect: (text: string) => void;
onSend: (text: string) => void;
}) {
const { t } = useTranslation();
const { chatT } = useI18n();
const { chatInputGuide } = useChatProviderStore();
const { data = [] } = useRequest2(
async () => {
if (!text) return [];
return await queryChatInputGuideList(
{
appId,
searchKey: text
},
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined
);
},
{
refreshDeps: [text],
throttleWait: 300
}
);
const filterData = data.filter((item) => item !== text).slice(0, 5);
return filterData.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={-3}
w={'100%'}
zIndex={150}
transform={'translateY(-100%)'}
>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'} gap={2} mb={2} px={2}>
<MyIcon name={'union'} />
<Box>{chatT('Input guide')}</Box>
</Flex>
{data.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item}
px={4}
py={3}
borderRadius={'sm'}
cursor={'pointer'}
overflow={'auto'}
_notLast={{
mb: 1
}}
bg={'myGray.50'}
color={'myGray.600'}
_hover={{
bg: 'primary.50',
color: 'primary.600',
'.send-icon': {
display: 'block'
}
}}
onClick={() => onSelect(item)}
>
<Box fontSize={'sm'} flex={'1 0 0'}>
<HighlightText rawText={item} matchText={text} />
</Box>
<MyTooltip label={t('core.chat.markdown.Send Question')}>
<MyIcon
className="send-icon"
display={'none'}
name={'chatSend'}
boxSize={4}
color={'myGray.500'}
_hover={{
color: 'primary.600'
}}
onClick={(e) => {
e.stopPropagation();
onSend(item);
}}
/>
</MyTooltip>
</Flex>
))}
</Box>
) : null;
}

View File

@ -2,17 +2,23 @@ import React, { useContext, createContext, useState, useMemo, useEffect, useCall
import { useAudioPlay } from '@/web/common/utils/voice'; import { useAudioPlay } from '@/web/common/utils/voice';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { import {
AppChatConfigType,
AppTTSConfigType, AppTTSConfigType,
AppWhisperConfigType, AppWhisperConfigType,
ChatInputGuideConfigType,
VariableItemType VariableItemType
} from '@fastgpt/global/core/app/type'; } from '@fastgpt/global/core/app/type';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type'; import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import {
defaultChatInputGuideConfig,
defaultTTSConfig,
defaultWhisperConfig
} from '@fastgpt/global/core/app/constants';
type useChatStoreType = OutLinkChatAuthProps & { type useChatStoreType = OutLinkChatAuthProps & {
welcomeText: string; welcomeText: string;
variableNodes: VariableItemType[]; variableList: VariableItemType[];
questionGuide: boolean; questionGuide: boolean;
ttsConfig: AppTTSConfigType; ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType; whisperConfig: AppWhisperConfigType;
@ -38,10 +44,11 @@ type useChatStoreType = OutLinkChatAuthProps & {
chatHistories: ChatSiteItemType[]; chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>; setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
isChatting: boolean; isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
}; };
const StateContext = createContext<useChatStoreType>({ const StateContext = createContext<useChatStoreType>({
welcomeText: '', welcomeText: '',
variableNodes: [], variableList: [],
questionGuide: false, questionGuide: false,
ttsConfig: { ttsConfig: {
type: 'none', type: 'none',
@ -87,11 +94,15 @@ const StateContext = createContext<useChatStoreType>({
}, },
finishSegmentedAudio: function (): void { finishSegmentedAudio: function (): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
},
chatInputGuide: {
open: false,
customUrl: ''
} }
}); });
export type ChatProviderProps = OutLinkChatAuthProps & { export type ChatProviderProps = OutLinkChatAuthProps & {
userGuideModule?: StoreNodeItemType; chatConfig?: AppChatConfigType;
// not chat test params // not chat test params
chatId?: string; chatId?: string;
@ -105,15 +116,19 @@ const Provider = ({
outLinkUid, outLinkUid,
teamId, teamId,
teamToken, teamToken,
userGuideModule, chatConfig = {},
children children
}: ChatProviderProps) => { }: ChatProviderProps) => {
const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]); const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]);
const { welcomeText, variableNodes, questionGuide, ttsConfig, whisperConfig } = useMemo( const {
() => splitGuideModule(userGuideModule), welcomeText = '',
[userGuideModule] variables = [],
); questionGuide = false,
ttsConfig = defaultTTSConfig,
whisperConfig = defaultWhisperConfig,
chatInputGuide = defaultChatInputGuideConfig
} = useMemo(() => chatConfig, [chatConfig]);
// segment audio // segment audio
const [audioPlayingChatId, setAudioPlayingChatId] = useState<string>(); const [audioPlayingChatId, setAudioPlayingChatId] = useState<string>();
@ -150,7 +165,7 @@ const Provider = ({
teamId, teamId,
teamToken, teamToken,
welcomeText, welcomeText,
variableNodes, variableList: variables,
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,
@ -167,7 +182,8 @@ const Provider = ({
setAudioPlayingChatId, setAudioPlayingChatId,
chatHistories, chatHistories,
setChatHistories, setChatHistories,
isChatting isChatting,
chatInputGuide
}; };
return <StateContext.Provider value={value}>{children}</StateContext.Provider>; return <StateContext.Provider value={value}>{children}</StateContext.Provider>;

View File

@ -1,98 +0,0 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import React, { useCallback, useEffect } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
export default function QuestionGuide({
guides,
setDropdownValue,
...props
}: {
guides: string[];
setDropdownValue?: (value: string) => void;
} & BoxProps) {
const [highlightedIndex, setHighlightedIndex] = React.useState(0);
const { appT } = useI18n();
const handleKeyDown = useCallback(
(event: any) => {
if (event.keyCode === 38) {
setHighlightedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
} else if (event.keyCode === 40) {
setHighlightedIndex((prevIndex) => Math.min(prevIndex + 1, guides.length - 1));
} else if (event.keyCode === 13 && guides[highlightedIndex]) {
setDropdownValue?.(guides[highlightedIndex]);
event.preventDefault();
}
},
[highlightedIndex, setDropdownValue, guides]
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
return guides.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={'100%'}
w={'auto'}
zIndex={99999}
maxH={'300px'}
overflow={'auto'}
className="nowheel"
{...props}
>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'} gap={2} mb={2} px={2}>
<MyIcon name={'union'} />
<Box>{appT('modules.Input Guide')}</Box>
</Flex>
{guides.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item}
px={4}
py={3}
borderRadius={'sm'}
cursor={'pointer'}
maxH={'300px'}
overflow={'auto'}
_notLast={{
mb: 1
}}
{...(highlightedIndex === index
? {
bg: 'primary.50',
color: 'primary.600'
}
: {
bg: 'myGray.50',
color: 'myGray.600'
})}
onMouseDown={(e) => {
e.preventDefault();
setDropdownValue?.(item);
}}
onMouseEnter={() => {
setHighlightedIndex(index);
}}
>
<Box fontSize={'sm'}>{item}</Box>
</Flex>
))}
</Box>
) : null;
}

View File

@ -12,12 +12,12 @@ import { ChatBoxInputFormType } from '../type.d';
const VariableInput = ({ const VariableInput = ({
appAvatar, appAvatar,
variableNodes, variableList,
chatForm, chatForm,
onSubmitVariables onSubmitVariables
}: { }: {
appAvatar?: string; appAvatar?: string;
variableNodes: VariableItemType[]; variableList: VariableItemType[];
onSubmitVariables: (e: Record<string, any>) => void; onSubmitVariables: (e: Record<string, any>) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>; chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => { }) => {
@ -40,7 +40,7 @@ const VariableInput = ({
bg={'white'} bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'} boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
> >
{variableNodes.map((item) => ( {variableList.map((item) => (
<Box key={item.id} mb={4}> <Box key={item.id} mb={4}>
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}> <Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
{item.label} {item.label}

View File

@ -45,7 +45,7 @@ import type {
ChatBoxInputType, ChatBoxInputType,
ChatBoxInputFormType ChatBoxInputFormType
} from './type.d'; } from './type.d';
import MessageInput from './MessageInput'; import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../core/chat/Divider'; import ChatBoxDivider from '../core/chat/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';
@ -59,6 +59,7 @@ import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
const ResponseTags = dynamic(() => import('./ResponseTags')); const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal')); const FeedbackModal = dynamic(() => import('./FeedbackModal'));
@ -81,7 +82,7 @@ type Props = OutLinkChatAuthProps & {
showEmptyIntro?: boolean; showEmptyIntro?: boolean;
appAvatar?: string; appAvatar?: string;
userAvatar?: string; userAvatar?: string;
userGuideModule?: StoreNodeItemType; chatConfig?: AppChatConfigType;
showFileSelector?: boolean; showFileSelector?: boolean;
active?: boolean; // can use active?: boolean; // can use
appId: string; appId: string;
@ -149,7 +150,7 @@ const ChatBox = (
const { const {
welcomeText, welcomeText,
variableNodes, variableList,
questionGuide, questionGuide,
startSegmentedAudio, startSegmentedAudio,
finishSegmentedAudio, finishSegmentedAudio,
@ -174,8 +175,8 @@ const ChatBox = (
/* variable */ /* variable */
const filterVariableNodes = useCreation( const filterVariableNodes = useCreation(
() => variableNodes.filter((item) => item.type !== VariableInputEnum.custom), () => variableList.filter((item) => item.type !== VariableInputEnum.custom),
[variableNodes] [variableList]
); );
// 滚动到底部 // 滚动到底部
@ -390,9 +391,9 @@ const ChatBox = (
return; return;
} }
// delete invalid variables 只保留在 variableNodes 中的变量 // delete invalid variables 只保留在 variableList 中的变量
const requestVariables: Record<string, any> = {}; const requestVariables: Record<string, any> = {};
variableNodes?.forEach((item) => { variableList?.forEach((item) => {
requestVariables[item.key] = variables[item.key] || ''; requestVariables[item.key] = variables[item.key] || '';
}); });
@ -566,7 +567,7 @@ const ChatBox = (
startSegmentedAudio, startSegmentedAudio,
t, t,
toast, toast,
variableNodes variableList
] ]
); );
@ -907,7 +908,7 @@ const ChatBox = (
{!!filterVariableNodes?.length && ( {!!filterVariableNodes?.length && (
<VariableInput <VariableInput
appAvatar={appAvatar} appAvatar={appAvatar}
variableNodes={filterVariableNodes} variableList={filterVariableNodes}
chatForm={chatForm} chatForm={chatForm}
onSubmitVariables={(data) => { onSubmitVariables={(data) => {
setValue('chatStarted', true); setValue('chatStarted', true);
@ -1000,7 +1001,7 @@ const ChatBox = (
</Box> </Box>
{/* message input */} {/* message input */}
{onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && ( {onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && (
<MessageInput <ChatInput
onSendMessage={sendPrompt} onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')} onStop={() => chatController.current?.abort('stop')}
TextareaDom={TextareaDom} TextareaDom={TextareaDom}

View File

@ -13,10 +13,19 @@ import Auth from './auth';
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(
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal')); () => import('@/components/support/user/team/UpdateInviteModal'),
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal')); { ssr: false }
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform')); );
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'), {
ssr: false
});
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'), {
ssr: false
});
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'), {
ssr: false
});
const pcUnShowLayoutRoute: Record<string, boolean> = { const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true, '/': true,
@ -114,12 +123,17 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</> </>
)} )}
</Box> </Box>
{!!userInfo && <UpdateInviteModal />} {feConfigs?.isPlus && (
{isNotSufficientModal && !isHideNavbar && <NotSufficientModal />} <>
{!!userInfo && <SystemMsgModal />} {!!userInfo && <UpdateInviteModal />}
{!!userInfo && importantInforms.length > 0 && ( {isNotSufficientModal && !isHideNavbar && <NotSufficientModal />}
<ImportantInform informs={importantInforms} refetch={refetchUnRead} /> {!!userInfo && <SystemMsgModal />}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)}
</>
)} )}
<Loading loading={loading} zIndex={999999} /> <Loading loading={loading} zIndex={999999} />
</> </>
); );

View File

@ -3,24 +3,28 @@ import { Flex, Input, InputProps } from '@chakra-ui/react';
interface Props extends InputProps { interface Props extends InputProps {
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
} }
const MyInput = ({ leftIcon, ...props }: Props) => { const MyInput = ({ leftIcon, rightIcon, ...props }: Props) => {
return ( return (
<Flex position={'relative'} alignItems={'center'}> <Flex h={'100%'} position={'relative'} alignItems={'center'}>
<Input w={'100%'} pl={leftIcon ? '30px !important' : 3} {...props} /> <Input
w={'100%'}
pl={leftIcon ? '34px !important' : 3}
pr={rightIcon ? '34px !important' : 3}
{...props}
/>
{leftIcon && ( {leftIcon && (
<Flex <Flex alignItems={'center'} position={'absolute'} left={3} w={'20px'} zIndex={10}>
alignItems={'center'}
position={'absolute'}
left={3}
w={'20px'}
zIndex={10}
transform={'translateY(1.5px)'}
>
{leftIcon} {leftIcon}
</Flex> </Flex>
)} )}
{rightIcon && (
<Flex alignItems={'center'} position={'absolute'} right={3} w={'20px'} zIndex={10}>
{rightIcon}
</Flex>
)}
</Flex> </Flex>
); );
}; };

View File

@ -1,479 +0,0 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import {
Box,
Button,
Flex,
ModalBody,
useDisclosure,
Switch,
Input,
Textarea,
InputGroup,
InputRightElement,
Checkbox,
useCheckboxGroup,
ModalFooter,
BoxProps
} from '@chakra-ui/react';
import React, { ChangeEvent, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import type { AppQuestionGuideTextConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useI18n } from '@/web/context/I18n';
import { fileDownload } from '@/web/common/file/utils';
import { getDocPath } from '@/web/common/system/doc';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getMyQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL } from '@/web/core/app/utils';
import { useQuery } from '@tanstack/react-query';
const csvTemplate = `"第一列内容"
"必填列"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段"
`;
const QGuidesConfig = ({
value,
onChange
}: {
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
}) => {
const { t } = useTranslation();
const { appT, commonT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isOpenTexts, onOpen: onOpenTexts, onClose: onCloseTexts } = useDisclosure();
const isOpenQuestionGuide = value.open;
const { appDetail } = useAppStore();
const [searchKey, setSearchKey] = React.useState<string>('');
const { data } = useQuery(
[appDetail._id, searchKey],
async () => {
return getMyQuestionGuides({
appId: appDetail._id,
customURL: getAppQGuideCustomURL(appDetail),
pageSize: 30,
current: 1,
searchKey
});
},
{
enabled: !!appDetail._id
}
);
useEffect(() => {
onChange({
...value,
textList: data?.list || []
});
}, [data]);
const formLabel = useMemo(() => {
if (!isOpenQuestionGuide) {
return t('core.app.whisper.Close');
}
return t('core.app.whisper.Open');
}, [t, isOpenQuestionGuide]);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Box fontWeight={'medium'}>{appT('modules.Question Guide')}</Box>
<Box flex={1} />
<MyTooltip label={appT('modules.Config question guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={appT('modules.Question Guide')}
iconSrc="core/app/inputGuides"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody px={[5, 16]} pt={[4, 8]} w={'500px'}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{appT('modules.Question Guide Switch')}
<Switch
isChecked={isOpenQuestionGuide}
size={'lg'}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenQuestionGuide && (
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Question Guide Texts')}
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{value.textList.length || 0}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={() => {
onOpenTexts();
onClose();
}}
>
{appT('modules.Config Texts')}
</Button>
</Flex>
<>
<Flex mt={8} alignItems={'center'}>
{appT('modules.Custom question guide URL')}
<Flex
onClick={() => window.open(getDocPath('/docs/course/custom_link'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} ml={4} mr={1} />
{commonT('common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<Textarea
mt={2}
bg={'myGray.50'}
defaultValue={value.customURL}
onBlur={(e) =>
onChange({
...value,
customURL: e.target.value
})
}
/>
</>
</>
)}
</ModalBody>
<ModalFooter px={[5, 16]} pb={[4, 8]}>
<Button onClick={() => onClose()}>{commonT('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
{isOpenTexts && (
<TextConfigModal
onCloseTexts={onCloseTexts}
onOpen={onOpen}
value={value}
onChange={onChange}
setSearchKey={setSearchKey}
/>
)}
</Flex>
);
};
export default React.memo(QGuidesConfig);
const TextConfigModal = ({
onCloseTexts,
onOpen,
value,
onChange,
setSearchKey
}: {
onCloseTexts: () => void;
onOpen: () => void;
value: AppQuestionGuideTextConfigType;
onChange: (e: AppQuestionGuideTextConfigType) => void;
setSearchKey: (key: string) => void;
}) => {
const { appT, commonT } = useI18n();
const fileInputRef = useRef<HTMLInputElement>(null);
const [checkboxValue, setCheckboxValue] = React.useState<string[]>([]);
const [isEditIndex, setIsEditIndex] = React.useState(-1);
const [isAdding, setIsAdding] = React.useState(false);
const [showIcons, setShowIcons] = React.useState<number | null>(null);
const { getCheckboxProps } = useCheckboxGroup();
const handleFileSelected = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
const content = e.target?.result as string;
const rows = content.split('\n');
const texts = rows.map((row) => row.split(',')[0]);
const newText = texts.filter((row) => value.textList.indexOf(row) === -1 && !!row);
onChange({
...value,
textList: [...newText, ...value.textList]
});
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
reader.readAsText(file);
}
};
const allSelected = useMemo(() => {
return value.textList.length === checkboxValue.length && value.textList.length !== 0;
}, [value.textList, checkboxValue]);
return (
<MyModal
title={appT('modules.Config Texts')}
iconSrc="core/app/inputGuides"
isOpen={true}
onClose={() => {
setCheckboxValue([]);
onCloseTexts();
onOpen();
}}
>
<ModalBody w={'500px'} px={0}>
<Flex gap={4} px={8} alignItems={'center'} borderBottom={'1px solid #E8EBF0'} pb={4}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={commonT('common.Search')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Input
type="file"
accept=".csv"
style={{ display: 'none' }}
ref={fileInputRef}
onChange={handleFileSelected}
/>
<Button
onClick={() => {
fileInputRef.current?.click();
}}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{commonT('common.Import')}
</Button>
<Box
cursor={'pointer'}
onClick={() => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
}}
>
<QuestionTip ml={-2} label={appT('modules.Only support CSV')} />
</Box>
</Flex>
<Box mt={4}>
<Flex justifyContent={'space-between'} px={8}>
<Flex alignItems={'center'}>
<Checkbox
sx={{
'.chakra-checkbox__control': {
bg: allSelected ? 'primary.50' : 'none',
boxShadow: allSelected && '0 0 0 2px #F0F4FF',
_hover: {
bg: 'primary.50'
},
border: allSelected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
value={'all'}
size={'lg'}
mr={2}
isChecked={allSelected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue(value.textList);
} else {
setCheckboxValue([]);
}
}}
/>
<Box fontSize={'sm'} color={'myGray.600'} fontWeight={'medium'}>
{commonT('common.Select all')}
</Box>
</Flex>
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={checkboxValue.length === 0 ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={() => {
setCheckboxValue([]);
onChange({
...value,
textList: value.textList.filter((_) => !checkboxValue.includes(_))
});
}}
>
{commonT('common.Delete')}
</Button>
<Button
display={checkboxValue.length !== 0 ? 'none' : 'flex'}
onClick={() => {
onChange({
...value,
textList: ['', ...value.textList]
});
setIsEditIndex(0);
setIsAdding(true);
}}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{commonT('common.Add')}
</Button>
</Flex>
</Flex>
<Box h={'400px'} pb={4} overflow={'auto'} px={8}>
{value.textList.map((text, index) => {
const selected = checkboxValue.includes(text);
return (
<Flex
key={index}
alignItems={'center'}
h={10}
mt={2}
onMouseEnter={() => setShowIcons(index)}
onMouseLeave={() => setShowIcons(null)}
>
<Checkbox
{...getCheckboxProps({ value: text })}
sx={{
'.chakra-checkbox__control': {
bg: selected ? 'primary.50' : 'none',
boxShadow: selected ? '0 0 0 2px #F0F4FF' : 'none',
_hover: {
bg: 'primary.50'
},
border: selected && '1px solid #3370FF',
color: 'primary.600'
},
svg: {
strokeWidth: '1px !important'
}
}}
size={'lg'}
mr={2}
isChecked={selected}
onChange={(e) => {
if (e.target.checked) {
setCheckboxValue([...checkboxValue, text]);
} else {
setCheckboxValue(checkboxValue.filter((_) => _ !== text));
}
}}
/>
{index === isEditIndex ? (
<InputGroup alignItems={'center'} h={'full'}>
<Input
autoFocus
h={'full'}
defaultValue={text}
onBlur={(e) => {
setIsEditIndex(-1);
if (
!e.target.value ||
(value.textList.indexOf(e.target.value) !== -1 &&
value.textList.indexOf(e.target.value) !== index)
) {
isAdding &&
onChange({
...value,
textList: value.textList.filter((_, i) => i !== index)
});
} else {
onChange({
...value,
textList: value.textList?.map((v, i) =>
i !== index ? v : e.target.value
)
});
}
setIsAdding(false);
}}
/>
<InputRightElement alignItems={'center'} pr={4} display={'flex'}>
<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />
</InputRightElement>
</InputGroup>
) : (
<Flex
h={10}
w={'full'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'1px solid #F0F1F6'}
_hover={{ border: '1px solid #94B5FF' }}
>
{text}
<Box flex={1} />
{checkboxValue.length === 0 && (
<Box display={showIcons === index ? 'flex' : 'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => setIsEditIndex(index)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => {
const temp = value.textList?.filter((_, i) => i !== index);
onChange({
...value,
textList: temp
});
}}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
})}
</Box>
</Box>
</ModalBody>
</MyModal>
);
};

View File

@ -92,20 +92,15 @@ const ScheduledTriggerConfig = ({
value, value,
onChange onChange
}: { }: {
value: AppScheduledTriggerConfigType | null; value?: AppScheduledTriggerConfigType;
onChange: (e: AppScheduledTriggerConfigType | null) => void; onChange: (e?: AppScheduledTriggerConfigType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { register, setValue, watch } = useForm<AppScheduledTriggerConfigType>({
defaultValues: { const timezone = value?.timezone;
cronString: value?.cronString || '', const cronString = value?.cronString;
timezone: value?.timezone, const defaultPrompt = value?.defaultPrompt || '';
defaultPrompt: value?.defaultPrompt || ''
}
});
const timezone = watch('timezone');
const cronString = watch('cronString');
const cronSelectList = useRef<MultipleSelectProps['list']>([ const cronSelectList = useRef<MultipleSelectProps['list']>([
{ {
@ -130,15 +125,39 @@ const ScheduledTriggerConfig = ({
} }
]); ]);
const onUpdate = useCallback(
({
cronString,
timezone,
defaultPrompt
}: {
cronString?: string;
timezone?: string;
defaultPrompt?: string;
}) => {
if (!cronString) {
onChange(undefined);
return;
}
onChange({
...value,
cronString,
timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: defaultPrompt || ''
});
},
[onChange, value]
);
/* cron string to config field */ /* cron string to config field */
const cronConfig = useMemo(() => { const cronConfig = useMemo(() => {
if (!cronString) { if (!cronString) {
return null; return;
} }
const cronField = cronParser2Fields(cronString); const cronField = cronParser2Fields(cronString);
if (!cronField) { if (!cronField) {
return null; return;
} }
if (cronField.dayOfMonth.length !== 31) { if (cronField.dayOfMonth.length !== 31) {
@ -169,19 +188,22 @@ const ScheduledTriggerConfig = ({
const cronConfig2cronString = useCallback( const cronConfig2cronString = useCallback(
(e: CronFieldType) => { (e: CronFieldType) => {
if (e[0] === CronJobTypeEnum.month) { const str = (() => {
setValue('cronString', `0 ${e[2]} ${e[1]} * *`); if (e[0] === CronJobTypeEnum.month) {
} else if (e[0] === CronJobTypeEnum.week) { return `0 ${e[2]} ${e[1]} * *`;
setValue('cronString', `0 ${e[2]} * * ${e[1]}`); } else if (e[0] === CronJobTypeEnum.week) {
} else if (e[0] === CronJobTypeEnum.day) { return `0 ${e[2]} * * ${e[1]}`;
setValue('cronString', `0 ${e[1]} * * *`); } else if (e[0] === CronJobTypeEnum.day) {
} else if (e[0] === CronJobTypeEnum.interval) { return `0 ${e[1]} * * *`;
setValue('cronString', `0 */${e[1]} * * *`); } else if (e[0] === CronJobTypeEnum.interval) {
} else { return `0 */${e[1]} * * *`;
setValue('cronString', ''); } else {
} return '';
}
})();
onUpdate({ cronString: str });
}, },
[setValue] [onUpdate]
); );
// cron config to show label // cron config to show label
@ -216,22 +238,9 @@ const ScheduledTriggerConfig = ({
return t('common.Not open'); return t('common.Not open');
}, [cronField, isOpenSchedule, t]); }, [cronField, isOpenSchedule, t]);
// update value
watch((data) => {
if (!data.cronString) {
onChange(null);
return;
}
onChange({
cronString: data.cronString,
timezone: data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: data.defaultPrompt || ''
});
});
useEffect(() => { useEffect(() => {
if (!value?.timezone) { if (!value?.timezone) {
setValue('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); onUpdate({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone });
} }
}, []); }, []);
@ -272,9 +281,9 @@ const ScheduledTriggerConfig = ({
isChecked={isOpenSchedule} isChecked={isOpenSchedule}
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
setValue('cronString', defaultCronString); onUpdate({ cronString: defaultCronString });
} else { } else {
setValue('cronString', ''); onUpdate({ cronString: '' });
} }
}} }}
/> />
@ -300,7 +309,7 @@ const ScheduledTriggerConfig = ({
<TimezoneSelect <TimezoneSelect
value={timezone} value={timezone}
onChange={(e) => { onChange={(e) => {
setValue('timezone', e); onUpdate({ timezone: e });
}} }}
/> />
</Box> </Box>
@ -308,10 +317,13 @@ const ScheduledTriggerConfig = ({
<Box mt={5}> <Box mt={5}>
<Box>{t('core.app.schedule.Default prompt')}</Box> <Box>{t('core.app.schedule.Default prompt')}</Box>
<Textarea <Textarea
{...register('defaultPrompt')} value={defaultPrompt}
rows={8} rows={8}
bg={'myGray.50'} bg={'myGray.50'}
placeholder={t('core.app.schedule.Default prompt placeholder')} placeholder={t('core.app.schedule.Default prompt placeholder')}
onChange={(e) => {
onUpdate({ defaultPrompt: e.target.value });
}}
/> />
</Box> </Box>
</> </>
@ -323,13 +335,13 @@ const ScheduledTriggerConfig = ({
}, [ }, [
cronConfig2cronString, cronConfig2cronString,
cronField, cronField,
defaultPrompt,
formatLabel, formatLabel,
isOpen, isOpen,
isOpenSchedule, isOpenSchedule,
onClose, onClose,
onOpen, onOpen,
register, onUpdate,
setValue,
t, t,
timezone timezone
]); ]);

View File

@ -11,12 +11,13 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import MySlider from '@/components/Slider'; import MySlider from '@/components/Slider';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
const TTSSelect = ({ const TTSSelect = ({
value, value = defaultTTSConfig,
onChange onChange
}: { }: {
value: AppTTSConfigType; value?: AppTTSConfigType;
onChange: (e: AppTTSConfigType) => void; onChange: (e: AppTTSConfigType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -39,10 +39,10 @@ import MyRadio from '@/components/common/MyRadio';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils'; import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
const VariableEdit = ({ const VariableEdit = ({
variables, variables = [],
onChange onChange
}: { }: {
variables: VariableItemType[]; variables?: VariableItemType[];
onChange: (data: VariableItemType[]) => void; onChange: (data: VariableItemType[]) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -6,14 +6,15 @@ import { useTranslation } from 'next-i18next';
import type { AppWhisperConfigType } from '@fastgpt/global/core/app/type.d'; import type { AppWhisperConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { defaultWhisperConfig } from '@fastgpt/global/core/app/constants';
const WhisperConfig = ({ const WhisperConfig = ({
isOpenAudio, isOpenAudio,
value, value = defaultWhisperConfig,
onChange onChange
}: { }: {
isOpenAudio: boolean; isOpenAudio: boolean;
value: AppWhisperConfigType; value?: AppWhisperConfigType;
onChange: (e: AppWhisperConfigType) => void; onChange: (e: AppWhisperConfigType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -0,0 +1,482 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import {
Box,
Button,
Flex,
ModalBody,
useDisclosure,
Switch,
Textarea,
Checkbox,
HStack
} from '@chakra-ui/react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import type { ChatInputGuideConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useI18n } from '@/web/context/I18n';
import { fileDownload } from '@/web/common/file/utils';
import { getDocPath } from '@/web/common/system/doc';
import {
delChatInputGuide,
getChatInputGuideList,
getCountChatInputGuideTotal,
postChatInputGuides,
putChatInputGuide
} from '@/web/core/chat/inputGuide/api';
import { useQuery } from '@tanstack/react-query';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { readCsvRawText } from '@fastgpt/web/common/file/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest } from 'ahooks';
import HighlightText from '@fastgpt/web/components/common/String/HighlightText';
import { defaultChatInputGuideConfig } from '@fastgpt/global/core/app/constants';
const csvTemplate = `"第一列内容"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段"`;
const InputGuideConfig = ({
appId,
value = defaultChatInputGuideConfig,
onChange
}: {
appId: string;
value?: ChatInputGuideConfigType;
onChange: (e: ChatInputGuideConfigType) => void;
}) => {
const { t } = useTranslation();
const { chatT, commonT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const {
isOpen: isOpenLexiconConfig,
onOpen: onOpenLexiconConfig,
onClose: onCloseLexiconConfig
} = useDisclosure();
const isOpenQuestionGuide = value.open;
const { data } = useQuery(
[appId, isOpenLexiconConfig],
() => {
return getCountChatInputGuideTotal({
appId
});
},
{
enabled: !!appId
}
);
const total = data?.total || 0;
const formLabel = useMemo(() => {
if (!isOpenQuestionGuide) {
return t('core.app.whisper.Close');
}
return t('core.app.whisper.Open');
}, [t, isOpenQuestionGuide]);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<HStack>
<Box>{chatT('Input guide')}</Box>
<QuestionTip label={chatT('Input guide tip')} />
</HStack>
<Box flex={1} />
<MyTooltip label={chatT('Config input guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={chatT('Input guide')}
iconSrc="core/app/inputGuides"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody px={[5, 16]} py={[4, 8]} w={'500px'}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{t('Is open')}
<Switch
isChecked={isOpenQuestionGuide}
size={'lg'}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenQuestionGuide && (
<>
<Flex mt={8} alignItems={'center'}>
{chatT('Input guide lexicon')}
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{total}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={() => {
onOpenLexiconConfig();
}}
>
{chatT('Config input guide lexicon')}
</Button>
</Flex>
<>
<Flex mt={8} alignItems={'center'}>
{chatT('Custom input guide url')}
<Flex
onClick={() => window.open(getDocPath('/docs/course/chat_input_guide'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} ml={4} mr={1} />
{commonT('common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<Textarea
mt={2}
bg={'myGray.50'}
defaultValue={value.customUrl}
onBlur={(e) =>
onChange({
...value,
customUrl: e.target.value
})
}
/>
</>
</>
)}
</ModalBody>
</MyModal>
{isOpenLexiconConfig && <LexiconConfigModal appId={appId} onClose={onCloseLexiconConfig} />}
</Flex>
);
};
export default React.memo(InputGuideConfig);
const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => void }) => {
const { chatT, commonT } = useI18n();
const { t } = useTranslation();
const { toast } = useToast();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.csv'
});
const [newData, setNewData] = useState<string>();
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [editDataId, setEditDataId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
const {
list,
setData,
ScrollList,
isLoading: isRequesting,
fetchData,
scroll2Top
} = useScrollPagination(getChatInputGuideList, {
refreshDeps: [searchKey],
debounceWait: 300,
itemHeight: 46,
overscan: 20,
pageSize: 20,
defaultParams: {
appId,
searchKey
}
});
const { run: createNewData, loading: isCreating } = useRequest2(
(textList: string[]) => {
if (textList.filter(Boolean).length === 0) {
return Promise.resolve();
}
scroll2Top();
return postChatInputGuides({
appId,
textList
}).then((res) => {
if (res.insertLength < textList.length) {
toast({
status: 'warning',
title: chatT('Insert input guide, Some data already exists', { len: res.insertLength })
});
} else {
toast({
status: 'success',
title: t('common.Add Success')
});
}
fetchData(1);
});
},
{
manual: true,
onSuccess() {
setNewData(undefined);
},
errorToast: t('error.Create failed')
}
);
const onUpdateData = ({ text, dataId }: { text: string; dataId: string }) => {
setData((state) =>
state.map((item) => {
if (item._id === dataId) {
return {
...item,
text
};
}
return item;
})
);
if (text) {
putChatInputGuide({
appId,
text,
dataId
});
}
setEditDataId(undefined);
};
const onDeleteData = (dataIdList: string[]) => {
setData((state) => state.filter((item) => !dataIdList.includes(item._id)));
delChatInputGuide({
appId,
dataIdList
});
};
const onSelectFile = async (files: File[]) => {
const file = files?.[0];
if (file) {
const list = await readCsvRawText({ file });
const textList = list.map((item) => item[0]?.trim() || '').filter(Boolean);
createNewData(textList);
}
};
const isLoading = isRequesting || isCreating;
return (
<MyModal
title={chatT('Config input guide lexicon title')}
iconSrc="core/app/inputGuides"
isOpen={true}
onClose={onClose}
isLoading={isLoading}
h={'600px'}
w={'500px'}
>
<Flex gap={4} px={8} py={4} mb={4} alignItems={'center'} borderBottom={'base'}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} color={'myGray.500'} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={commonT('common.Search')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Button
onClick={onOpenSelectFile}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{commonT('common.Import')}
</Button>
<Box
cursor={'pointer'}
onClick={() => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
}}
>
<QuestionTip ml={-2} label={chatT('Csv input lexicon tip')} />
</Box>
</Flex>
<Box px={8}>
{/* button */}
<Flex mb={1} justifyContent={'space-between'}>
<Box flex={1} />
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={selectedRows.length === 0 ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={() => {
onDeleteData(selectedRows);
setSelectedRows([]);
}}
>
{commonT('common.Delete')}
</Button>
<Button
display={selectedRows.length !== 0 ? 'none' : 'flex'}
onClick={() => {
setNewData('');
}}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{commonT('common.Add')}
</Button>
</Flex>
</Flex>
{/* new data input */}
{newData !== undefined && (
<Box mt={5} ml={list.length > 0 ? 7 : 0}>
<MyInput
autoFocus
rightIcon={<MyIcon name={'save'} w={'14px'} cursor={'pointer'} />}
placeholder={chatT('New input guide lexicon')}
onBlur={(e) => {
createNewData([e.target.value.trim()]);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
createNewData([e.currentTarget.value.trim()]);
}
}}
/>
</Box>
)}
</Box>
<ScrollList
px={8}
flex={'1 0 0'}
EmptyChildren={<EmptyTip text={chatT('Chat input guide lexicon is empty')} />}
>
{list.map((data, index) => {
const item = data.data;
const selected = selectedRows.includes(item._id);
const edited = editDataId === item._id;
return (
<Flex
key={index}
alignItems={'center'}
h={10}
mt={3}
_hover={{
'& .icon-list': {
display: 'flex'
}
}}
>
<Checkbox
size={'lg'}
mr={2}
isChecked={selected}
onChange={(e) => {
if (e.target.checked) {
setSelectedRows([...selectedRows, item._id]);
} else {
setSelectedRows(selectedRows.filter((id) => id !== item._id));
}
}}
/>
{edited ? (
<Box h={'full'} flex={'1 0 0'}>
<MyInput
autoFocus
defaultValue={item.text}
rightIcon={<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />}
onBlur={(e) => {
onUpdateData({
text: e.target.value.trim(),
dataId: item._id
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onUpdateData({
text: e.currentTarget.value.trim(),
dataId: item._id
});
}
}}
/>
</Box>
) : (
<Flex
h={'40px'}
w={0}
flex={'1 0 0'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'base'}
_hover={{ borderColor: 'primary.300' }}
>
<Box className="textEllipsis" w={0} flex={'1 0 0'}>
<HighlightText rawText={item.text} matchText={searchKey} />
</Box>
{selectedRows.length === 0 && (
<Box className="icon-list" display={'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => setEditDataId(item._id)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => onDeleteData([item._id])}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
})}
</ScrollList>
<File onSelect={onSelectFile} />
</MyModal>
);
};

View File

@ -29,7 +29,8 @@ import {
initWorkflowEdgeStatus, initWorkflowEdgeStatus,
storeNodes2RuntimeNodes storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
export type ChatTestComponentRef = { export type ChatTestComponentRef = {
resetChatTest: () => void; resetChatTest: () => void;
@ -37,13 +38,11 @@ export type ChatTestComponentRef = {
const ChatTest = ( const ChatTest = (
{ {
app,
isOpen, isOpen,
nodes = [], nodes = [],
edges = [], edges = [],
onClose onClose
}: { }: {
app: AppSchema;
isOpen: boolean; isOpen: boolean;
nodes?: StoreNodeItemType[]; nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[]; edges?: StoreEdgeItemType[];
@ -54,6 +53,7 @@ const ChatTest = (
const { t } = useTranslation(); const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const startChat = useCallback( const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => { async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
@ -70,8 +70,8 @@ const ChatTest = (
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)), nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges), edges: initWorkflowEdgeStatus(edges),
variables, variables,
appId: app._id, appId: appDetail._id,
appName: `调试-${app.name}`, appName: `调试-${appDetail.name}`,
mode: 'test' mode: 'test'
}, },
onMessage: generatingMessage, onMessage: generatingMessage,
@ -80,7 +80,7 @@ const ChatTest = (
return { responseText, responseData, newVariables }; return { responseText, responseData, newVariables };
}, },
[app._id, app.name, edges, nodes] [appDetail._id, appDetail.name, edges, nodes]
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@ -139,11 +139,11 @@ const ChatTest = (
<Box flex={1}> <Box flex={1}>
<ChatBox <ChatBox
ref={ChatBoxRef} ref={ChatBoxRef}
appId={app._id} appId={appDetail._id}
appAvatar={app.avatar} appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar} userAvatar={userInfo?.avatar}
showMarkIcon showMarkIcon
userGuideModule={getGuideModule(nodes)} chatConfig={appDetail.chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(nodes)} showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat} onStartChat={startChat}
onDelMessage={() => {}} onDelMessage={() => {}}

View File

@ -5,7 +5,7 @@ import { useCallback, useState } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils'; import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { flowNode2StoreNodes } from '../../utils'; import { uiWorkflow2StoreWorkflow } from '../../utils';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -52,7 +52,7 @@ export const useDebug = () => {
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) { if (!checkResults) {
const storeNodes = flowNode2StoreNodes({ nodes, edges }); const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return JSON.stringify(storeNodes); return JSON.stringify(storeNodes);
} else { } else {

View File

@ -39,6 +39,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context'; import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const CurlImportModal = dynamic(() => import('./CurlImportModal')); const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [ export const HttpHeaders = [
@ -251,6 +252,7 @@ export function RenderHttpProps({
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params); const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value; const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams); const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
@ -262,7 +264,11 @@ export function RenderHttpProps({
// get variable // get variable
const variables = useMemo(() => { const variables = useMemo(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t); const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon( const moduleVariables = formatEditorVariablePickerIcon(
inputs inputs

View File

@ -30,6 +30,7 @@ import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow'; import { Position, useReactFlow } from 'reactflow';
import { getReferenceDataValueType } from '@/web/core/workflow/utils'; import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon'; import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
import { AppContext } from '@/web/core/app/context/appContext';
const ListItem = ({ const ListItem = ({
provided, provided,
@ -342,15 +343,17 @@ const ConditionSelect = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
// get condition type // get condition type
const valueType = useMemo(() => { const valueType = useMemo(() => {
return getReferenceDataValueType({ return getReferenceDataValueType({
variable, variable,
nodeList, nodeList,
chatConfig: appDetail.chatConfig,
t t
}); });
}, [nodeList, t, variable]); }, [appDetail.chatConfig, nodeList, t, variable]);
const conditionList = useMemo(() => { const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList; if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;

View File

@ -1,16 +1,15 @@
import React, { useMemo, useTransition } from 'react'; import React, { Dispatch, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow'; import { NodeProps } from 'reactflow';
import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react'; import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { welcomeTextTip } from '@fastgpt/global/core/workflow/template/tip'; import { welcomeTextTip } from '@fastgpt/global/core/workflow/template/tip';
import QGSwitch from '@/components/core/app/QGSwitch'; import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect'; import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig'; import WhisperConfig from '@/components/core/app/WhisperConfig';
import QGuidesConfig from '@/components/core/app/QGuidesConfig'; import InputGuideConfig from '@/components/core/chat/appConfig/InputGuideConfig';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils'; import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants'; import { TTSTypeEnum } from '@/web/core/app/constants';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@ -19,12 +18,35 @@ import NodeCard from './render/NodeCard';
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig'; import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { VariableItemType } from '@fastgpt/global/core/app/type'; import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit'; import VariableEdit from '@/components/core/app/VariableEdit';
import { AppContext } from '@/web/core/app/context/appContext';
type ComponentProps = {
chatConfig: AppChatConfigType;
setAppDetail: Dispatch<React.SetStateAction<AppDetailType>>;
};
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme(); const theme = useTheme();
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({
chatConfig: appDetail.chatConfig,
systemConfigNode: data,
isPublicFetch: true
});
}, [data, appDetail]);
const componentsProps = useMemo(
() => ({
chatConfig,
setAppDetail
}),
[chatConfig, setAppDetail]
);
return ( return (
<> <>
@ -40,24 +62,24 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{...data} {...data}
> >
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag"> <Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
<WelcomeText data={data} /> <WelcomeText {...componentsProps} />
<Box pt={4}> <Box pt={4}>
<ChatStartVariable data={data} /> <ChatStartVariable {...componentsProps} />
</Box> </Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}> <Box mt={3} pt={3} borderTop={theme.borders.base}>
<TTSGuide data={data} /> <TTSGuide {...componentsProps} />
</Box> </Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}> <Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide data={data} /> <WhisperGuide {...componentsProps} />
</Box> </Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}> <Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} /> <QuestionGuide {...componentsProps} />
</Box> </Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}> <Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger data={data} /> <ScheduledTrigger {...componentsProps} />
</Box> </Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}> <Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionInputGuide data={data} /> <QuestionInputGuide {...componentsProps} />
</Box> </Box>
</Box> </Box>
</NodeCard> </NodeCard>
@ -67,13 +89,9 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
export default React.memo(NodeUserGuide); export default React.memo(NodeUserGuide);
function WelcomeText({ data }: { data: FlowNodeItemType }) { function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { inputs, nodeId } = data;
const [, startTst] = useTransition(); const [, startTst] = useTransition();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const welcomeText = inputs.find((item) => item.key === NodeInputKeyEnum.welcomeText);
return ( return (
<> <>
@ -84,181 +102,136 @@ function WelcomeText({ data }: { data: FlowNodeItemType }) {
<QuestionOutlineIcon display={['none', 'inline']} ml={1} /> <QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip> </MyTooltip>
</Flex> </Flex>
{welcomeText && ( <Textarea
<Textarea className="nodrag"
className="nodrag" rows={6}
rows={6} fontSize={'12px'}
fontSize={'12px'} resize={'both'}
resize={'both'} defaultValue={welcomeText}
defaultValue={welcomeText.value} bg={'myWhite.500'}
bg={'myWhite.500'} placeholder={t(welcomeTextTip)}
placeholder={t(welcomeTextTip)} onChange={(e) => {
onChange={(e) => { startTst(() => {
startTst(() => { setAppDetail((state) => ({
onChangeNode({ ...state,
nodeId, chatConfig: {
key: NodeInputKeyEnum.welcomeText, ...state.chatConfig,
type: 'updateInput', welcomeText: e.target.value
value: { }
...welcomeText, }));
value: e.target.value });
} }}
}); />
});
}}
/>
)}
</> </>
); );
} }
function ChatStartVariable({ data }: { data: FlowNodeItemType }) { function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: ComponentProps) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const variables = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const updateVariables = useMemoizedFn((value: VariableItemType[]) => { const updateVariables = useMemoizedFn((value: VariableItemType[]) => {
// update system config node setAppDetail((state) => ({
onChangeNode({ ...state,
nodeId, chatConfig: {
key: NodeInputKeyEnum.variables, ...state.chatConfig,
type: 'updateInput', variables: value
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.variables),
value
} }
}); }));
}); });
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />; return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
} }
function QuestionGuide({ data }: { data: FlowNodeItemType }) { function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return ( return (
<QGSwitch <QGSwitch
isChecked={questionGuide} isChecked={questionGuide}
size={'md'} size={'md'}
onChange={(e) => { onChange={(e) => {
const value = e.target.checked; const value = e.target.checked;
onChangeNode({ setAppDetail((state) => ({
nodeId, ...state,
key: NodeInputKeyEnum.questionGuide, chatConfig: {
type: 'updateInput', ...state.chatConfig,
value: { questionGuide: value
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide),
value
} }
}); }));
}} }}
/> />
); );
} }
function TTSGuide({ data }: { data: FlowNodeItemType }) { function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { ttsConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return ( return (
<TTSSelect <TTSSelect
value={ttsConfig} value={ttsConfig}
onChange={(e) => { onChange={(e) => {
onChangeNode({ setAppDetail((state) => ({
nodeId, ...state,
key: NodeInputKeyEnum.tts, chatConfig: {
type: 'updateInput', ...state.chatConfig,
value: { ttsConfig: e
...inputs.find((item) => item.key === NodeInputKeyEnum.tts),
value: e
} }
}); }));
}} }}
/> />
); );
} }
function WhisperGuide({ data }: { data: FlowNodeItemType }) { function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
const { inputs, nodeId } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { ttsConfig, whisperConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return ( return (
<WhisperConfig <WhisperConfig
isOpenAudio={ttsConfig.type !== TTSTypeEnum.none} isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
value={whisperConfig} value={whisperConfig}
onChange={(e) => { onChange={(e) => {
onChangeNode({ setAppDetail((state) => ({
nodeId, ...state,
key: NodeInputKeyEnum.whisper, chatConfig: {
type: 'updateInput', ...state.chatConfig,
value: { whisperConfig: e
...inputs.find((item) => item.key === NodeInputKeyEnum.whisper),
value: e
} }
}); }));
}} }}
/> />
); );
} }
function ScheduledTrigger({ data }: { data: FlowNodeItemType }) { function ScheduledTrigger({
const { inputs, nodeId } = data; chatConfig: { scheduledTriggerConfig },
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); setAppDetail
const { scheduledTriggerConfig } = splitGuideModule({ inputs } as StoreNodeItemType); }: ComponentProps) {
return ( return (
<ScheduledTriggerConfig <ScheduledTriggerConfig
value={scheduledTriggerConfig} value={scheduledTriggerConfig}
onChange={(e) => { onChange={(e) => {
onChangeNode({ setAppDetail((state) => ({
nodeId, ...state,
key: NodeInputKeyEnum.scheduleTrigger, chatConfig: {
type: 'updateInput', ...state.chatConfig,
value: { scheduledTriggerConfig: e
...inputs.find((item) => item.key === NodeInputKeyEnum.scheduleTrigger),
value: e
} }
}); }));
}} }}
/> />
); );
} }
function QuestionInputGuide({ data }: { data: FlowNodeItemType }) { function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
const { inputs, nodeId } = data; const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { questionGuideText } = splitGuideModule({ inputs } as StoreNodeItemType);
return ( return appId ? (
<QGuidesConfig <InputGuideConfig
value={questionGuideText} appId={appId}
value={chatInputGuide}
onChange={(e) => { onChange={(e) => {
onChangeNode({ setAppDetail((state) => ({
nodeId, ...state,
key: NodeInputKeyEnum.questionGuideText, chatConfig: {
type: 'updateInput', ...state.chatConfig,
value: { chatInputGuide: e
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuideText),
value: e
} }
}); }));
}} }}
/> />
); ) : null;
} }

View File

@ -32,6 +32,7 @@ import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference'; import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getReferenceDataValueType } from '@/web/core/workflow/utils'; import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data; const { inputs = [], nodeId } = data;
@ -39,6 +40,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const updateList = useMemo( const updateList = useMemo(
() => () =>
@ -85,6 +87,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const valueType = getReferenceDataValueType({ const valueType = getReferenceDataValueType({
variable: updateItem.variable, variable: updateItem.variable,
nodeList, nodeList,
chatConfig: appDetail.chatConfig,
t t
}); });

View File

@ -13,14 +13,20 @@ import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io'; import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { nodeId, outputs } = data; const { nodeId, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variablesOutputs = useCreation(() => { const variablesOutputs = useCreation(() => {
const variables = getWorkflowGlobalVariables(nodeList, t); const variables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return variables.map<FlowNodeOutputItemType>((item) => ({ return variables.map<FlowNodeOutputItemType>((item) => ({
id: item.key, id: item.key,

View File

@ -7,15 +7,21 @@ import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { AppContext } from '@/web/core/app/context/appContext';
const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => { const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable // get variable
const variables = useCreation(() => { const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t); const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon( const moduleVariables = formatEditorVariablePickerIcon(
inputs inputs

View File

@ -14,6 +14,7 @@ import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context'; import { WorkflowContext } from '@/components/core/workflow/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/web/core/app/context/appContext';
const MultipleRowSelect = dynamic( const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect') () => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
@ -98,6 +99,7 @@ export const useReference = ({
value?: any; value?: any;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowContext, (v) => v.edges);
@ -106,6 +108,7 @@ export const useReference = ({
nodeId, nodeId,
nodes: nodeList, nodes: nodeList,
edges: edges, edges: edges,
chatConfig: appDetail.chatConfig,
t t
}); });

View File

@ -5,11 +5,6 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type'; import { PromptTemplateItem } from '@fastgpt/global/core/ai/type';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/workflow/utils';
import { ModalBody } from '@chakra-ui/react'; import { ModalBody } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip'; import MyTooltip from '@/components/MyTooltip';
import { import {
@ -22,12 +17,12 @@ import PromptTemplate from '@/components/PromptTemplate';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Reference from './Reference'; import Reference from './Reference';
import { getSystemVariables } from '@/web/core/app/utils';
import ValueTypeLabel from '../../ValueTypeLabel'; import ValueTypeLabel from '../../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context'; import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const LabelStyles: BoxProps = { const LabelStyles: BoxProps = {
fontSize: ['sm', 'md'] fontSize: ['sm', 'md']
@ -52,9 +47,14 @@ const SettingQuotePrompt = (props: RenderInputProps) => {
}); });
const aiChatQuoteTemplate = watch('quoteTemplate'); const aiChatQuoteTemplate = watch('quoteTemplate');
const aiChatQuotePrompt = watch('quotePrompt'); const aiChatQuotePrompt = watch('quotePrompt');
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variables = useCreation(() => { const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t); const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return globalVariables; return globalVariables;
}, [nodeList, t]); }, [nodeList, t]);

View File

@ -7,15 +7,21 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context'; import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable // get variable
const variables = useCreation(() => { const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables(nodeList, t); const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon( const moduleVariables = formatEditorVariablePickerIcon(
inputs inputs

View File

@ -8,7 +8,6 @@ import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context'; import { WorkflowContext } from '../context';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
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';
@ -16,6 +15,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppContext } from '@/web/core/app/context/appContext';
const PublishHistoriesSlider = () => { const PublishHistoriesSlider = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -23,7 +23,7 @@ const PublishHistoriesSlider = () => {
content: t('core.workflow.publish.OnRevert version confirm') content: t('core.workflow.publish.OnRevert version confirm')
}); });
const { appDetail, setAppDetail } = useAppStore(); const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = useContextSelector(WorkflowContext, (e) => e.appId); const appId = useContextSelector(WorkflowContext, (e) => e.appId);
const setIsShowVersionHistories = useContextSelector( const setIsShowVersionHistories = useContextSelector(
WorkflowContext, WorkflowContext,
@ -73,11 +73,11 @@ const PublishHistoriesSlider = () => {
editEdges: appDetail.edges editEdges: appDetail.edges
}); });
setAppDetail({ setAppDetail((state) => ({
...appDetail, ...state,
modules: data.nodes, modules: data.nodes,
edges: data.edges edges: data.edges
}); }));
onCloseSlider(data); onCloseSlider(data);
} }

View File

@ -13,7 +13,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe'; import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useCreation, useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import React, { import React, {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
@ -32,11 +32,13 @@ import {
useEdgesState, useEdgesState,
useNodesState useNodesState
} from 'reactflow'; } from 'reactflow';
import { createContext } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants'; import { defaultRunningStatus } from './constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils'; import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; 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 { AppContext } from '@/web/core/app/context/appContext';
type OnChange<ChangesType> = (changes: ChangesType[]) => void; type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@ -83,7 +85,11 @@ type WorkflowContextType = {
toolInputs: FlowNodeInputItemType[]; toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[]; commonInputs: FlowNodeInputItemType[];
}; };
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => Promise<void>; initData: (e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig?: AppChatConfigType;
}) => Promise<void>;
// debug // debug
workflowDebugData: workflowDebugData:
@ -223,6 +229,7 @@ const WorkflowContextProvider = ({
const { appId, pluginId } = value; const { appId, pluginId } = value;
const { toast } = useToast(); const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowWrapper = useRef<HTMLDivElement>(null);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
/* edge */ /* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
@ -426,12 +433,18 @@ const WorkflowContextProvider = ({
}; };
}; };
const initData = useMemoizedFn( const initData = useMemoizedFn(async (e: Parameters<WorkflowContextType['initData']>[0]) => {
async (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => { setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []); setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
} }
); });
/* debug */ /* debug */
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>(); const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();

View File

@ -4,7 +4,7 @@ import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { type Node, type Edge } from 'reactflow'; import { type Node, type Edge } from 'reactflow';
export const flowNode2StoreNodes = ({ export const uiWorkflow2StoreWorkflow = ({
nodes, nodes,
edges edges
}: { }: {

View File

@ -15,7 +15,7 @@ import {
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.'; import { TeamContext } from '.';
import { import {

View File

@ -7,7 +7,7 @@ import {
TeamMemberStatusMap TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu'; import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.'; import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';

View File

@ -16,6 +16,7 @@ export type AppUpdateParams = {
intro?: string; intro?: string;
nodes?: AppSchema['modules']; nodes?: AppSchema['modules'];
edges?: AppSchema['edges']; edges?: AppSchema['edges'];
chatConfig?: AppSchema['chatConfig'];
permission?: AppSchema['permission']; permission?: AppSchema['permission'];
teamTags?: AppSchema['teamTags']; teamTags?: AppSchema['teamTags'];
}; };
@ -24,6 +25,7 @@ export type PostPublishAppProps = {
type: `${AppTypeEnum}`; type: `${AppTypeEnum}`;
nodes: AppSchema['modules']; nodes: AppSchema['modules'];
edges: AppSchema['edges']; edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];
}; };
export type PostRevertAppProps = { export type PostRevertAppProps = {

View File

@ -1,4 +1,4 @@
import type { AppTTSConfigType } from '@fastgpt/global/core/app/type.d'; import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.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';
@ -34,7 +34,7 @@ export type InitChatResponse = {
variables: Record<string, any>; variables: Record<string, any>;
history: ChatItemType[]; history: ChatItemType[];
app: { app: {
userGuideModule?: StoreNodeItemType; chatConfig?: AppChatConfigType;
chatModels?: string[]; chatModels?: string[];
name: string; name: string;
avatar: string; avatar: string;

View File

@ -6,7 +6,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user'; import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit'; import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {

View File

@ -5,9 +5,9 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string }; const { appId } = req.query as { appId: string };
@ -47,7 +47,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}, },
{ session } { session }
); );
await MongoAppQGuide.deleteMany( await MongoChatInputGuide.deleteMany(
{ {
appId appId
}, },

View File

@ -1,6 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';

View File

@ -1,20 +1,20 @@
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user'; import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema'; import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import axios from 'axios'; import axios from 'axios';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { textList = [], appId, customURL } = req.body; const { textList = [], appId, customUrl } = req.body;
if (!customURL) { if (!customUrl) {
const { teamId } = await authUserNotVisitor({ req, authToken: true }); const { teamId } = await authUserNotVisitor({ req, authToken: true });
const currentQGuide = await MongoAppQGuide.find({ appId, teamId }); const currentQGuide = await MongoChatInputGuide.find({ appId, teamId });
const currentTexts = currentQGuide.map((item) => item.text); const currentTexts = currentQGuide.map((item) => item.text);
const textsToDelete = currentTexts.filter((text) => !textList.includes(text)); const textsToDelete = currentTexts.filter((text) => !textList.includes(text));
await MongoAppQGuide.deleteMany({ text: { $in: textsToDelete }, appId, teamId }); await MongoChatInputGuide.deleteMany({ text: { $in: textsToDelete }, appId, teamId });
const newTexts = textList.filter((text: string) => !currentTexts.includes(text)); const newTexts = textList.filter((text: string) => !currentTexts.includes(text));
@ -24,10 +24,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
teamId: teamId teamId: teamId
})); }));
await MongoAppQGuide.insertMany(newDocuments); await MongoChatInputGuide.insertMany(newDocuments);
} else { } else {
try { try {
const response = await axios.post(customURL, { const response = await axios.post(customUrl, {
textList, textList,
appId appId
}); });

View File

@ -1,48 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoAppQGuide } from '@fastgpt/service/core/app/qGuideSchema';
import axios from 'axios';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
import { NextAPI } from '@/service/middleware/entry';
type Props = PaginationProps<{
appId: string;
customURL: string;
searchKey: string;
}>;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId, customURL, current, pageSize, searchKey } = req.query as unknown as Props;
if (!customURL) {
const [result, total] = await Promise.all([
MongoAppQGuide.find({
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
})
.sort({
time: -1
})
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoAppQGuide.countDocuments({ appId })
]);
return {
list: result.map((item) => item.text) || [],
total
};
} else {
try {
const response = await axios.get(customURL as string, {
params: {
appid: appId
}
});
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({ error });
}
}
}
export default NextAPI(handler);

View File

@ -7,7 +7,7 @@ import { NextAPI } from '@/service/middleware/entry';
/* 获取我的模型 */ /* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { name, avatar, type, intro, nodes, edges, permission, teamTags } = const { name, avatar, type, intro, nodes, edges, chatConfig, permission, teamTags } =
req.body as AppUpdateParams; req.body as AppUpdateParams;
const { appId } = req.query as { appId: string }; const { appId } = req.query as { appId: string };
@ -39,7 +39,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}), }),
...(edges && { ...(edges && {
edges edges
}) }),
...(chatConfig && { chatConfig })
} }
); );
} }

View File

@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';

View File

@ -1,11 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller'; import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time'; import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api'; import { PostPublishAppProps } from '@/global/core/app/api';
@ -13,14 +12,12 @@ type Response = {};
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> { async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
const { appId } = req.query as { appId: string }; const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], type } = req.body as PostPublishAppProps; const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
await authApp({ appId, req, per: 'w', authToken: true }); await authApp({ appId, req, per: 'w', authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes }); const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
const { scheduledTriggerConfig } = splitGuideModule(getGuideModule(formatNodes || []));
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
// create version histories // create version histories
await MongoAppVersion.create( await MongoAppVersion.create(
@ -28,7 +25,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
{ {
appId, appId,
nodes: formatNodes, nodes: formatNodes,
edges edges,
chatConfig
} }
], ],
{ session } { session }
@ -38,12 +36,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
await MongoApp.findByIdAndUpdate(appId, { await MongoApp.findByIdAndUpdate(appId, {
modules: formatNodes, modules: formatNodes,
edges, edges,
chatConfig,
updateTime: new Date(), updateTime: new Date(),
version: 'v2', version: 'v2',
type, type,
scheduledTriggerConfig, scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: scheduledTriggerConfig scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig) ? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null : null
}); });
}); });

View File

@ -1,11 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller'; import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time'; import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostRevertAppProps } from '@/global/core/app/api'; import { PostRevertAppProps } from '@/global/core/app/api';
@ -28,7 +27,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
const { nodes: formatEditNodes } = beforeUpdateAppFormat({ nodes: editNodes }); const { nodes: formatEditNodes } = beforeUpdateAppFormat({ nodes: editNodes });
const { scheduledTriggerConfig } = splitGuideModule(getGuideModule(version.nodes)); const scheduledTriggerConfig = version.chatConfig.scheduledTriggerConfig;
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
// 为编辑中的数据创建一个版本 // 为编辑中的数据创建一个版本

View File

@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d'; import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
@ -61,10 +61,12 @@ async function handler(
variables: chat?.variables || {}, variables: chat?.variables || {},
history, history,
app: { app: {
userGuideModule: replaceAppChatConfig({ chatConfig: getAppChatConfig({
node: getGuideModule(nodes), chatConfig: app.chatConfig,
variableList: chat?.variableList, systemConfigNode: getGuideModule(nodes),
welcomeText: chat?.welcomeText storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}), }),
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,

View File

@ -0,0 +1,30 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export type countChatInputGuideTotalQuery = { appId: string };
export type countChatInputGuideTotalBody = {};
export type countChatInputGuideTotalResponse = { total: number };
async function handler(
req: ApiRequestProps<countChatInputGuideTotalBody, countChatInputGuideTotalQuery>,
res: ApiResponseType<any>
): Promise<countChatInputGuideTotalResponse> {
await authCert({ req, authToken: true });
const appId = req.query.appId;
if (!appId) {
return {
total: 0
};
}
return {
total: await MongoChatInputGuide.countDocuments({ appId })
};
}
export default NextAPI(handler);

View File

@ -0,0 +1,45 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
export type createChatInputGuideQuery = {};
export type createInputGuideBody = {
appId: string;
textList: string[];
};
export type createInputGuideResponse = {
insertLength: number;
};
async function handler(
req: ApiRequestProps<createInputGuideBody, createChatInputGuideQuery>,
res: ApiResponseType<any>
): Promise<createInputGuideResponse> {
const { appId, textList } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
try {
const result = await MongoChatInputGuide.insertMany(
textList.map((text) => ({
appId,
text
})),
{
ordered: false
}
);
return {
insertLength: result.length
};
} catch (error: any) {
const errLength = error.writeErrors?.length ?? textList.length;
return {
insertLength: textList.length - errLength
};
}
}
export default NextAPI(handler);

View File

@ -0,0 +1,27 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
export type deleteChatInputGuideQuery = {};
export type deleteInputGuideBody = { appId: string; dataIdList: string[] };
export type deleteInputGuideResponse = {};
async function handler(
req: ApiRequestProps<deleteInputGuideBody, deleteChatInputGuideQuery>,
res: ApiResponseType<any>
): Promise<deleteInputGuideResponse> {
const { appId, dataIdList } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
console.log(dataIdList);
await MongoChatInputGuide.deleteMany({
_id: { $in: dataIdList },
appId
});
return {};
}
export default NextAPI(handler);

View File

@ -0,0 +1,42 @@
import type { NextApiResponse } from 'next';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
export type ChatInputGuideProps = PaginationProps<{
appId: string;
searchKey: string;
}>;
export type ChatInputGuideResponse = PaginationResponse<ChatInputGuideSchemaType>;
async function handler(
req: ApiRequestProps<{}, ChatInputGuideProps>,
res: NextApiResponse<any>
): Promise<ChatInputGuideResponse> {
const { appId, pageSize, current, searchKey } = req.query;
await authApp({ req, appId, authToken: true, per: 'r' });
const params = {
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
};
const [result, total] = await Promise.all([
MongoChatInputGuide.find(params)
.sort({ _id: -1 })
.skip(pageSize * (current - 1))
.limit(pageSize),
MongoChatInputGuide.countDocuments(params)
]);
return {
list: result,
total
};
}
export default NextAPI(handler);

View File

@ -0,0 +1,34 @@
import type { NextApiResponse } from 'next';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
export type QueryChatInputGuideProps = {
appId: string;
searchKey: string;
};
export type QueryChatInputGuideResponse = string[];
async function handler(
req: ApiRequestProps<{}, QueryChatInputGuideProps>,
res: NextApiResponse<any>
): Promise<QueryChatInputGuideResponse> {
const { appId, searchKey } = req.query;
await authApp({ req, appId, authToken: true, authApiKey: true, per: 'r' });
const params = {
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
};
const result = await MongoChatInputGuide.find(params).sort({ _id: -1 }).limit(6);
return result
.map((item) => item.text)
.filter(Boolean)
.filter((item) => item !== searchKey);
}
export default NextAPI(handler);

View File

@ -0,0 +1,36 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
export type updateChatInputGuideQuery = {};
export type updateInputGuideBody = {
appId: string;
dataId: string;
text: string;
};
export type updateInputGuideResponse = {};
async function handler(
req: ApiRequestProps<updateInputGuideBody, updateChatInputGuideQuery>,
res: ApiResponseType<any>
): Promise<updateInputGuideResponse> {
const { appId, dataId, text } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
await MongoChatInputGuide.findOneAndUpdate(
{
_id: dataId,
appId
},
{
text
}
);
return {};
}
export default NextAPI(handler);

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d'; import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getChatItems } from '@fastgpt/service/core/chat/controller'; import { getChatItems } from '@fastgpt/service/core/chat/controller';
@ -72,10 +72,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {}, variables: chat?.variables || {},
history, history,
app: { app: {
userGuideModule: replaceAppChatConfig({ chatConfig: getAppChatConfig({
node: getGuideModule(nodes), chatConfig: app.chatConfig,
variableList: chat?.variableList, systemConfigNode: getGuideModule(nodes),
welcomeText: chat?.welcomeText storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}), }),
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,

View File

@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api.d'; import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api.d';
@ -73,10 +73,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {}, variables: chat?.variables || {},
history, history,
app: { app: {
userGuideModule: replaceAppChatConfig({ chatConfig: getAppChatConfig({
node: getGuideModule(nodes), chatConfig: app.chatConfig,
variableList: chat?.variableList, systemConfigNode: getGuideModule(nodes),
welcomeText: chat?.welcomeText storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}), }),
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,

View File

@ -1,82 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { appId, chatId, loadCustomFeedbacks } = req.query as InitChatProps;
if (!appId) {
return jsonRes(res, {
code: 501,
message: "You don't have an app yet"
});
}
// auth app permission
const [{ app, tmbId }, chat] = await Promise.all([
authApp({
req,
authToken: true,
appId,
per: 'r'
}),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// // auth chat permission
// if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
// throw new Error(ChatErrEnum.unAuthChat);
// }
// get app and history
const { history } = await getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
});
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@ -26,7 +26,7 @@ async function handler(
}); });
const [rebuildingCount, trainingCount] = await Promise.all([ const [rebuildingCount, trainingCount] = await Promise.all([
MongoDatasetData.countDocuments({ teamId, datasetId, rebuilding: true }), MongoDatasetData.countDocuments({ rebuilding: true, teamId, datasetId }),
MongoDatasetTraining.countDocuments({ teamId, datasetId }) MongoDatasetTraining.countDocuments({ teamId, datasetId })
]); ]);

View File

@ -7,7 +7,6 @@ import { getAIApi } from '@fastgpt/service/core/ai/config';
import { pushWhisperUsage } from '@/service/support/wallet/usage/push'; import { pushWhisperUsage } from '@/service/support/wallet/usage/push';
import { authChatCert } from '@/service/support/permission/auth/chat'; import { authChatCert } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
@ -47,14 +46,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
// auth role // auth role
const { teamId, tmbId } = await authChatCert({ req, authToken: true }); const { teamId, tmbId } = await authChatCert({ req, authToken: true });
// auth app // auth app
const app = await MongoApp.findById(appId, 'modules').lean(); // const app = await MongoApp.findById(appId, 'modules').lean();
if (!app) { // if (!app) {
throw new Error('app not found'); // throw new Error('app not found');
} // }
const { whisperConfig } = splitGuideModule(getGuideModule(app?.modules)); // if (!whisperConfig?.open) {
if (!whisperConfig?.open) { // throw new Error('Whisper is not open in the app');
throw new Error('Whisper is not open in the app'); // }
}
const ai = getAIApi(); const ai = getAIApi();

View File

@ -170,7 +170,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// 1. get and concat history; 2. get app workflow // 1. get and concat history; 2. get app workflow
const limit = getMaxHistoryLimitFromNodes(app.modules); const limit = getMaxHistoryLimitFromNodes(app.modules);
const [{ history }, { nodes, edges }] = await Promise.all([ const [{ history }, { nodes, edges, chatConfig }] = await Promise.all([
getChatItems({ getChatItems({
appId: app._id, appId: app._id,
chatId, chatId,
@ -249,6 +249,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId, teamId,
tmbId: tmbId, tmbId: tmbId,
nodes, nodes,
appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
shareId, shareId,

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react'; import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData'; import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
@ -9,8 +8,7 @@ import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest'; import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest';
import { flowNode2StoreNodes } from '@/components/core/workflow/utils'; import { uiWorkflow2StoreWorkflow } from '@/components/core/workflow/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
@ -27,19 +25,16 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context'; import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval, useUpdateEffect } from 'ahooks'; import { useInterval, useUpdateEffect } from 'ahooks';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils'; import { AppContext } from '@/web/core/app/context/appContext';
import { importQuestionGuides } from '@/web/core/app/api';
import { getAppQGuideCustomURL, getNodesWithNoQGuide } from '@/web/core/app/utils';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings')); const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic( const PublishHistories = dynamic(
() => import('@/components/core/workflow/components/PublishHistoriesSlider') () => import('@/components/core/workflow/components/PublishHistoriesSlider')
); );
type Props = { app: AppSchema; onClose: () => void }; type Props = { onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
app,
ChatTestRef, ChatTestRef,
setWorkflowTestData, setWorkflowTestData,
onClose onClose
@ -55,7 +50,9 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
> >
>; >;
}) { }) {
const isV2Workflow = app?.version === 'v2'; const { appDetail } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const theme = useTheme(); const theme = useTheme();
const { toast } = useToast(); const { toast } = useToast();
@ -66,7 +63,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({ const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
content: t('core.app.Publish Confirm') content: t('core.app.Publish Confirm')
}); });
const { publishApp, updateAppDetail } = useAppStore(); const { publishApp, updateAppDetail } = useContextSelector(AppContext, (v) => v);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -90,7 +87,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) { if (!checkResults) {
const storeNodes = flowNode2StoreNodes({ nodes, edges }); const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return storeNodes; return storeNodes;
} else { } else {
@ -112,12 +109,13 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
if (nodes.length === 0) return null; if (nodes.length === 0) return null;
setIsSaving(true); setIsSaving(true);
const storeWorkflow = flowNode2StoreNodes({ nodes, edges }); const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
try { try {
await updateAppDetail(app._id, { await updateAppDetail({
...storeWorkflow, ...storeWorkflow,
type: AppTypeEnum.advanced, type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore //@ts-ignore
version: 'v2' version: 'v2'
}); });
@ -134,7 +132,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
return null; return null;
}, },
[isV2Workflow, isShowVersionHistories, edges, updateAppDetail, app._id, t] [isV2Workflow, isShowVersionHistories, edges, updateAppDetail, appDetail.chatConfig, t]
); );
const onclickPublish = useCallback(async () => { const onclickPublish = useCallback(async () => {
@ -142,19 +140,10 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const data = await flowData2StoreDataAndCheck(); const data = await flowData2StoreDataAndCheck();
if (data) { if (data) {
try { try {
const { questionGuideText } = splitGuideModule(getGuideModule(data.nodes)); await publishApp({
await importQuestionGuides({
appId: app._id,
textList: questionGuideText.textList,
customURL: getAppQGuideCustomURL(app)
});
const newNodes = getNodesWithNoQGuide(data.nodes, questionGuideText);
await publishApp(app._id, {
...data, ...data,
nodes: newNodes,
type: AppTypeEnum.advanced, type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore //@ts-ignore
version: 'v2' version: 'v2'
}); });
@ -172,7 +161,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
} }
setIsSaving(false); setIsSaving(false);
}, [flowData2StoreDataAndCheck, publishApp, app._id, toast, t, ChatTestRef]); }, [flowData2StoreDataAndCheck, publishApp, appDetail.chatConfig, toast, t, ChatTestRef]);
const saveAndBack = useCallback(async () => { const saveAndBack = useCallback(async () => {
try { try {
@ -188,7 +177,8 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
JSON.stringify( JSON.stringify(
{ {
nodes: filterSensitiveNodesData(data.nodes), nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges edges: data.edges,
chatConfig: appDetail.chatConfig
}, },
null, null,
2 2
@ -196,7 +186,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
appT('Export Config Successful') appT('Export Config Successful')
); );
} }
}, [appT, copyData, flowData2StoreDataAndCheck]); }, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
// effect // effect
useBeforeunload({ useBeforeunload({
@ -205,7 +195,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
}); });
useInterval(() => { useInterval(() => {
if (!app._id) return; if (!appDetail._id) return;
onclickSave(!!workflowDebugData); onclickSave(!!workflowDebugData);
}, 20000); }, 20000);
@ -235,7 +225,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
/> />
<Box ml={[2, 4]}> <Box ml={[2, 4]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'}> <Box fontSize={['md', 'lg']} fontWeight={'bold'}>
{app.name} {appDetail.name}
</Box> </Box>
{!isShowVersionHistories && isV2Workflow && ( {!isShowVersionHistories && isV2Workflow && (
<MyTooltip label={t('core.app.Onclick to save')}> <MyTooltip label={t('core.app.Onclick to save')}>
@ -327,7 +317,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
theme.borders.base, theme.borders.base,
isSaving, isSaving,
saveAndBack, saveAndBack,
app.name, appDetail.name,
isShowVersionHistories, isShowVersionHistories,
isV2Workflow, isV2Workflow,
t, t,
@ -354,7 +344,6 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
}); });
const Header = (props: Props) => { const Header = (props: Props) => {
const { app } = props;
const ChatTestRef = useRef<ChatTestComponentRef>(null); const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [workflowTestData, setWorkflowTestData] = useState<{ const [workflowTestData, setWorkflowTestData] = useState<{
@ -374,13 +363,7 @@ const Header = (props: Props) => {
ChatTestRef={ChatTestRef} ChatTestRef={ChatTestRef}
setWorkflowTestData={setWorkflowTestData} setWorkflowTestData={setWorkflowTestData}
/> />
<ChatTest <ChatTest ref={ChatTestRef} isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
ref={ChatTestRef}
isOpen={isOpenTest}
{...workflowTestData}
app={app}
onClose={onCloseTest}
/>
</> </>
); );
}; };

View File

@ -7,11 +7,15 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt'; import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import WorkflowContextProvider, { WorkflowContext } from '@/components/core/workflow/context'; import WorkflowContextProvider, { WorkflowContext } from '@/components/core/workflow/context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { useMount } from 'ahooks';
type Props = { app: AppSchema; onClose: () => void }; type Props = { onClose: () => void };
const Render = ({ app, onClose }: Props) => { const Render = ({ onClose }: Props) => {
const isV2Workflow = app?.version === 'v2'; const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const isV2Workflow = appDetail?.version === 'v2';
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false, showCancel: false,
content: content:
@ -21,26 +25,23 @@ const Render = ({ app, onClose }: Props) => {
const initData = useContextSelector(WorkflowContext, (v) => v.initData); const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const workflowStringData = JSON.stringify({ const workflowStringData = JSON.stringify({
nodes: app.modules || [], nodes: appDetail.modules || [],
edges: app.edges || [] edges: appDetail.edges || []
}); });
useEffect(() => { useMount(() => {
if (!isV2Workflow) return;
initData(JSON.parse(workflowStringData));
}, [isV2Workflow, initData, app._id, workflowStringData]);
useEffect(() => {
if (!isV2Workflow) { if (!isV2Workflow) {
openConfirm(() => { openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((app.modules || []) as any)))); initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})(); })();
} else {
initData(JSON.parse(workflowStringData));
} }
}, [app.modules, initData, isV2Workflow, openConfirm]); });
const memoRender = useMemo(() => { const memoRender = useMemo(() => {
return <Flow Header={<Header app={app} onClose={onClose} />} />; return <Flow Header={<Header onClose={onClose} />} />;
}, [app, onClose]); }, [onClose]);
return ( return (
<> <>
@ -51,12 +52,13 @@ const Render = ({ app, onClose }: Props) => {
}; };
export default React.memo(function FlowEdit(props: Props) { export default React.memo(function FlowEdit(props: Props) {
const filterAppIds = useMemo(() => [props.app._id], [props.app._id]); const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const filterAppIds = useMemo(() => [appDetail._id], [appDetail._id]);
return ( return (
<WorkflowContextProvider <WorkflowContextProvider
value={{ value={{
appId: props.app._id, appId: appDetail._id,
mode: 'app', mode: 'app',
filterAppIds, filterAppIds,
basicNodeTemplates: appSystemModuleTemplates basicNodeTemplates: appSystemModuleTemplates

View File

@ -19,10 +19,11 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import PermissionRadio from '@/components/support/permission/Radio'; import PermissionRadio from '@/components/support/permission/Radio';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const InfoModal = ({ const InfoModal = ({
defaultApp, defaultApp,
@ -35,7 +36,7 @@ const InfoModal = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { updateAppDetail } = useAppStore(); const { updateAppDetail } = useContextSelector(AppContext, (v) => v);
const { File, onOpen: onOpenSelectFile } = useSelectFile({ const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png', fileType: '.jpg,.png',
@ -55,7 +56,7 @@ const InfoModal = ({
// submit config // submit config
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({ const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
mutationFn: async (data: AppSchema) => { mutationFn: async (data: AppSchema) => {
await updateAppDetail(data._id, { await updateAppDetail({
name: data.name, name: data.name,
avatar: data.avatar, avatar: data.avatar,
intro: data.intro, intro: data.intro,

View File

@ -335,7 +335,7 @@ const DetailLogsModal = ({
feedbackType={'admin'} feedbackType={'admin'}
showMarkIcon showMarkIcon
showVoiceIcon={false} showVoiceIcon={false}
userGuideModule={chat?.app?.userGuideModule} chatConfig={chat?.app?.chatConfig}
appId={appId} appId={appId}
chatId={chatId} chatId={chatId}
/> />

View File

@ -8,7 +8,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { AppSchema } from '@fastgpt/global/core/app/type.d'; import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { delModelById } from '@/web/core/app/api'; import { delModelById } from '@/web/core/app/api';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import PermissionIconText from '@/components/support/permission/IconText'; import PermissionIconText from '@/components/support/permission/IconText';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
@ -16,6 +15,8 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from './TagsEditModal'; import TagsEditModal from './TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const InfoModal = dynamic(() => import('../InfoModal')); const InfoModal = dynamic(() => import('../InfoModal'));
const AppCard = ({ appId }: { appId: string }) => { const AppCard = ({ appId }: { appId: string }) => {
@ -24,7 +25,7 @@ const AppCard = ({ appId }: { appId: string }) => {
const { appT } = useI18n(); const { appT } = useI18n();
const { toast } = useToast(); const { toast } = useToast();
const { appDetail } = useAppStore(); const { appDetail } = useContextSelector(AppContext, (v) => v);
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>(); const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>(); const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();

View File

@ -16,12 +16,13 @@ import {
initWorkflowEdgeStatus, initWorkflowEdgeStatus,
storeNodes2RuntimeNodes storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { useCreation, useMemoizedFn, useSafeState } from 'ahooks'; import { useMemoizedFn, useSafeState } from 'ahooks';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { form2AppWorkflow } from '@/web/core/app/utils'; import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
const ChatTest = ({ const ChatTest = ({
editForm, editForm,
@ -35,18 +36,15 @@ const ChatTest = ({
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
const { appDetail } = useAppStore(); const { appDetail } = useContextSelector(AppContext, (v) => v);
const { watch } = editForm; const { watch } = editForm;
const chatConfig = watch('chatConfig');
const [workflowData, setWorkflowData] = useSafeState({ const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [], nodes: appDetail.modules || [],
edges: appDetail.edges || [] edges: appDetail.edges || []
}); });
const userGuideModule = useCreation(
() => getGuideModule(workflowData.nodes),
[workflowData.nodes]
);
const startChat = useMemoizedFn( const startChat = useMemoizedFn(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => { async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
@ -131,7 +129,7 @@ const ChatTest = ({
appAvatar={appDetail.avatar} appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar} userAvatar={userInfo?.avatar}
showMarkIcon showMarkIcon
userGuideModule={userGuideModule} chatConfig={chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(workflowData.nodes)} showFileSelector={checkChatSupportSelectFileByModules(workflowData.nodes)}
onStartChat={startChat} onStartChat={startChat}
onDelMessage={() => {}} onDelMessage={() => {}}

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