From fdcf53ea3872298fb244ac85c076cf0afdbf1d00 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 21 Jul 2023 17:35:15 +0800 Subject: [PATCH] feat: overview setting --- client/src/components/ChatBox/index.tsx | 4 +- client/src/constants/flow/ModuleTemplate.ts | 38 +- client/src/constants/theme.ts | 3 + client/src/pages/api/admin/initv4.ts | 202 +++--- client/src/pages/api/app/modules/kb/search.ts | 24 +- .../app/detail/components/BasicEdit/index.tsx | 527 +++++++++++++++ .../detail/components/Charts/TotalUsage.tsx | 2 +- .../Edit/components/Nodes/NodeKbSearch.tsx | 71 +- .../Edit/components/Nodes/NodeUserGuide.tsx | 10 +- .../Edit/components/Nodes/NodeVariable.tsx | 6 +- .../Edit/components/Plugins/KBSelect.tsx | 143 ---- .../pages/app/detail/components/InfoModal.tsx | 67 +- .../app/detail/components/KBSelectModal.tsx | 214 ++++++ .../pages/app/detail/components/OverView.tsx | 177 ++--- .../detail/components/VariableEditModal.tsx | 214 ++++++ client/src/pages/app/detail/index.tsx | 4 +- .../chat/components/ChatHistorySlider.tsx | 2 +- .../src/pages/chat/components/SliderApps.tsx | 2 +- client/src/pages/chat/index.tsx | 2 +- client/src/store/user.ts | 14 +- client/src/types/plugin.d.ts | 2 + client/src/utils/app.ts | 611 ++++++++++++++++++ 22 files changed, 1920 insertions(+), 419 deletions(-) create mode 100644 client/src/pages/app/detail/components/BasicEdit/index.tsx delete mode 100644 client/src/pages/app/detail/components/Edit/components/Plugins/KBSelect.tsx create mode 100644 client/src/pages/app/detail/components/KBSelectModal.tsx create mode 100644 client/src/pages/app/detail/components/VariableEditModal.tsx create mode 100644 client/src/utils/app.ts diff --git a/client/src/components/ChatBox/index.tsx b/client/src/components/ChatBox/index.tsx index 1c0cf869b..52337c1bb 100644 --- a/client/src/components/ChatBox/index.tsx +++ b/client/src/components/ChatBox/index.tsx @@ -650,9 +650,9 @@ const ChatBox = ( diff --git a/client/src/constants/flow/ModuleTemplate.ts b/client/src/constants/flow/ModuleTemplate.ts index b822a819b..564e42db1 100644 --- a/client/src/constants/flow/ModuleTemplate.ts +++ b/client/src/constants/flow/ModuleTemplate.ts @@ -9,6 +9,14 @@ import { } from './inputTemplate'; import { rawSearchKey } from '../chat'; +export const ChatModelSystemTip = + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}'; +export const ChatModelLimitTip = + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。可使用变量,例如 {{language}}。引导例子:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"'; +export const userGuideTip = '可以添加特殊的对话前后引导模块,更好的让用户进行对话'; +export const welcomeTextTip = + '每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]: 用户点击后可以直接发送该问题'; + export const VariableModule: AppModuleTemplateItemType = { logo: '/imgs/module/variable.png', name: '全局变量', @@ -30,7 +38,7 @@ export const VariableModule: AppModuleTemplateItemType = { export const UserGuideModule: AppModuleTemplateItemType = { logo: '/imgs/module/userGuide.png', name: '用户引导', - intro: '可以添加特殊的对话前后引导模块,更好的让用户进行对话', + intro: userGuideTip, type: AppModuleItemTypeEnum.userGuide, flowType: FlowModuleTypeEnum.userGuide, inputs: [ @@ -77,7 +85,7 @@ export const HistoryModule: AppModuleTemplateItemType = { key: 'maxContext', type: FlowInputItemTypeEnum.numberInput, label: '最长记录数', - value: 4, + value: 6, min: 0, max: 50 }, @@ -146,20 +154,16 @@ export const ChatModule: AppModuleTemplateItemType = { key: 'systemPrompt', type: FlowInputItemTypeEnum.textarea, label: '系统提示词', - description: - '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', - placeholder: - '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + description: ChatModelSystemTip, + placeholder: ChatModelSystemTip, value: '' }, { key: 'limitPrompt', type: FlowInputItemTypeEnum.textarea, label: '限定词', - description: - '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', - placeholder: - '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + description: ChatModelLimitTip, + placeholder: ChatModelLimitTip, value: '' }, // Input_Template_TFSwitch, @@ -191,7 +195,7 @@ export const KBSearchModule: AppModuleTemplateItemType = { url: '/app/modules/kb/search', inputs: [ { - key: 'kb_ids', + key: 'kbList', type: FlowInputItemTypeEnum.custom, label: '关联的知识库', value: [], @@ -548,7 +552,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = key: 'maxContext', type: 'numberInput', label: '最长记录数', - value: 4, + value: 10, min: 0, max: 50, connected: false @@ -627,7 +631,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = key: 'maxContext', type: 'numberInput', label: '最长记录数', - value: 4, + value: 10, min: 0, max: 50, connected: false @@ -788,7 +792,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = ...KBSearchModule, inputs: [ { - key: 'kb_ids', + key: 'kbList', type: 'custom', label: '关联的知识库', value: [], @@ -1100,7 +1104,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = key: 'maxContext', type: 'numberInput', label: '最长记录数', - value: 4, + value: 10, min: 0, max: 50, connected: false @@ -1239,7 +1243,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = key: 'maxContext', type: 'numberInput', label: '最长记录数', - value: 4, + value: 10, min: 0, max: 50, connected: false @@ -1400,7 +1404,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = ...KBSearchModule, inputs: [ { - key: 'kb_ids', + key: 'kbList', type: 'custom', label: '关联的知识库', value: [], diff --git a/client/src/constants/theme.ts b/client/src/constants/theme.ts index 2f46b59d4..97ed528c4 100644 --- a/client/src/constants/theme.ts +++ b/client/src/constants/theme.ts @@ -163,6 +163,9 @@ const Textarea: ComponentStyleConfig = { border: '1px solid', borderRadius: 'base', borderColor: 'myGray.200', + _hover: { + borderColor: '' + }, _focus: { borderColor: 'myBlue.600', boxShadow: '0px 0px 4px #A8DBFF', diff --git a/client/src/pages/api/admin/initv4.ts b/client/src/pages/api/admin/initv4.ts index a1750866f..8e96c950e 100644 --- a/client/src/pages/api/admin/initv4.ts +++ b/client/src/pages/api/admin/initv4.ts @@ -52,6 +52,49 @@ const chatTemplate = ({ }, moduleId: '7z5g5h' }, + { + logo: '/imgs/module/history.png', + name: '聊天记录', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'historyNode', + url: '/app/modules/init/history', + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 4, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: '7pacf0', + key: 'history' + } + ] + } + ], + position: { + x: 452.5466249541586, + y: 1276.3930310334215 + }, + moduleId: 'xj0c9p' + }, { logo: '/imgs/module/AI.png', name: 'AI 对话', @@ -176,49 +219,6 @@ const chatTemplate = ({ y: 890.014595014464 }, moduleId: '7pacf0' - }, - { - logo: '/imgs/module/history.png', - name: '聊天记录', - intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', - type: 'initInput', - flowType: 'historyNode', - url: '/app/modules/init/history', - inputs: [ - { - key: 'maxContext', - type: 'numberInput', - label: '最长记录数', - value: 4, - min: 0, - max: 50, - connected: false - }, - { - key: 'history', - type: 'hidden', - label: '聊天记录', - connected: false - } - ], - outputs: [ - { - key: 'history', - label: '聊天记录', - type: 'source', - targets: [ - { - moduleId: '7pacf0', - key: 'history' - } - ] - } - ], - position: { - x: 452.5466249541586, - y: 1276.3930310334215 - }, - moduleId: 'xj0c9p' } ]; }; @@ -228,7 +228,7 @@ const kbTemplate = ({ maxToken, systemPrompt, limitPrompt, - kbs = [], + kbList = [], searchSimilarity, searchLimit, searchEmptyText @@ -238,7 +238,7 @@ const kbTemplate = ({ maxToken: number; systemPrompt: string; limitPrompt: string; - kbs: string[]; + kbList: { kbId: string }[]; searchSimilarity: number; searchLimit: number; searchEmptyText: string; @@ -446,10 +446,10 @@ const kbTemplate = ({ url: '/app/modules/kb/search', inputs: [ { - key: 'kb_ids', + key: 'kbList', type: 'custom', label: '关联的知识库', - value: kbs, + value: kbList, list: [], connected: false }, @@ -588,54 +588,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await connectToDatabase(); const { limit = 1000 } = req.body as { limit: number }; + let skip = 0; + const total = await App.countDocuments(); + let promise = Promise.resolve(); + console.log(total); - // 遍历所有的 app - const apps = await App.find( - { - chat: { $ne: null }, - modules: { $exists: false } - // userId: '63f9a14228d2a688d8dc9e1b' - }, - '_id chat' - ).limit(limit); - - await Promise.all( - apps.map(async (app) => { - if (!app.chat) return app; - const modules = (() => { - if (app.chat.relatedKbs.length === 0) { - return chatTemplate({ - model: app.chat.chatModel, - temperature: app.chat.temperature, - maxToken: app.chat.maxToken, - systemPrompt: app.chat.systemPrompt, - limitPrompt: app.chat.limitPrompt - }); - } else { - return kbTemplate({ - model: app.chat.chatModel, - temperature: app.chat.temperature, - maxToken: app.chat.maxToken, - systemPrompt: app.chat.systemPrompt, - limitPrompt: app.chat.limitPrompt, - kbs: app.chat.relatedKbs, - searchEmptyText: app.chat.searchEmptyText, - searchLimit: app.chat.searchLimit, - searchSimilarity: app.chat.searchSimilarity - }); - } - })(); - - await App.findByIdAndUpdate(app.id, { - modules + for (let i = 0; i < total; i += limit) { + const skipVal = skip; + skip += limit; + promise = promise + .then(() => init(limit, skipVal)) + .then(() => { + console.log(skipVal); }); - return modules; - }) - ); + } - jsonRes(res, { - data: apps.length - }); + await promise; + + jsonRes(res, {}); } catch (error) { jsonRes(res, { code: 500, @@ -643,3 +613,51 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } } + +async function init(limit: number, skip: number) { + // 遍历 app + const apps = await App.find( + { + chat: { $ne: null } + // modules: { $exists: false }, + // userId: '63f9a14228d2a688d8dc9e1b' + }, + '_id chat' + ) + .limit(limit) + .skip(skip); + + return Promise.all( + apps.map(async (app) => { + if (!app.chat) return app; + const modules = (() => { + if (app.chat.relatedKbs.length === 0) { + return chatTemplate({ + model: app.chat.chatModel, + temperature: app.chat.temperature, + maxToken: app.chat.maxToken, + systemPrompt: app.chat.systemPrompt, + limitPrompt: app.chat.limitPrompt + }); + } else { + return kbTemplate({ + model: app.chat.chatModel, + temperature: app.chat.temperature, + maxToken: app.chat.maxToken, + systemPrompt: app.chat.systemPrompt, + limitPrompt: app.chat.limitPrompt, + kbList: app.chat.relatedKbs.map((id) => ({ kbId: id })), + searchEmptyText: app.chat.searchEmptyText, + searchLimit: app.chat.searchLimit, + searchSimilarity: app.chat.searchSimilarity + }); + } + })(); + + await App.findByIdAndUpdate(app.id, { + modules + }); + return modules; + }) + ); +} diff --git a/client/src/pages/api/app/modules/kb/search.ts b/client/src/pages/api/app/modules/kb/search.ts index e5f342e73..e4bc3c195 100644 --- a/client/src/pages/api/app/modules/kb/search.ts +++ b/client/src/pages/api/app/modules/kb/search.ts @@ -9,6 +9,7 @@ import { getVector } from '@/pages/api/openapi/plugin/vector'; import { countModelPrice, pushTaskBillListItem } from '@/service/events/pushBill'; import { getModel } from '@/service/utils/data'; import { authUser } from '@/service/utils/auth'; +import type { SelectedKbType } from '@/types/plugin'; export type QuoteItemType = { kb_id: string; @@ -18,7 +19,7 @@ export type QuoteItemType = { source?: string; }; type Props = { - kb_ids: string[]; + kbList: SelectedKbType; history: ChatItemType[]; similarity: number; limit: number; @@ -37,19 +38,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex try { await authUser({ req, authRoot: true }); - const { kb_ids = [], userChatInput } = req.body as Props; + const { kbList = [], userChatInput } = req.body as Props; if (!userChatInput) { throw new Error('用户输入为空'); } - if (!Array.isArray(kb_ids) || kb_ids.length === 0) { + if (!Array.isArray(kbList) || kbList.length === 0) { throw new Error('没有选择知识库'); } const result = await kbSearch({ ...req.body, - kb_ids, + kbList, userChatInput }); @@ -66,7 +67,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex }); export async function kbSearch({ - kb_ids = [], + kbList = [], history = [], similarity = 0.8, limit = 5, @@ -74,12 +75,9 @@ export async function kbSearch({ userChatInput, billId }: Props): Promise { - if (kb_ids.length === 0) - return { - isEmpty: true, - rawSearch: [], - quotePrompt: undefined - }; + if (kbList.length === 0) { + return Promise.reject('没有选择知识库'); + } // get vector const vectorModel = global.vectorModels[0].model; @@ -93,8 +91,8 @@ export async function kbSearch({ PgClient.query( `BEGIN; SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10}; - select kb_id,id,q,a,source from modelData where kb_id IN (${kb_ids - .map((item) => `'${item}'`) + select kb_id,id,q,a,source from modelData where kb_id IN (${kbList + .map((item) => `'${item.kbId}'`) .join(',')}) AND vector <#> '[${vectors[0]}]' < -${similarity} order by vector <#> '[${ vectors[0] }]' limit ${limit}; diff --git a/client/src/pages/app/detail/components/BasicEdit/index.tsx b/client/src/pages/app/detail/components/BasicEdit/index.tsx new file mode 100644 index 000000000..7f51cd7fe --- /dev/null +++ b/client/src/pages/app/detail/components/BasicEdit/index.tsx @@ -0,0 +1,527 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + Box, + Flex, + Grid, + BoxProps, + Textarea, + useTheme, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useDisclosure, + Button, + IconButton +} from '@chakra-ui/react'; +import { useUserStore } from '@/store/user'; +import { useQuery } from '@tanstack/react-query'; +import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons'; +import { useForm, useFieldArray } from 'react-hook-form'; +import { + appModules2Form, + getDefaultAppForm, + appForm2Modules, + type EditFormType +} from '@/utils/app'; +import { chatModelList } from '@/store/static'; +import { formatPrice } from '@/utils/user'; +import { + ChatModelSystemTip, + ChatModelLimitTip, + welcomeTextTip +} from '@/constants/flow/ModuleTemplate'; +import { AppModuleItemType, VariableItemType } from '@/types/app'; +import { useRequest } from '@/hooks/useRequest'; +import { useConfirm } from '@/hooks/useConfirm'; +import { FlowModuleTypeEnum } from '@/constants/flow'; +import { streamFetch } from '@/api/fetch'; + +import dynamic from 'next/dynamic'; +import MySelect from '@/components/Select'; +import MySlider from '@/components/Slider'; +import MyTooltip from '@/components/MyTooltip'; +import Avatar from '@/components/Avatar'; +import MyIcon from '@/components/Icon'; +import ChatBox, { + getSpecialModule, + type ComponentRef, + type StartChatFnProps +} from '@/components/ChatBox'; +import { addVariable } from '../VariableEditModal'; +import { KBSelectModal, KbParamsModal } from '../KBSelectModal'; + +const VariableEditModal = dynamic(() => import('../VariableEditModal')); + +const Settings = ({ appId }: { appId: string }) => { + const theme = useTheme(); + const { appDetail, updateAppDetail, loadKbList, myKbList } = useUserStore(); + + const [editVariable, setEditVariable] = useState(); + + useQuery(['initkb', appId], () => loadKbList()); + + const [refresh, setRefresh] = useState(false); + + const { openConfirm, ConfirmChild } = useConfirm({ + title: '警告', + content: '保存后将会覆盖高级编排配置,请确保该应用未使用高级编排功能。' + }); + const { register, setValue, getValues, reset, handleSubmit, control } = useForm({ + defaultValues: getDefaultAppForm() + }); + const { + fields: variables, + append: appendVariable, + remove: removeVariable, + replace: replaceVariables + } = useFieldArray({ + control, + name: 'variables' + }); + const { fields: kbList, replace: replaceKbList } = useFieldArray({ + control, + name: 'kb.list' + }); + + const { + isOpen: isOpenKbSelect, + onOpen: onOpenKbSelect, + onClose: onCloseKbSelect + } = useDisclosure(); + const { + isOpen: isOpenKbParams, + onOpen: onOpenKbParams, + onClose: onCloseKbParams + } = useDisclosure(); + + const chatModelSelectList = useMemo(() => { + return chatModelList.map((item) => ({ + value: item.model, + label: `${item.name} (${formatPrice(item.price, 1000)} 元/1k tokens)` + })); + }, [refresh]); + const tokenLimit = useMemo(() => { + return ( + chatModelList.find((item) => item.model === getValues('chatModel.model'))?.contextMaxToken || + 4000 + ); + }, [getValues, refresh]); + const selectedKbList = useMemo( + () => myKbList.filter((item) => kbList.find((kb) => kb.kbId === item._id)), + [myKbList, kbList] + ); + + const appModule2Form = useCallback(() => { + const formVal = appModules2Form(appDetail.modules); + reset(formVal); + setRefresh((state) => !state); + }, [appDetail.modules, reset]); + + const { mutate: onSubmitSave, isLoading: isSaving } = useRequest({ + mutationFn: async (data: EditFormType) => { + const modules = appForm2Modules(data); + + await updateAppDetail(appDetail._id, { + modules + }); + }, + successToast: '保存成功', + errorToast: '保存出现异常' + }); + + useEffect(() => { + appModule2Form(); + }, [appModule2Form]); + + const BoxStyles: BoxProps = { + bg: 'myWhite.300', + px: 4, + py: 3, + borderRadius: 'lg', + border: theme.borders.base + }; + const BoxBtnStyles: BoxProps = { + cursor: 'pointer', + px: 3, + py: '2px', + borderRadius: 'md', + _hover: { + bg: 'myGray.200' + } + }; + + return ( + + + + 应用配置 + + + + + + + + {/* variable */} + + + + + 变量 + + setEditVariable(addVariable())}> + + 新增 + + + + + + + + + + + + + + + {variables.map((item, index) => ( + + + + + + + ))} + +
变量名变量 key必填
{item.label} {item.key}{item.required ? '✔' : ''} + setEditVariable(item)} + /> + removeVariable(index)} + /> +
+
+
+
+ + + + + AI 配置 + + + + + 对话模型 + + { + setValue('chatModel.model', val); + const maxToken = + chatModelList.find((item) => item.model === getValues('chatModel.model')) + ?.contextMaxToken || 4000; + const token = maxToken / 2; + setValue('chatModel.maxToken', token); + setRefresh(!refresh); + }} + /> + + + + 温度 + + + { + setValue('chatModel.temperature', e); + setRefresh(!refresh); + }} + /> + + + + + 回复上限 + + + { + setValue('chatModel.maxToken', val); + setRefresh(!refresh); + }} + /> + + + + + 提示词 + + + + + + + + + 限定词 + + + + + + + + + {/* kb */} + + + + + 知识库 + + + + 选择 + + + + 参数 + + + + 相似度: {getValues('kb.searchSimilarity')}, 单次搜索数量: {getValues('kb.searchLimit')}, + 空搜索时拒绝回复: {getValues('kb.searchEmptyText') !== '' ? 'true' : 'false'} + + + {selectedKbList.map((item) => ( + + + + {item.name} + + + ))} + + + + {/* welcome */} + + + + 对话开场白 + + + + + + +
+ + + + + + + + ); +}; diff --git a/client/src/pages/app/detail/components/OverView.tsx b/client/src/pages/app/detail/components/OverView.tsx index e87edff9d..e3db24876 100644 --- a/client/src/pages/app/detail/components/OverView.tsx +++ b/client/src/pages/app/detail/components/OverView.tsx @@ -7,12 +7,13 @@ import { useToast } from '@/hooks/useToast'; import { useLoading } from '@/hooks/useLoading'; import { delModelById } from '@/api/app'; import { useConfirm } from '@/hooks/useConfirm'; -import dynamic from 'next/dynamic'; import { AppSchema } from '@/types/mongoSchema'; +import dynamic from 'next/dynamic'; import Avatar from '@/components/Avatar'; import MyIcon from '@/components/Icon'; import TotalUsage from './Charts/TotalUsage'; +import BasicEdit from './BasicEdit'; const InfoModal = dynamic(() => import('./InfoModal')); @@ -54,98 +55,100 @@ const OverView = ({ appId }: { appId: string }) => { return ( - - - - - 基本信息 - - - - - - {appDetail.name} - - } - variant={'base'} - borderRadius={'md'} - aria-label={'delete'} - _hover={{ - bg: 'myGray.100', - color: 'red.600' - }} - onClick={openConfirm(handleDelModel)} - /> - - - {appDetail.intro || '快来给应用一个介绍~'} - - - - - - - + + + + 基本信息 - - - 近 14 日消费 + + + + + {appDetail.name} + + } + variant={'base'} + borderRadius={'md'} + aria-label={'delete'} + _hover={{ + bg: 'myGray.100', + color: 'red.600' + }} + onClick={openConfirm(handleDelModel)} + /> + + + {appDetail.intro || '快来给应用一个介绍~'} - - - + + + + + + + + + + 近 14 日消费 + + + + + + + {settingAppInfo && ( - setSettingAppInfo(undefined)} - onSuccess={refetch} - /> + setSettingAppInfo(undefined)} /> )} - diff --git a/client/src/pages/app/detail/components/VariableEditModal.tsx b/client/src/pages/app/detail/components/VariableEditModal.tsx new file mode 100644 index 000000000..81b6d2a89 --- /dev/null +++ b/client/src/pages/app/detail/components/VariableEditModal.tsx @@ -0,0 +1,214 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + NumberInput, + NumberInputField, + NumberInputStepper, + NumberIncrementStepper, + NumberDecrementStepper, + Flex, + Switch, + Input, + Grid, + FormControl, + useTheme +} from '@chakra-ui/react'; +import { SmallAddIcon } from '@chakra-ui/icons'; +import { VariableInputEnum } from '@/constants/app'; +import type { VariableItemType } from '@/types/app'; +import MyIcon from '@/components/Icon'; +import { useForm } from 'react-hook-form'; +import { useFieldArray } from 'react-hook-form'; +import { customAlphabet } from 'nanoid'; +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); + +const VariableTypeList = [ + { label: '文本', icon: 'settingLight', key: VariableInputEnum.input }, + { label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select } +]; + +export type VariableFormType = { + variable: VariableItemType; +}; + +const VariableEditModal = ({ + defaultVariable, + onClose, + onSubmit +}: { + defaultVariable: VariableItemType; + onClose: () => void; + onSubmit: (data: VariableFormType) => void; +}) => { + const theme = useTheme(); + const [refresh, setRefresh] = useState(false); + + const { reset, getValues, setValue, register, control, handleSubmit } = useForm( + { + defaultValues: { + variable: defaultVariable + } + } + ); + const { + fields: selectEnums, + append: appendEnums, + remove: removeEnums + } = useFieldArray({ + control, + name: 'variable.enums' + }); + + return ( + + + + + + 变量设置 + + + + 必填 + + + + 变量名 + + + + 变量 key + + + + + 字段类型 + + + {VariableTypeList.map((item) => ( + { + setValue('variable.type', item.key); + setRefresh(!refresh); + } + })} + > + + {item.label} + + ))} + + + {getValues('variable.type') === VariableInputEnum.input && ( + <> + + 最大长度 + + + + + + + + + + + + )} + + {getValues('variable.type') === VariableInputEnum.select && ( + <> + + 选项 + + + {selectEnums.map((item, i) => ( + + + + + removeEnums(i)} + /> + + ))} + + + + )} + + + + + + + + + ); +}; + +export default React.memo(VariableEditModal); + +export const defaultVariable: VariableItemType = { + id: nanoid(), + key: 'key', + label: 'label', + type: VariableInputEnum.input, + required: true, + maxLen: 50, + enums: [{ value: '' }] +}; +export const addVariable = () => { + const newVariable = { ...defaultVariable, id: nanoid() }; + return newVariable; +}; diff --git a/client/src/pages/app/detail/index.tsx b/client/src/pages/app/detail/index.tsx index 87c5f1f7c..00cb7d9d0 100644 --- a/client/src/pages/app/detail/index.tsx +++ b/client/src/pages/app/detail/index.tsx @@ -54,7 +54,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { const tabList = useMemo( () => [ - { label: '概览', id: TabEnum.overview, icon: 'overviewLight' }, + { label: '基础', id: TabEnum.overview, icon: 'overviewLight' }, { label: '高级编排', id: TabEnum.settings, icon: 'settingLight' }, { label: '链接分享', id: TabEnum.share, icon: 'shareLight' }, { label: 'API访问', id: TabEnum.API, icon: 'apiLight' }, @@ -150,7 +150,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { appId && - router.push({ + router.replace({ pathname: '/app/detail', query: { appId } }) diff --git a/client/src/pages/chat/components/SliderApps.tsx b/client/src/pages/chat/components/SliderApps.tsx index 24949645f..091c9f8b1 100644 --- a/client/src/pages/chat/components/SliderApps.tsx +++ b/client/src/pages/chat/components/SliderApps.tsx @@ -21,7 +21,7 @@ const SliderApps = ({ appId }: { appId: string }) => { px={3} borderRadius={'md'} _hover={{ bg: 'myGray.200' }} - onClick={() => router.push('/app/list')} + onClick={() => router.back()} > { top: item.top }))} onChangeChat={(chatId) => { - router.push({ + router.replace({ query: { chatId: chatId || '', appId diff --git a/client/src/store/user.ts b/client/src/store/user.ts index 1ee9f765b..dd4005af2 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -2,11 +2,11 @@ import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import type { UserType, UserUpdateParams } from '@/types/user'; -import { getMyModels, getModelById } from '@/api/app'; +import { getMyModels, getModelById, putAppById } from '@/api/app'; import { formatPrice } from '@/utils/user'; import { getTokenLogin } from '@/api/user'; import { defaultApp } from '@/constants/model'; -import { AppListItemType } from '@/types/app'; +import { AppListItemType, AppUpdateParams } from '@/types/app'; import type { KbItemType, KbListItemType } from '@/types/plugin'; import { getKbList, getKbById } from '@/api/plugins/kb'; import { defaultKbDetail } from '@/constants/kb'; @@ -22,6 +22,7 @@ type State = { loadMyModels: () => Promise; appDetail: AppSchema; loadAppDetail: (id: string, init?: boolean) => Promise; + updateAppDetail(appId: string, data: AppUpdateParams): Promise; clearAppModules(): void; // kb myKbList: KbListItemType[]; @@ -79,6 +80,15 @@ export const useUserStore = create()( }); return res; }, + async updateAppDetail(appId: string, data: AppUpdateParams) { + await putAppById(appId, data); + set((state) => { + state.appDetail = { + ...state.appDetail, + ...data + }; + }); + }, clearAppModules() { set((state) => { state.appDetail = { diff --git a/client/src/types/plugin.d.ts b/client/src/types/plugin.d.ts index 8a04d1f0a..5d38733f5 100644 --- a/client/src/types/plugin.d.ts +++ b/client/src/types/plugin.d.ts @@ -1,5 +1,7 @@ import type { kbSchema } from './mongoSchema'; +export type SelectedKbType = { kbId: string }[]; + export type KbListItemType = { _id: string; avatar: string; diff --git a/client/src/utils/app.ts b/client/src/utils/app.ts new file mode 100644 index 000000000..21371bcd0 --- /dev/null +++ b/client/src/utils/app.ts @@ -0,0 +1,611 @@ +import type { AppModuleItemType, VariableItemType } from '@/types/app'; +import { chatModelList, vectorModelList } from '@/store/static'; +import { FlowModuleTypeEnum } from '@/constants/flow'; +import { FlowInputItemType } from '@/types/flow'; +import { SystemInputEnum } from '@/constants/app'; +import type { SelectedKbType } from '@/types/plugin'; +import { + VariableModule, + UserGuideModule, + ChatModule, + HistoryModule, + UserInputModule, + KBSearchModule, + AnswerModule +} from '@/constants/flow/ModuleTemplate'; +import { rawSearchKey } from '@/constants/chat'; + +export type EditFormType = { + chatModel: { + model: string; + systemPrompt: string; + limitPrompt: string; + temperature: number; + maxToken: number; + frequency: number; + presence: number; + }; + kb: { + list: SelectedKbType; + searchSimilarity: number; + searchLimit: number; + searchEmptyText: string; + }; + guide: { + welcome: { + text: string; + }; + }; + variables: VariableItemType[]; +}; +export const getDefaultAppForm = (): EditFormType => { + const defaultChatModel = chatModelList[0]; + const defaultVectorModel = vectorModelList[0]; + return { + chatModel: { + model: defaultChatModel.model, + systemPrompt: '', + limitPrompt: '', + temperature: 0, + maxToken: defaultChatModel.contextMaxToken / 2, + frequency: 0.5, + presence: -0.5 + }, + kb: { + list: [], + searchSimilarity: 0.8, + searchLimit: 5, + searchEmptyText: '' + }, + guide: { + welcome: { + text: '' + } + }, + variables: [] + }; +}; + +export const appModules2Form = (modules: AppModuleItemType[]) => { + const defaultAppForm = getDefaultAppForm(); + const updateVal = ({ + formKey, + inputs, + key + }: { + formKey: string; + inputs: FlowInputItemType[]; + key: string; + }) => { + const propertyPath = formKey.split('.'); + let currentObj: any = defaultAppForm; + for (let i = 0; i < propertyPath.length - 1; i++) { + currentObj = currentObj[propertyPath[i]]; + } + + const val = + inputs.find((item) => item.key === key)?.value || + currentObj[propertyPath[propertyPath.length - 1]]; + + currentObj[propertyPath[propertyPath.length - 1]] = val; + }; + + modules.forEach((module) => { + if (module.flowType === FlowModuleTypeEnum.chatNode) { + updateVal({ + formKey: 'chatModel.model', + inputs: module.inputs, + key: 'model' + }); + updateVal({ + formKey: 'chatModel.temperature', + inputs: module.inputs, + key: 'temperature' + }); + updateVal({ + formKey: 'chatModel.maxToken', + inputs: module.inputs, + key: 'maxToken' + }); + updateVal({ + formKey: 'chatModel.systemPrompt', + inputs: module.inputs, + key: 'systemPrompt' + }); + updateVal({ + formKey: 'chatModel.limitPrompt', + inputs: module.inputs, + key: 'limitPrompt' + }); + } else if (module.flowType === FlowModuleTypeEnum.kbSearchNode) { + updateVal({ + formKey: 'kb.list', + inputs: module.inputs, + key: 'kbList' + }); + updateVal({ + formKey: 'kb.searchSimilarity', + inputs: module.inputs, + key: 'similarity' + }); + updateVal({ + formKey: 'kb.searchLimit', + inputs: module.inputs, + key: 'limit' + }); + // empty text + const emptyOutputs = module.outputs.find((item) => item.key === 'isEmpty')?.targets || []; + const emptyOutput = emptyOutputs[0]; + if (emptyOutput) { + const target = modules.find((item) => item.moduleId === emptyOutput.moduleId); + defaultAppForm.kb.searchEmptyText = + target?.inputs?.find((item) => item.key === 'answerText')?.value || ''; + } + } else if (module.flowType === FlowModuleTypeEnum.userGuide) { + const val = + module.inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value || ''; + if (val) { + defaultAppForm.guide.welcome = { + text: val + }; + } + } else if (module.flowType === FlowModuleTypeEnum.variable) { + defaultAppForm.variables = + module.inputs.find((item) => item.key === SystemInputEnum.variables)?.value || []; + } + }); + + return defaultAppForm; +}; + +const chatModelInput = (formData: EditFormType) => [ + { + key: 'model', + type: 'custom', + label: '对话模型', + value: formData.chatModel.model, + list: chatModelList.map((item) => ({ + label: item.name, + value: item.model + })), + connected: false + }, + { + key: 'temperature', + type: 'custom', + label: '温度', + value: formData.chatModel.temperature, + min: 0, + max: 10, + step: 1, + markList: [ + { + label: '严谨', + value: 0 + }, + { + label: '发散', + value: 10 + } + ], + connected: false + }, + { + key: 'maxToken', + type: 'custom', + label: '回复上限', + value: formData.chatModel.maxToken, + min: 100, + max: 16000, + step: 50, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '16000', + value: 16000 + } + ], + connected: false + }, + { + key: 'systemPrompt', + type: 'textarea', + label: '系统提示词', + description: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + placeholder: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + value: formData.chatModel.systemPrompt, + connected: false + }, + { + key: 'limitPrompt', + type: 'textarea', + label: '限定词', + description: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + placeholder: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + value: formData.chatModel.limitPrompt, + connected: false + }, + { + key: 'quotePrompt', + type: 'target', + label: '引用内容', + connected: formData.kb.list.length > 0 + }, + { + key: 'history', + type: 'target', + label: '聊天记录', + connected: true + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } +]; +const welcomeTemplate = (formData: EditFormType) => + formData.guide?.welcome?.text + ? [ + { + ...UserGuideModule, + inputs: [ + { + key: 'welcomeText', + type: 'input', + label: '开场白', + value: formData.guide.welcome.text, + connected: false + } + ], + outputs: [], + position: { + x: 447.98520778293346, + y: 721.4016845336229 + }, + moduleId: 'v7lq0x' + } + ] + : []; +const variableTemplate = (formData: EditFormType) => + formData.variables.length > 0 + ? [ + { + ...VariableModule, + inputs: [ + { + key: 'variables', + type: 'systemInput', + label: '变量输入', + value: formData.variables, + connected: false + } + ], + outputs: [], + position: { + x: 444.0369195277651, + y: 1008.5185781784537 + }, + moduleId: '7blchb' + } + ] + : []; +const simpleChatTemplate = (formData: EditFormType) => [ + { + ...UserInputModule, + inputs: [ + { + key: 'userChatInput', + type: 'systemInput', + label: '用户问题', + connected: false + } + ], + outputs: [ + { + key: 'userChatInput', + label: '用户问题', + type: 'source', + targets: [ + { + moduleId: 'chatModule', + key: 'userChatInput' + } + ] + } + ], + position: { + x: 464.32198615344566, + y: 1602.2698463081606 + }, + moduleId: '7z5g5h' + }, + { + ...HistoryModule, + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 10, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: 'chatModule', + key: 'history' + } + ] + } + ], + position: { + x: 452.5466249541586, + y: 1276.3930310334215 + }, + moduleId: 'xj0c9p' + }, + { + ...ChatModule, + inputs: chatModelInput(formData), + outputs: [ + { + key: 'answerText', + label: '模型回复', + description: '直接响应,无需配置', + type: 'hidden', + targets: [] + } + ], + position: { + x: 981.9682828103937, + y: 890.014595014464 + }, + moduleId: 'chatModule' + } +]; +const kbTemplate = (formData: EditFormType) => [ + { + ...UserInputModule, + inputs: [ + { + key: 'userChatInput', + type: 'systemInput', + label: '用户问题', + connected: false + } + ], + outputs: [ + { + key: 'userChatInput', + label: '用户问题', + type: 'source', + targets: [ + { + moduleId: 'chatModule', + key: 'userChatInput' + }, + { + moduleId: 'q9v14m', + key: 'userChatInput' + } + ] + } + ], + position: { + x: 464.32198615344566, + y: 1602.2698463081606 + }, + moduleId: '7z5g5h' + }, + { + ...HistoryModule, + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 10, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: 'chatModule', + key: 'history' + } + ] + } + ], + position: { + x: 452.5466249541586, + y: 1276.3930310334215 + }, + moduleId: 'xj0c9p' + }, + { + ...KBSearchModule, + inputs: [ + { + key: 'kbList', + type: 'custom', + label: '关联的知识库', + value: formData.kb.list, + list: [], + connected: true + }, + { + key: 'similarity', + type: 'custom', + label: '相似度', + value: formData.kb.searchSimilarity, + min: 0, + max: 1, + step: 0.01, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '1', + value: 1 + } + ], + connected: false + }, + { + key: 'limit', + type: 'custom', + label: '单次搜索上限', + description: '最多取 n 条记录作为本次问题引用', + value: formData.kb.searchLimit, + min: 1, + max: 20, + step: 1, + markList: [ + { + label: '1', + value: 1 + }, + { + label: '20', + value: 20 + } + ], + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: false + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'isEmpty', + label: '搜索结果为空', + type: 'source', + targets: [ + { + moduleId: 'emptyText', + key: 'switch' + } + ] + }, + { + key: 'quotePrompt', + label: '引用内容', + description: '搜索结果为空时不返回', + type: 'source', + targets: [ + { + moduleId: 'chatModule', + key: 'quotePrompt' + } + ] + } + ], + position: { + x: 956.0838440206068, + y: 887.462827870246 + }, + moduleId: 'q9v14m' + }, + ...(formData.kb.searchEmptyText + ? [ + { + ...AnswerModule, + inputs: [ + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'answerText', + value: formData.kb.searchEmptyText, + type: 'input', + label: '回复的内容', + connected: false + } + ], + outputs: [], + position: { + x: 1570.7651822907549, + y: 637.8753731306779 + }, + moduleId: 'emptyText' + } + ] + : []), + { + ...ChatModule, + inputs: chatModelInput(formData), + outputs: [ + { + key: 'answerText', + label: '模型回复', + description: '直接响应,无需配置', + type: 'hidden', + targets: [] + } + ], + position: { + x: 1551.71405495818, + y: 977.4911578918461 + }, + moduleId: 'chatModule' + } +]; + +export const appForm2Modules = (formData: EditFormType) => { + const modules = [ + ...welcomeTemplate(formData), + ...variableTemplate(formData), + ...(formData.kb.list.length > 0 ? kbTemplate(formData) : simpleChatTemplate(formData)) + ]; + + return modules as AppModuleItemType[]; +};