diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml index ef78f3cf06ec..be5e4e7df927 100644 --- a/.vscode/i18n-ally-custom-framework.yml +++ b/.vscode/i18n-ally-custom-framework.yml @@ -25,6 +25,7 @@ usageMatchRegex: - "[^\\w\\d]publishT\\(['\"`]({key})['\"`]" - "[^\\w\\d]workflowT\\(['\"`]({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 # and works like how the i18next framework identifies the namespace scope from the diff --git a/docSite/content/docs/course/chat_input_guide.md b/docSite/content/docs/course/chat_input_guide.md new file mode 100644 index 000000000000..cf4b9d430016 --- /dev/null +++ b/docSite/content/docs/course/chat_input_guide.md @@ -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 - 搜索关键字 \ No newline at end of file diff --git a/docSite/content/docs/course/custom_link.md b/docSite/content/docs/course/custom_link.md deleted file mode 100644 index 6546d244de33..000000000000 --- a/docSite/content/docs/course/custom_link.md +++ /dev/null @@ -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'] - } -``` -接口应当按照获取的数据格式存储相对应的词库数组 - diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 93b6f3ada4b3..136d0439dac9 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -1,4 +1,4 @@ -import { AppWhisperConfigType } from './type'; +import { AppTTSConfigType, AppWhisperConfigType } from './type'; export enum AppTypeEnum { simple = 'simple', @@ -13,14 +13,16 @@ export const AppTypeMap = { } }; +export const defaultTTSConfig: AppTTSConfigType = { type: 'web' }; + export const defaultWhisperConfig: AppWhisperConfigType = { open: false, autoSend: false, autoTTSResponse: false }; -export const defaultQuestionGuideTextConfig = { +export const defaultChatInputGuideConfig = { open: false, textList: [], - customURL: '' + customUrl: '' }; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 5c30ea6c9c83..5c8487b839e5 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -8,7 +8,7 @@ import { DatasetSearchModeEnum } from '../dataset/constants'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; import { StoreEdgeItemType } from '../workflow/type/edge'; -export interface AppSchema { +export type AppSchema = { _id: string; teamId: string; tmbId: string; @@ -23,13 +23,14 @@ export interface AppSchema { edges: StoreEdgeItemType[]; // App system config + chatConfig: AppChatConfigType; scheduledTriggerConfig?: AppScheduledTriggerConfigType | null; scheduledTriggerNextTime?: Date; permission: `${PermissionTypeEnum}`; inited?: boolean; teamTags: string[]; -} +}; export type AppListItemType = { _id: string; @@ -66,33 +67,19 @@ export type AppSimpleEditFormType = { datasetSearchExtensionBg?: string; }; selectedTools: FlowNodeTemplateType[]; - userGuide: { - 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; - }; + chatConfig: AppChatConfigType; }; -/* 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 = { model: string; temperature: number; @@ -125,10 +112,9 @@ export type AppWhisperConfigType = { autoTTSResponse: boolean; }; // question guide text -export type AppQuestionGuideTextConfigType = { +export type ChatInputGuideConfigType = { open: boolean; - textList: string[]; - customURL: string; + customUrl: string; }; // interval timer export type AppScheduledTriggerConfigType = { diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts index 28ae43c56b40..fc4dcbc1da4d 100644 --- a/packages/global/core/app/utils.ts +++ b/packages/global/core/app/utils.ts @@ -1,50 +1,42 @@ -import type { AppSimpleEditFormType } from '../app/type'; +import type { AppChatConfigType, AppSimpleEditFormType } from '../app/type'; import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { NodeInputKeyEnum, FlowNodeTemplateTypeEnum } from '../workflow/constants'; 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 { DatasetSearchModeEnum } from '../dataset/constants'; -import { defaultQuestionGuideTextConfig, defaultWhisperConfig } from './constants'; -export const getDefaultAppForm = (): AppSimpleEditFormType => { - return { - aiSettings: { - model: 'gpt-3.5-turbo', - systemPrompt: '', - temperature: 0, - isResponseAnswerText: true, - maxHistories: 6, - maxToken: 4000 - }, - dataset: { - datasets: [], - similarity: 0.4, - limit: 1500, - searchMode: DatasetSearchModeEnum.embedding, - usingReRank: false, - datasetSearchUsingExtensionQuery: true, - datasetSearchExtensionBg: '' - }, - selectedTools: [], - userGuide: { - welcomeText: '', - variables: [], - questionGuide: false, - tts: { - type: 'web' - }, - whisper: defaultWhisperConfig, - scheduleTrigger: null, - questionGuideText: defaultQuestionGuideTextConfig - } - }; -}; +export const getDefaultAppForm = (): AppSimpleEditFormType => ({ + aiSettings: { + model: 'gpt-3.5-turbo', + systemPrompt: '', + temperature: 0, + isResponseAnswerText: true, + maxHistories: 6, + maxToken: 4000 + }, + dataset: { + datasets: [], + similarity: 0.4, + limit: 1500, + searchMode: DatasetSearchModeEnum.embedding, + usingReRank: false, + datasetSearchUsingExtensionQuery: true, + datasetSearchExtensionBg: '' + }, + selectedTools: [], + chatConfig: {} +}); /* 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 findInputValueByKey = (inputs: FlowNodeInputItemType[], key: string) => { return inputs.find((item) => item.key === key)?.value; }; @@ -103,26 +95,6 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => { node.inputs, 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) { if (!node.pluginId) return; @@ -139,6 +111,12 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => { outputs: node.outputs, templateType: FlowNodeTemplateTypeEnum.other }); + } else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) { + defaultAppForm.chatConfig = getAppChatConfig({ + chatConfig, + systemConfigNode: node, + isPublicFetch: true + }); } }); diff --git a/packages/global/core/app/version.d.ts b/packages/global/core/app/version.d.ts index 152014e7a8b8..4b3c9920bb67 100644 --- a/packages/global/core/app/version.d.ts +++ b/packages/global/core/app/version.d.ts @@ -1,5 +1,6 @@ import { StoreNodeItemType } from '../workflow/type'; import { StoreEdgeItemType } from '../workflow/type/edge'; +import { AppChatConfigType } from './type'; export type AppVersionSchemaType = { _id: string; @@ -7,4 +8,5 @@ export type AppVersionSchemaType = { time: Date; nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[]; + chatConfig: AppChatConfigType; }; diff --git a/packages/global/core/chat/inputGuide/type.d.ts b/packages/global/core/chat/inputGuide/type.d.ts new file mode 100644 index 000000000000..c67b3c5f0171 --- /dev/null +++ b/packages/global/core/chat/inputGuide/type.d.ts @@ -0,0 +1,5 @@ +export type ChatInputGuideSchemaType = { + _id: string; + appId: string; + text: string; +}; diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index f8e5f6d7de7d..2566bcc6f112 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -10,7 +10,7 @@ import { import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { NodeOutputKeyEnum } from '../workflow/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 { DatasetSearchModeEnum } from '../dataset/constants'; import { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type'; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 46a269305920..e789d71175ad 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -45,7 +45,7 @@ export enum NodeInputKeyEnum { whisper = 'whisper', variables = 'variables', scheduleTrigger = 'scheduleTrigger', - questionGuideText = 'questionGuideText', + chatInputGuide = 'chatInputGuide', // entry userChatInput = 'userChatInput', diff --git a/packages/global/core/workflow/template/system/globalVariable.ts b/packages/global/core/workflow/template/system/globalVariable.ts deleted file mode 100644 index 59053487710e..000000000000 --- a/packages/global/core/workflow/template/system/globalVariable.ts +++ /dev/null @@ -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 - })) - }; -}; diff --git a/packages/global/core/workflow/template/system/systemConfig.ts b/packages/global/core/workflow/template/system/systemConfig.ts index 4357746d4397..fbaffe118dd6 100644 --- a/packages/global/core/workflow/template/system/systemConfig.ts +++ b/packages/global/core/workflow/template/system/systemConfig.ts @@ -1,10 +1,6 @@ -import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../node/constant'; +import { FlowNodeTypeEnum } from '../../node/constant'; import { FlowNodeTemplateType } from '../../type/index.d'; -import { - WorkflowIOValueTypeEnum, - NodeInputKeyEnum, - FlowNodeTemplateTypeEnum -} from '../../constants'; +import { FlowNodeTemplateTypeEnum, WorkflowIOValueTypeEnum } from '../../constants'; import { getHandleConfig } from '../utils'; export const SystemConfigNode: FlowNodeTemplateType = { @@ -19,50 +15,6 @@ export const SystemConfigNode: FlowNodeTemplateType = { unique: true, forbidDelete: true, version: '481', - 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: '' - } - ], + inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 65cd2a7be7ba..19dc863ae0b3 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -12,10 +12,15 @@ import type { AppTTSConfigType, AppWhisperConfigType, AppScheduledTriggerConfigType, - AppQuestionGuideTextConfigType + ChatInputGuideConfigType, + AppChatConfigType } from '../app/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'; export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => { @@ -41,70 +46,81 @@ export const splitGuideModule = (guideModules?: StoreNodeItemType) => { const welcomeText: string = guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.welcomeText)?.value || ''; - const variableNodes: VariableItemType[] = + const variables: VariableItemType[] = guideModules?.inputs.find((item) => item.key === NodeInputKeyEnum.variables)?.value || []; const questionGuide: boolean = !!guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value || false; - const ttsConfig: AppTTSConfigType = guideModules?.inputs?.find( - (item) => item.key === NodeInputKeyEnum.tts - )?.value || { type: 'web' }; + const ttsConfig: AppTTSConfigType = + guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.tts)?.value || + defaultTTSConfig; const whisperConfig: AppWhisperConfigType = guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.whisper)?.value || defaultWhisperConfig; - const scheduledTriggerConfig: AppScheduledTriggerConfigType | null = - guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.scheduleTrigger)?.value ?? - null; + const scheduledTriggerConfig: AppScheduledTriggerConfigType = guideModules?.inputs?.find( + (item) => item.key === NodeInputKeyEnum.scheduleTrigger + )?.value; - const questionGuideText: AppQuestionGuideTextConfigType = guideModules?.inputs?.find( - (item) => item.key === NodeInputKeyEnum.questionGuideText - )?.value || { - open: false - }; + const chatInputGuide: ChatInputGuideConfigType = + guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.chatInputGuide)?.value || + defaultChatInputGuideConfig; return { welcomeText, - variableNodes, + variables, questionGuide, ttsConfig, whisperConfig, scheduledTriggerConfig, - questionGuideText + chatInputGuide }; }; -export const replaceAppChatConfig = ({ - node, - variableList, - welcomeText +export const getAppChatConfig = ({ + chatConfig, + systemConfigNode, + storeVariables, + storeWelcomeText, + isPublicFetch = false }: { - node?: StoreNodeItemType; - variableList?: VariableItemType[]; - welcomeText?: string; -}): StoreNodeItemType | undefined => { - if (!node) return; - return { - ...node, - inputs: node.inputs.map((input) => { - if (input.key === NodeInputKeyEnum.variables && variableList) { - return { - ...input, - value: variableList - }; - } - if (input.key === NodeInputKeyEnum.welcomeText && welcomeText) { - return { - ...input, - value: welcomeText - }; - } - - return input; - }) + chatConfig?: AppChatConfigType; + systemConfigNode?: StoreNodeItemType; + storeVariables?: VariableItemType[]; + storeWelcomeText?: string; + isPublicFetch: boolean; +}): AppChatConfigType => { + const { + welcomeText, + variables, + questionGuide, + ttsConfig, + whisperConfig, + scheduledTriggerConfig, + chatInputGuide + } = splitGuideModule(systemConfigNode); + + 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) => { diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index cc5927d90b65..dd45fad32c0d 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -2,7 +2,7 @@ import { AppSchema } from '@fastgpt/global/core/app/type'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { getLLMModel } from '../ai/model'; -import { MongoAppVersion } from './versionSchema'; +import { MongoAppVersion } from './version/schema'; export const beforeUpdateAppFormat = ({ nodes @@ -55,11 +55,13 @@ export const getAppLatestVersion = async (appId: string, app?: AppSchema) => { if (version) { return { nodes: version.nodes, - edges: version.edges + edges: version.edges, + chatConfig: version.chatConfig || app?.chatConfig || {} }; } return { nodes: app?.modules || [], - edges: app?.edges || [] + edges: app?.edges || [], + chatConfig: app?.chatConfig || {} }; }; diff --git a/packages/service/core/app/qGuideSchema.ts b/packages/service/core/app/qGuideSchema.ts deleted file mode 100644 index 4f0741a93be5..000000000000 --- a/packages/service/core/app/qGuideSchema.ts +++ /dev/null @@ -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 = - models[AppQGuideCollectionName] || model(AppQGuideCollectionName, AppQGuideSchema); - -MongoAppQGuide.syncIndexes(); diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 0990f4f7bada..cb8c1f1d6768 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -10,6 +10,16 @@ import { 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({ teamId: { type: Schema.Types.ObjectId, @@ -47,6 +57,16 @@ const AppSchema = new Schema({ default: () => new Date() }, + // role and auth + permission: { + type: String, + enum: Object.keys(PermissionTypeMap), + default: PermissionTypeEnum.private + }, + teamTags: { + type: [String] + }, + // tmp store modules: { type: Array, @@ -56,6 +76,10 @@ const AppSchema = new Schema({ type: Array, default: [] }, + chatConfig: { + type: chatConfigType, + default: {} + }, scheduledTriggerConfig: { cronString: { @@ -74,14 +98,6 @@ const AppSchema = new Schema({ inited: { type: Boolean - }, - permission: { - type: String, - enum: Object.keys(PermissionTypeMap), - default: PermissionTypeEnum.private - }, - teamTags: { - type: [String] } }); diff --git a/packages/service/core/app/versionSchema.ts b/packages/service/core/app/version/schema.ts similarity index 81% rename from packages/service/core/app/versionSchema.ts rename to packages/service/core/app/version/schema.ts index c573078266eb..983ffbba0eab 100644 --- a/packages/service/core/app/versionSchema.ts +++ b/packages/service/core/app/version/schema.ts @@ -1,6 +1,7 @@ -import { connectionMongo, type Model } from '../../common/mongo'; +import { connectionMongo, type Model } from '../../../common/mongo'; const { Schema, model, models } = connectionMongo; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; +import { chatConfigType } from '../schema'; export const AppVersionCollectionName = 'app_versions'; @@ -21,6 +22,10 @@ const AppVersionSchema = new Schema({ edges: { type: Array, default: [] + }, + chatConfig: { + type: chatConfigType, + default: {} } }); diff --git a/packages/service/core/chat/inputGuide/schema.ts b/packages/service/core/chat/inputGuide/schema.ts new file mode 100644 index 000000000000..a8f14343e0c0 --- /dev/null +++ b/packages/service/core/chat/inputGuide/schema.ts @@ -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 = + models[ChatInputGuideCollectionName] || model(ChatInputGuideCollectionName, ChatInputGuideSchema); + +MongoChatInputGuide.syncIndexes(); diff --git a/packages/service/core/dataset/training/controller.ts b/packages/service/core/dataset/training/controller.ts index 19ef5e4ad382..984e4a3c05cd 100644 --- a/packages/service/core/dataset/training/controller.ts +++ b/packages/service/core/dataset/training/controller.ts @@ -168,7 +168,8 @@ export async function pushDataListToTrainingQueue({ indexes: item.indexes })), { - session + session, + ordered: false } ); } catch (error: any) { diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index b40a13c2dc7d..8d214ffa994d 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -44,6 +44,7 @@ import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { dispatchSystemConfig } from './init/systemConfig'; import { dispatchUpdateVariable } from './tools/runUpdateVar'; +import { addLog } from '../../../common/system/log'; const callbackMap: Record = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -137,7 +138,6 @@ export async function dispatchWorkFlow(data: Props): Promise void }) => new Promise((resolve, reject) => { @@ -29,3 +30,47 @@ export const loadFile2Buffer = ({ file, onError }: { file: File; onError?: (err: reject('The browser does not support file content reading'); } }); + +export const readFileRawText = ({ + file, + onError +}: { + file: File; + onError?: (err: any) => void; +}) => { + return new Promise((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; +}; diff --git a/packages/web/components/common/EmptyTip/index.tsx b/packages/web/components/common/EmptyTip/index.tsx index d36a66e8b406..e91294353d65 100644 --- a/packages/web/components/common/EmptyTip/index.tsx +++ b/packages/web/components/common/EmptyTip/index.tsx @@ -10,7 +10,7 @@ type Props = FlexProps & { const EmptyTip = ({ text, ...props }: Props) => { const { t } = useTranslation(); return ( - + {text || t('common.empty.Common Tip')} diff --git a/packages/web/components/common/Icon/index.tsx b/packages/web/components/common/Icon/index.tsx index 091a87321938..aec245e7ed19 100644 --- a/packages/web/components/common/Icon/index.tsx +++ b/packages/web/components/common/Icon/index.tsx @@ -15,7 +15,7 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType .catch((error) => console.log(error)); }, [name]); - return !!name && !!iconPaths[name] ? ( + return !!IconComponent ? ( { return ( - + {isLoading && } {children} diff --git a/packages/web/components/common/MyModal/index.tsx b/packages/web/components/common/MyModal/index.tsx index cc00ca9ef3b1..2cfd10b6a324 100644 --- a/packages/web/components/common/MyModal/index.tsx +++ b/packages/web/components/common/MyModal/index.tsx @@ -11,11 +11,13 @@ import { useMediaQuery } from '@chakra-ui/react'; import MyIcon from '../Icon'; +import MyBox from '../MyBox'; export interface MyModalProps extends ModalContentProps { iconSrc?: string; title?: any; isCentered?: boolean; + isLoading?: boolean; isOpen: boolean; onClose?: () => void; } @@ -27,6 +29,7 @@ const MyModal = ({ title, children, isCentered, + isLoading, w = 'auto', maxW = ['90vw', '600px'], ...props @@ -39,6 +42,7 @@ const MyModal = ({ onClose={() => onClose && onClose()} autoFocus={false} isCentered={isPc ? isCentered : true} + blockScrollOnMount={false} > )} - {children} - + ); diff --git a/packages/web/components/common/String/HighlightText.tsx b/packages/web/components/common/String/HighlightText.tsx new file mode 100644 index 000000000000..85f09148c76e --- /dev/null +++ b/packages/web/components/common/String/HighlightText.tsx @@ -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 ( + + {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 ( + + {part} + + ); + })} + + ); +}; + +export default HighlightText; diff --git a/packages/web/hooks/useRequest.tsx b/packages/web/hooks/useRequest.tsx index 15795d8f40d0..38e0de15f02d 100644 --- a/packages/web/hooks/useRequest.tsx +++ b/packages/web/hooks/useRequest.tsx @@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import type { UseMutationOptions } from '@tanstack/react-query'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { useTranslation } from 'next-i18next'; +import { useRequest as ahooksUseRequest } from 'ahooks'; interface Props extends UseMutationOptions { successToast?: string | null; @@ -39,3 +40,50 @@ export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...pr return mutation; }; + +type UseRequestFunProps = Parameters< + typeof ahooksUseRequest +>; +export const useRequest2 = ( + server: UseRequestFunProps[0], + options: UseRequestFunProps[1] & { + errorToast?: string; + successToast?: string; + } = {}, + plugin?: UseRequestFunProps[2] +) => { + const { t } = useTranslation(); + const { errorToast, successToast, ...rest } = options || {}; + const { toast } = useToast(); + + const res = ahooksUseRequest( + 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; +}; diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index dd989351b736..40de950ef4da 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -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 { useToast } from './useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; 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 { useTranslation } from 'next-i18next'; @@ -13,12 +21,19 @@ export function useScrollPagination< >( api: (data: TParams) => Promise, { + debounceWait, + throttleWait, + refreshDeps, itemHeight = 50, overscan = 10, pageSize = 10, defaultParams = {} }: { + debounceWait?: number; + throttleWait?: number; + refreshDeps?: any[]; + itemHeight: number; overscan?: number; @@ -45,7 +60,7 @@ export function useScrollPagination< }); const loadData = useLockFn(async (num: number = current) => { - if (noMore.current) return; + if (noMore.current && num !== 1) return; setTrue(); @@ -59,7 +74,7 @@ export function useScrollPagination< setCurrent(num); if (num === 1) { - // reload + // init or reload setData(res.list); noMore.current = res.list.length >= res.total; } else { @@ -78,34 +93,48 @@ export function useScrollPagination< setFalse(); }); + const scroll2Top = () => { + if (containerRef.current) { + containerRef.current.scrollTop = 0; + } + }; + const ScrollList = useMemoizedFn( ({ children, + EmptyChildren, isLoading, ...props - }: { children: React.ReactNode; isLoading?: boolean } & BoxProps) => { + }: { + children: React.ReactNode; + EmptyChildren?: React.ReactNode; + isLoading?: boolean; + } & BoxProps) => { return ( <> {children} + {noMore.current && list.length > 0 && ( + + {t('common.No more data')} + + )} + {list.length === 0 && !isLoading && EmptyChildren && <>{EmptyChildren}} - {noMore.current && ( - - {t('common.No more data')} - - )} ); } ); - useMount(() => { - loadData(1); + useRequest(() => loadData(1), { + refreshDeps, + debounceWait: data.length === 0 ? 0 : debounceWait, + throttleWait }); const scroll = useScroll(containerRef); useEffect(() => { - if (!containerRef.current) return; + if (!containerRef.current || list.length === 0) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; @@ -118,8 +147,10 @@ export function useScrollPagination< containerRef, list, data, + setData, isLoading, ScrollList, - fetchData: loadData + fetchData: loadData, + scroll2Top }; } diff --git a/packages/web/package.json b/packages/web/package.json index 59021b6fc23b..e64a52800b60 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,7 +26,6 @@ "lodash": "^4.17.21", "next-i18next": "15.2.0", "papaparse": "^5.4.1", - "pdfjs-dist": "4.0.269", "react": "18.3.1", "use-context-selector": "^1.4.4", "react-day-picker": "^8.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1be8c2c05d50..7df766bfe3ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,9 +283,6 @@ importers: papaparse: specifier: ^5.4.1 version: 5.4.1 - pdfjs-dist: - specifier: 4.0.269 - version: 4.0.269(encoding@0.1.13) react: specifier: 18.3.1 version: 18.3.1 diff --git a/projects/app/i18n/en/app.json b/projects/app/i18n/en/app.json index 03734f220f22..22b8ab29208e 100644 --- a/projects/app/i18n/en/app.json +++ b/projects/app/i18n/en/app.json @@ -47,14 +47,6 @@ "type": "\"{{type}}\" type\n{{description}}" }, "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" } } diff --git a/projects/app/i18n/en/chat.json b/projects/app/i18n/en/chat.json new file mode 100644 index 000000000000..6f5f39b9d7b6 --- /dev/null +++ b/projects/app/i18n/en/chat.json @@ -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" +} diff --git a/projects/app/i18n/en/common.json b/projects/app/i18n/en/common.json index 8f0008ce52f8..088919cd6896 100644 --- a/projects/app/i18n/en/common.json +++ b/projects/app/i18n/en/common.json @@ -3,6 +3,7 @@ "App": "App", "Export": "Export", "Folder": "Folder", + "Is open": "Opened", "Login": "Login", "Move": "Move", "Name": "Name", @@ -16,6 +17,7 @@ "Action": "Action", "Add": "Add", "Add New": "Add New", + "Add Success": "Add successfully", "All": "All", "Back": "Back", "Beta": "Beta", @@ -1177,6 +1179,7 @@ } }, "error": { + "Create failed": "Create failed", "fileNotFound": "File not found~", "team": { "overSize": "Team members exceed the limit" diff --git a/projects/app/i18n/zh/app.json b/projects/app/i18n/zh/app.json index 23fdd132aca4..83c85cec359e 100644 --- a/projects/app/i18n/zh/app.json +++ b/projects/app/i18n/zh/app.json @@ -46,14 +46,6 @@ "type": "\"{{type}}\"类型\n{{description}}" }, "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": "模块名不能为空" } } diff --git a/projects/app/i18n/zh/chat.json b/projects/app/i18n/zh/chat.json new file mode 100644 index 000000000000..a49b7e501e7f --- /dev/null +++ b/projects/app/i18n/zh/chat.json @@ -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": "新词库" +} diff --git a/projects/app/i18n/zh/common.json b/projects/app/i18n/zh/common.json index de5c62844834..d051a193301e 100644 --- a/projects/app/i18n/zh/common.json +++ b/projects/app/i18n/zh/common.json @@ -3,6 +3,7 @@ "App": "应用", "Export": "导出", "Folder": "文件夹", + "Is open": "是否开启", "Login": "登录", "Move": "移动", "Name": "名称", @@ -16,6 +17,7 @@ "Action": "操作", "Add": "添加", "Add New": "新增", + "Add Success": "添加成功", "All": "全部", "Back": "返回", "Beta": "实验版", @@ -1184,6 +1186,7 @@ } }, "error": { + "Create failed": "创建失败", "fileNotFound": "文件找不到了~", "team": { "overSize": "团队成员超出上限" diff --git a/projects/app/next.config.js b/projects/app/next.config.js index 9124b72c1ebe..1a54fab54488 100644 --- a/projects/app/next.config.js +++ b/projects/app/next.config.js @@ -86,7 +86,7 @@ const nextConfig = { return config; }, - transpilePackages: ['@fastgpt/*', 'ahooks', '@chakra-ui/*', 'react'], + transpilePackages: ['@fastgpt/*', 'ahooks'], experimental: { // 指定导出包优化,按需引入包模块 optimizePackageImports: ['mongoose', 'pg'], diff --git a/projects/app/package.json b/projects/app/package.json index ce001a7771c0..ab5180639535 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.8", + "version": "4.8.1", "private": false, "scripts": { "dev": "next dev", diff --git a/projects/app/src/components/ChatBox/MessageInput.tsx b/projects/app/src/components/ChatBox/Input/ChatInput.tsx similarity index 90% rename from projects/app/src/components/ChatBox/MessageInput.tsx rename to projects/app/src/components/ChatBox/Input/ChatInput.tsx index 24f6669d4aef..afcbbdef43a9 100644 --- a/projects/app/src/components/ChatBox/MessageInput.tsx +++ b/projects/app/src/components/ChatBox/Input/ChatInput.tsx @@ -1,9 +1,9 @@ import { useSpeech } from '@/web/common/hooks/useSpeech'; import { useSystemStore } from '@/web/common/system/useSystemStore'; 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 MyTooltip from '../MyTooltip'; +import MyTooltip from '../../MyTooltip'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; 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 { useRequest } from '@fastgpt/web/hooks/useRequest'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; -import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from './type'; -import { textareaMinH } from './constants'; +import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type'; +import { textareaMinH } from '../constants'; import { UseFormReturn, useFieldArray } from 'react-hook-form'; -import { useChatProviderStore } from './Provider'; -import QuestionGuide from './components/QustionGuide'; -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'; +import { useChatProviderStore } from '../Provider'; +import dynamic from 'next/dynamic'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); -const MessageInput = ({ +const InputGuideBox = dynamic(() => import('./InputGuideBox')); + +const ChatInput = ({ onSendMessage, onStop, TextareaDom, @@ -38,7 +36,7 @@ const MessageInput = ({ TextareaDom: React.MutableRefObject; resetInputVal: (val: ChatBoxInputType) => void; chatForm: UseFormReturn; - appId?: string; + appId: string; }) => { const { setValue, watch, control } = chatForm; const inputValue = watch('input'); @@ -53,12 +51,19 @@ const MessageInput = ({ name: 'files' }); - const { shareId, outLinkUid, teamId, teamToken, isChatting, whisperConfig, autoTTSResponse } = - useChatProviderStore(); + const { + shareId, + outLinkUid, + teamId, + teamToken, + isChatting, + whisperConfig, + autoTTSResponse, + chatInputGuide + } = useChatProviderStore(); const { isPc, whisperModel } = useSystemStore(); const canvasRef = useRef(null); const { t } = useTranslation(); - const { appDetail } = useAppStore(); const havInput = !!inputValue || fileList.length > 0; const hasFileUploading = fileList.some((item) => !item.url); @@ -150,9 +155,9 @@ const MessageInput = ({ ); /* on send */ - const handleSend = async () => { + const handleSend = async (val?: string) => { if (!canSendMessage) return; - const textareaValue = TextareaDom.current?.value || ''; + const textareaValue = val || TextareaDom.current?.value || ''; onSendMessage({ text: textareaValue.trim(), @@ -211,23 +216,6 @@ const MessageInput = ({ startSpeak(finishWhisperTranscription); }, [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 ( + {/* Chat input guide box */} + {chatInputGuide.open && ( + { + setValue('input', e); + }} + onSend={(e) => { + handleSend(e); + }} + /> + )} + {/* translate loading */} - {/* popup */} - {havInput && ( - setValue('input', value)} - bottom={'100%'} - top={'auto'} - left={0} - right={0} - mb={2} - overflowY={'auto'} - boxShadow={'sm'} - /> - )} - {/* file preview */} {fileList.map((item, index) => ( @@ -415,12 +402,7 @@ const MessageInput = ({ // @ts-ignore e.key === 'a' && e.ctrlKey && e.target?.select(); - if ( - (isPc || window !== parent) && - e.keyCode === 13 && - !e.shiftKey && - !(havInput && data?.list.length && data?.list.length > 0) - ) { + if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) { handleSend(); e.preventDefault(); } @@ -556,4 +538,4 @@ const MessageInput = ({ ); }; -export default React.memo(MessageInput); +export default React.memo(ChatInput); diff --git a/projects/app/src/components/ChatBox/Input/InputGuideBox.tsx b/projects/app/src/components/ChatBox/Input/InputGuideBox.tsx new file mode 100644 index 000000000000..bdba32d23fd7 --- /dev/null +++ b/projects/app/src/components/ChatBox/Input/InputGuideBox.tsx @@ -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 ? ( + + + + {chatT('Input guide')} + + {data.map((item, index) => ( + onSelect(item)} + > + + + + + { + e.stopPropagation(); + onSend(item); + }} + /> + + + ))} + + ) : null; +} diff --git a/projects/app/src/components/ChatBox/Provider.tsx b/projects/app/src/components/ChatBox/Provider.tsx index fd9a8499d759..b6d9149003bf 100644 --- a/projects/app/src/components/ChatBox/Provider.tsx +++ b/projects/app/src/components/ChatBox/Provider.tsx @@ -2,17 +2,23 @@ import React, { useContext, createContext, useState, useMemo, useEffect, useCall import { useAudioPlay } from '@/web/common/utils/voice'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; -import { splitGuideModule } from '@fastgpt/global/core/workflow/utils'; import { + AppChatConfigType, AppTTSConfigType, AppWhisperConfigType, + ChatInputGuideConfigType, VariableItemType } from '@fastgpt/global/core/app/type'; import { ChatSiteItemType } from '@fastgpt/global/core/chat/type'; +import { + defaultChatInputGuideConfig, + defaultTTSConfig, + defaultWhisperConfig +} from '@fastgpt/global/core/app/constants'; type useChatStoreType = OutLinkChatAuthProps & { welcomeText: string; - variableNodes: VariableItemType[]; + variableList: VariableItemType[]; questionGuide: boolean; ttsConfig: AppTTSConfigType; whisperConfig: AppWhisperConfigType; @@ -38,10 +44,11 @@ type useChatStoreType = OutLinkChatAuthProps & { chatHistories: ChatSiteItemType[]; setChatHistories: React.Dispatch>; isChatting: boolean; + chatInputGuide: ChatInputGuideConfigType; }; const StateContext = createContext({ welcomeText: '', - variableNodes: [], + variableList: [], questionGuide: false, ttsConfig: { type: 'none', @@ -87,11 +94,15 @@ const StateContext = createContext({ }, finishSegmentedAudio: function (): void { throw new Error('Function not implemented.'); + }, + chatInputGuide: { + open: false, + customUrl: '' } }); export type ChatProviderProps = OutLinkChatAuthProps & { - userGuideModule?: StoreNodeItemType; + chatConfig?: AppChatConfigType; // not chat test params chatId?: string; @@ -105,15 +116,19 @@ const Provider = ({ outLinkUid, teamId, teamToken, - userGuideModule, + chatConfig = {}, children }: ChatProviderProps) => { const [chatHistories, setChatHistories] = useState([]); - const { welcomeText, variableNodes, questionGuide, ttsConfig, whisperConfig } = useMemo( - () => splitGuideModule(userGuideModule), - [userGuideModule] - ); + const { + welcomeText = '', + variables = [], + questionGuide = false, + ttsConfig = defaultTTSConfig, + whisperConfig = defaultWhisperConfig, + chatInputGuide = defaultChatInputGuideConfig + } = useMemo(() => chatConfig, [chatConfig]); // segment audio const [audioPlayingChatId, setAudioPlayingChatId] = useState(); @@ -150,7 +165,7 @@ const Provider = ({ teamId, teamToken, welcomeText, - variableNodes, + variableList: variables, questionGuide, ttsConfig, whisperConfig, @@ -167,7 +182,8 @@ const Provider = ({ setAudioPlayingChatId, chatHistories, setChatHistories, - isChatting + isChatting, + chatInputGuide }; return {children}; diff --git a/projects/app/src/components/ChatBox/components/QustionGuide.tsx b/projects/app/src/components/ChatBox/components/QustionGuide.tsx deleted file mode 100644 index b71e22be3eaa..000000000000 --- a/projects/app/src/components/ChatBox/components/QustionGuide.tsx +++ /dev/null @@ -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 ? ( - - - - {appT('modules.Input Guide')} - - {guides.map((item, index) => ( - { - e.preventDefault(); - - setDropdownValue?.(item); - }} - onMouseEnter={() => { - setHighlightedIndex(index); - }} - > - {item} - - ))} - - ) : null; -} diff --git a/projects/app/src/components/ChatBox/components/VariableInput.tsx b/projects/app/src/components/ChatBox/components/VariableInput.tsx index bb258709e102..fe93c56268fb 100644 --- a/projects/app/src/components/ChatBox/components/VariableInput.tsx +++ b/projects/app/src/components/ChatBox/components/VariableInput.tsx @@ -12,12 +12,12 @@ import { ChatBoxInputFormType } from '../type.d'; const VariableInput = ({ appAvatar, - variableNodes, + variableList, chatForm, onSubmitVariables }: { appAvatar?: string; - variableNodes: VariableItemType[]; + variableList: VariableItemType[]; onSubmitVariables: (e: Record) => void; chatForm: UseFormReturn; }) => { @@ -40,7 +40,7 @@ const VariableInput = ({ bg={'white'} boxShadow={'0 0 8px rgba(0,0,0,0.15)'} > - {variableNodes.map((item) => ( + {variableList.map((item) => ( {item.label} diff --git a/projects/app/src/components/ChatBox/index.tsx b/projects/app/src/components/ChatBox/index.tsx index f982e502e43c..521ff6a021b5 100644 --- a/projects/app/src/components/ChatBox/index.tsx +++ b/projects/app/src/components/ChatBox/index.tsx @@ -45,7 +45,7 @@ import type { ChatBoxInputType, ChatBoxInputFormType } from './type.d'; -import MessageInput from './MessageInput'; +import ChatInput from './Input/ChatInput'; import ChatBoxDivider from '../core/chat/Divider'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { getNanoid } from '@fastgpt/global/common/string/tools'; @@ -59,6 +59,7 @@ import ChatItem from './components/ChatItem'; import dynamic from 'next/dynamic'; import { useCreation } from 'ahooks'; +import { AppChatConfigType } from '@fastgpt/global/core/app/type'; const ResponseTags = dynamic(() => import('./ResponseTags')); const FeedbackModal = dynamic(() => import('./FeedbackModal')); @@ -81,7 +82,7 @@ type Props = OutLinkChatAuthProps & { showEmptyIntro?: boolean; appAvatar?: string; userAvatar?: string; - userGuideModule?: StoreNodeItemType; + chatConfig?: AppChatConfigType; showFileSelector?: boolean; active?: boolean; // can use appId: string; @@ -149,7 +150,7 @@ const ChatBox = ( const { welcomeText, - variableNodes, + variableList, questionGuide, startSegmentedAudio, finishSegmentedAudio, @@ -174,8 +175,8 @@ const ChatBox = ( /* variable */ const filterVariableNodes = useCreation( - () => variableNodes.filter((item) => item.type !== VariableInputEnum.custom), - [variableNodes] + () => variableList.filter((item) => item.type !== VariableInputEnum.custom), + [variableList] ); // 滚动到底部 @@ -390,9 +391,9 @@ const ChatBox = ( return; } - // delete invalid variables, 只保留在 variableNodes 中的变量 + // delete invalid variables, 只保留在 variableList 中的变量 const requestVariables: Record = {}; - variableNodes?.forEach((item) => { + variableList?.forEach((item) => { requestVariables[item.key] = variables[item.key] || ''; }); @@ -566,7 +567,7 @@ const ChatBox = ( startSegmentedAudio, t, toast, - variableNodes + variableList ] ); @@ -907,7 +908,7 @@ const ChatBox = ( {!!filterVariableNodes?.length && ( { setValue('chatStarted', true); @@ -1000,7 +1001,7 @@ const ChatBox = ( {/* message input */} {onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && ( - chatController.current?.abort('stop')} TextareaDom={TextareaDom} diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index 75b81a438622..3959fd596be3 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -13,10 +13,19 @@ import Auth from './auth'; const Navbar = dynamic(() => import('./navbar')); const NavbarPhone = dynamic(() => import('./navbarPhone')); -const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal')); -const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal')); -const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal')); -const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform')); +const UpdateInviteModal = dynamic( + () => import('@/components/support/user/team/UpdateInviteModal'), + { ssr: false } +); +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 = { '/': true, @@ -114,12 +123,17 @@ const Layout = ({ children }: { children: JSX.Element }) => { )} - {!!userInfo && } - {isNotSufficientModal && !isHideNavbar && } - {!!userInfo && } - {!!userInfo && importantInforms.length > 0 && ( - + {feConfigs?.isPlus && ( + <> + {!!userInfo && } + {isNotSufficientModal && !isHideNavbar && } + {!!userInfo && } + {!!userInfo && importantInforms.length > 0 && ( + + )} + )} + ); diff --git a/projects/app/src/components/MyInput/index.tsx b/projects/app/src/components/MyInput/index.tsx index 5b52fb4cef66..46d83e7e57f9 100644 --- a/projects/app/src/components/MyInput/index.tsx +++ b/projects/app/src/components/MyInput/index.tsx @@ -3,24 +3,28 @@ import { Flex, Input, InputProps } from '@chakra-ui/react'; interface Props extends InputProps { leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; } -const MyInput = ({ leftIcon, ...props }: Props) => { +const MyInput = ({ leftIcon, rightIcon, ...props }: Props) => { return ( - - + + {leftIcon && ( - + {leftIcon} )} + {rightIcon && ( + + {rightIcon} + + )} ); }; diff --git a/projects/app/src/components/core/app/QGuidesConfig.tsx b/projects/app/src/components/core/app/QGuidesConfig.tsx deleted file mode 100644 index 60cd88778f6c..000000000000 --- a/projects/app/src/components/core/app/QGuidesConfig.tsx +++ /dev/null @@ -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(''); - - 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 ( - - - {appT('modules.Question Guide')} - - - - - - - - {appT('modules.Question Guide Switch')} - { - onChange({ - ...value, - open: e.target.checked - }); - }} - /> - - {isOpenQuestionGuide && ( - <> - - {appT('modules.Question Guide Texts')} - - {value.textList.length || 0} - - - - - <> - - {appT('modules.Custom question guide URL')} - window.open(getDocPath('/docs/course/custom_link'))} - color={'primary.700'} - alignItems={'center'} - cursor={'pointer'} - > - - {commonT('common.Documents')} - - - -