feat: overview setting

This commit is contained in:
archer
2023-07-21 17:35:15 +08:00
parent b7b20a353f
commit fdcf53ea38
22 changed files with 1920 additions and 419 deletions

View File

@@ -650,9 +650,9 @@ const ChatBox = (
<Box
py={'18px'}
position={'relative'}
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
boxShadow={`0 0 10px rgba(0,0,0,0.2)`}
borderTop={['1px solid', 0]}
borderTopColor={'gray.200'}
borderTopColor={'myGray.200'}
borderRadius={['none', 'md']}
backgroundColor={'white'}
>

View File

@@ -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: [],

View File

@@ -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',

View File

@@ -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;
})
);
}

View File

@@ -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<Response> {
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};

View File

@@ -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<VariableItemType>();
useQuery(['initkb', appId], () => loadKbList());
const [refresh, setRefresh] = useState(false);
const { openConfirm, ConfirmChild } = useConfirm({
title: '警告',
content: '保存后将会覆盖高级编排配置,请确保该应用未使用高级编排功能。'
});
const { register, setValue, getValues, reset, handleSubmit, control } = useForm<EditFormType>({
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 (
<Flex
flexDirection={'column'}
h={'100%'}
borderRight={'1.5px solid'}
borderColor={'myGray.200'}
pt={4}
>
<Flex overflowY={'auto'} pr={4} justifyContent={'space-between'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
<MyTooltip label={'仅包含基础功能,复杂 agent 功能请使用高级编排。'}>
<QuestionOutlineIcon ml={2} fontSize={'md'} />
</MyTooltip>
</Box>
<Button
isLoading={isSaving}
fontSize={'sm'}
onClick={openConfirm(handleSubmit((data) => onSubmitSave(data)))}
>
</Button>
</Flex>
<Box flex={'1 0 0'} my={4} pr={4} overflowY={'auto'}>
{/* variable */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Avatar src={'/imgs/module/variable.png'} objectFit={'contain'} w={'18px'} />
<Box ml={2} flex={1}>
</Box>
<Flex {...BoxBtnStyles} onClick={() => setEditVariable(addVariable())}>
+&ensp;
</Flex>
</Flex>
<Box
mt={2}
borderRadius={'lg'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom="none"
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th></Th>
<Th> key</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={item.id}>
<Td>{item.label} </Td>
<Td>{item.key}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => setEditVariable(item)}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => removeVariable(index)}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box>
<Box mt={5} {...BoxStyles}>
<Flex alignItems={'center'}>
<Avatar src={'/imgs/module/AI.png'} w={'18px'} />
<Box ml={2}>AI </Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px']} flexShrink={0}>
</Box>
<MySelect
width={['100%', '300px']}
value={getValues('chatModel.model')}
list={chatModelSelectList}
onchange={(val: any) => {
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);
}}
/>
</Flex>
<Flex alignItems={'center'} my={10}>
<Box w={['60px', '100px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '严谨', value: 0 },
{ label: '发散', value: 10 }
]}
width={'95%'}
min={0}
max={10}
value={getValues('chatModel.temperature')}
onChange={(e) => {
setValue('chatModel.temperature', e);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={12} mb={10}>
<Box w={['60px', '100px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: `${tokenLimit}`, value: tokenLimit }
]}
width={'95%'}
min={100}
max={tokenLimit}
step={50}
value={getValues('chatModel.maxToken')}
onChange={(val) => {
setValue('chatModel.maxToken', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex mt={10} alignItems={'flex-start'}>
<Box w={['60px', '100px']} flexShrink={0}>
<MyTooltip label={ChatModelSystemTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Box>
<Textarea
rows={5}
placeholder={ChatModelSystemTip}
borderColor={'myGray.100'}
{...register('chatModel.systemPrompt')}
></Textarea>
</Flex>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px']} flexShrink={0}>
<MyTooltip label={ChatModelLimitTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Box>
<Textarea
rows={5}
placeholder={ChatModelLimitTip}
borderColor={'myGray.100'}
{...register('chatModel.limitPrompt')}
></Textarea>
</Flex>
</Box>
{/* kb */}
<Box mt={5} {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<Avatar src={'/imgs/module/db.png'} w={'18px'} />
<Box ml={2}></Box>
</Flex>
<Flex alignItems={'center'} mr={3} {...BoxBtnStyles} onClick={onOpenKbSelect}>
<SmallAddIcon />
</Flex>
<Flex alignItems={'center'} {...BoxBtnStyles} onClick={onOpenKbParams}>
<MyIcon name={'edit'} w={'14px'} mr={1} />
</Flex>
</Flex>
<Flex mt={1} color={'myGray.600'} fontSize={['sm', 'md']}>
: {getValues('kb.searchSimilarity')}, : {getValues('kb.searchLimit')},
: {getValues('kb.searchEmptyText') !== '' ? 'true' : 'false'}
</Flex>
<Grid templateColumns={['1fr', 'repeat(2,1fr)']} my={2} gridGap={[2, 4]}>
{selectedKbList.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
</Flex>
))}
</Grid>
</Box>
{/* welcome */}
<Box mt={5} {...BoxStyles}>
<Flex alignItems={'center'}>
<Avatar src={'/imgs/module/userGuide.png'} w={'18px'} />
<Box mx={2}></Box>
<MyTooltip label={welcomeTextTip}>
<QuestionOutlineIcon />
</MyTooltip>
</Flex>
<Textarea
mt={2}
rows={5}
placeholder={welcomeTextTip}
borderColor={'myGray.100'}
{...register('guide.welcome.text')}
/>
</Box>
</Box>
<ConfirmChild />
{editVariable && (
<VariableEditModal
defaultVariable={editVariable}
onClose={() => setEditVariable(undefined)}
onSubmit={({ variable }) => {
const record = variables.find((item) => item.id === variable.id);
if (record) {
replaceVariables(
variables.map((item) => (item.id === variable.id ? variable : item))
);
} else {
appendVariable(variable);
}
setEditVariable(undefined);
}}
/>
)}
{isOpenKbSelect && (
<KBSelectModal
kbList={myKbList}
activeKbs={selectedKbList.map((item) => ({ kbId: item._id }))}
onClose={onCloseKbSelect}
onChange={replaceKbList}
/>
)}
{isOpenKbParams && (
<KbParamsModal
searchEmptyText={getValues('kb.searchEmptyText')}
searchLimit={getValues('kb.searchLimit')}
searchSimilarity={getValues('kb.searchSimilarity')}
onClose={onCloseKbParams}
onChange={({ searchEmptyText, searchLimit, searchSimilarity }) => {
setValue('kb.searchEmptyText', searchEmptyText);
setValue('kb.searchLimit', searchLimit);
setValue('kb.searchSimilarity', searchSimilarity);
setRefresh((state) => !state);
}}
/>
)}
</Flex>
);
};
const ChatTest = ({ appId }: { appId: string }) => {
const { appDetail } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const [modules, setModules] = useState<AppModuleItemType[]>([]);
const startChat = useCallback(
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = messages.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, rawSearch } = await streamFetch({
url: '/api/chat/chatTest',
data: {
history,
prompt: messages[messages.length - 2].content,
modules,
variables,
appId,
appName: `调试-${appDetail.name}`
},
onMessage: generatingMessage,
abortSignal: controller
});
return { responseText, rawSearch };
},
[modules, appId, appDetail.name]
);
const resetChatBox = useCallback(() => {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}, []);
useEffect(() => {
const formVal = appModules2Form(appDetail.modules);
setModules(appForm2Modules(formVal));
resetChatBox();
}, [appDetail, resetChatBox]);
return (
<Flex flexDirection={'column'} h={'100%'} pl={4} py={4}>
<Flex>
<Box fontSize={['md', 'xl']} fontWeight={'bold'} flex={1}>
</Box>
<MyTooltip label={'重置'}>
<IconButton
className="chat"
size={'sm'}
icon={<MyIcon name={'clearLight'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
resetChatBox();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appAvatar={appDetail.avatar}
{...getSpecialModule(modules)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
);
};
const BasicEdit = ({ appId }: { appId: string }) => {
return (
<Grid gridTemplateColumns={['1fr', '550px 1fr']} h={'100%'}>
<Settings appId={appId} />
<ChatTest appId={appId} />
</Grid>
);
};
export default BasicEdit;

View File

@@ -192,7 +192,7 @@ const TokenUsage = ({ appId }: { appId: string }) => {
}, [screenWidth]);
return (
<Box ref={Dom} w={'100%'} flex={'1 0 0'} h={'100%'} position={'relative'}>
<Box ref={Dom} w={'100%'} flex={'1 0 0'} h={'100%'} minH={'150px'} position={'relative'}>
<Loading fixed={false} />
</Box>
);

View File

@@ -1,12 +1,73 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/flow';
import { Flex, Box, Button, useTheme, useDisclosure, Grid } from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import NodeCard from '../modules/NodeCard';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import KBSelect from '../Plugins/KBSelect';
import { KBSelectModal } from '../../../KBSelectModal';
import type { SelectedKbType } from '@/types/plugin';
import Avatar from '@/components/Avatar';
const KBSelect = ({
activeKbs = [],
onChange
}: {
activeKbs: SelectedKbType;
onChange: (e: SelectedKbType) => void;
}) => {
const theme = useTheme();
const { myKbList, loadKbList } = useUserStore();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const showKbList = useMemo(
() => myKbList.filter((item) => activeKbs.find((kb) => kb.kbId === item._id)),
[myKbList, activeKbs]
);
useQuery(['initkb'], loadKbList);
return (
<>
<Grid gridTemplateColumns={'1fr 1fr'} gridGap={4}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{showKbList.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenKbSelect && (
<KBSelectModal
kbList={myKbList}
activeKbs={activeKbs}
onChange={onChange}
onClose={onCloseKbSelect}
/>
)}
</>
);
};
const NodeKbSearch = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
@@ -20,9 +81,9 @@ const NodeKbSearch = ({
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
kb_ids: ({ key, value }) => (
kbList: ({ key, value }) => (
<KBSelect
relatedKbs={value}
activeKbs={value}
onChange={(e) => {
onChangeNode({
moduleId,

View File

@@ -7,12 +7,8 @@ import { FlowModuleItemType } from '@/types/flow';
import Container from '../modules/Container';
import { SystemInputEnum } from '@/constants/app';
import MyIcon from '@/components/Icon';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyTooltip from '@/components/MyTooltip';
const welcomePlaceholder =
'每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]: 用户点击后可以直接发送该问题';
import { welcomeTextTip } from '@/constants/flow/ModuleTemplate';
const NodeUserGuide = ({
data: { inputs, outputs, onChangeNode, ...props }
@@ -30,7 +26,7 @@ const NodeUserGuide = ({
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
<Box></Box>
<MyTooltip label={welcomePlaceholder}>
<MyTooltip label={welcomeTextTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
@@ -40,7 +36,7 @@ const NodeUserGuide = ({
resize={'both'}
defaultValue={welcomeText}
bg={'myWhite.500'}
placeholder={welcomePlaceholder}
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId: props.moduleId,

View File

@@ -45,7 +45,7 @@ const VariableTypeList = [
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
];
const defaultVariable: VariableItemType = {
export const defaultVariable: VariableItemType = {
id: nanoid(),
key: 'key',
label: 'label',
@@ -66,10 +66,6 @@ const NodeUserGuide = ({
?.value as VariableItemType[]) || [],
[inputs]
);
const welcomeText = useMemo(
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
[inputs]
);
const [refresh, setRefresh] = useState(false);
const { isOpen, onClose, onOpen } = useDisclosure();

View File

@@ -1,143 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Card,
Flex,
Box,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
ModalFooter,
ModalCloseButton,
useTheme,
useDisclosure,
Grid
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import Avatar from '@/components/Avatar';
const KBSelect = ({
relatedKbs = [],
onChange
}: {
relatedKbs: string[];
onChange: (e: string[]) => void;
}) => {
const theme = useTheme();
const { myKbList, loadKbList } = useUserStore();
const [selectedIdList, setSelectedIdList] = useState<string[]>(relatedKbs);
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const showKbList = useMemo(
() => myKbList.filter((item) => relatedKbs.includes(item._id)),
[myKbList, relatedKbs]
);
useEffect(() => {
loadKbList();
}, []);
return (
<>
<Grid gridTemplateColumns={'1fr 1fr'} gridGap={4}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{showKbList.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
))}
</Grid>
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent
display={'flex'}
flexDirection={'column'}
w={'800px'}
maxW={'90vw'}
h={['90vh', 'auto']}
>
<ModalHeader>({selectedIdList.length})</ModalHeader>
<ModalCloseButton />
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{myKbList.map((item) => (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
order={relatedKbs.includes(item._id) ? 0 : 1}
_hover={{
boxShadow: 'md'
}}
{...(selectedIdList.includes(item._id)
? {
bg: 'myBlue.300'
}
: {})}
onClick={() => {
let ids = [...selectedIdList];
if (!selectedIdList.includes(item._id)) {
ids = ids.concat(item._id);
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
}
ids = ids.filter((id) => myKbList.find((item) => item._id === id));
setSelectedIdList(ids);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
onCloseKbSelect();
onChange(selectedIdList);
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default KBSelect;

View File

@@ -17,10 +17,11 @@ import {
import { useForm } from 'react-hook-form';
import { AppSchema } from '@/types/mongoSchema';
import { useToast } from '@/hooks/useToast';
import { delModelById, putAppById } from '@/api/app';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { useUserStore } from '@/store/user';
import { useRequest } from '@/hooks/useRequest';
import Avatar from '@/components/Avatar';
const InfoModal = ({
@@ -30,9 +31,11 @@ const InfoModal = ({
}: {
defaultApp: AppSchema;
onClose: () => void;
onSuccess: () => void;
onSuccess?: () => void;
}) => {
const { toast } = useToast();
const { updateAppDetail } = useUserStore();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
@@ -47,31 +50,30 @@ const InfoModal = ({
} = useForm({
defaultValues: defaultApp
});
const [btnLoading, setBtnLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: AppSchema) => {
setBtnLoading(true);
try {
await putAppById(data._id, {
name: data.name,
avatar: data.avatar,
intro: data.intro,
chat: data.chat,
share: data.share
});
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
mutationFn: async (data: AppSchema) => {
await updateAppDetail(data._id, {
name: data.name,
avatar: data.avatar,
intro: data.intro,
chat: data.chat,
share: data.share
});
},
[toast]
);
onSuccess() {
onSuccess && onSuccess();
onClose();
toast({
title: '更新成功',
status: 'success'
});
},
errorToast: '更新失败'
});
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
// deep search message
@@ -91,7 +93,7 @@ const InfoModal = ({
}, [errors, toast]);
const saveUpdateModel = useCallback(
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
() => handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)(),
[handleSubmit, saveSubmitError, saveSubmitSuccess]
);
@@ -165,22 +167,7 @@ const InfoModal = ({
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button
isLoading={btnLoading}
onClick={async () => {
try {
await saveUpdateModel();
onSuccess();
onClose();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
}
}}
>
<Button isLoading={btnLoading} onClick={saveUpdateModel}>
</Button>
</ModalFooter>

View File

@@ -0,0 +1,214 @@
import React, { useState } from 'react';
import {
Card,
Flex,
Box,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
ModalFooter,
ModalCloseButton,
useTheme,
Textarea
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { KbListItemType } from '@/types/plugin';
import { useForm } from 'react-hook-form';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { SelectedKbType } from '@/types/plugin';
import MySlider from '@/components/Slider';
export type KbParamsType = {
searchSimilarity: number;
searchLimit: number;
searchEmptyText: string;
};
export const KBSelectModal = ({
kbList,
activeKbs = [],
onChange,
onClose
}: {
kbList: KbListItemType[];
activeKbs: SelectedKbType;
onChange: (e: SelectedKbType) => void;
onClose: () => void;
}) => {
const theme = useTheme();
const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs);
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent
display={'flex'}
flexDirection={'column'}
w={'800px'}
maxW={'90vw'}
h={['90vh', 'auto']}
>
<ModalHeader>({selectedKbList.length})</ModalHeader>
<ModalCloseButton />
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{kbList.map((item) =>
(() => {
const selected = !!selectedKbList.find((kb) => kb.kbId === item._id);
const active = !!activeKbs.find((kb) => kb.kbId === item._id);
return (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
order={active ? 0 : 1}
_hover={{
boxShadow: 'md'
}}
{...(selected
? {
bg: 'myBlue.300'
}
: {})}
onClick={() => {
if (selected) {
setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id));
} else {
setSelectedKbList((state) => [...state, { kbId: item._id }]);
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
</Card>
);
})()
)}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
onClose();
onChange(selectedKbList);
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export const KbParamsModal = ({
searchEmptyText,
searchLimit,
searchSimilarity,
onClose,
onChange
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
const [refresh, setRefresh] = useState(false);
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
defaultValues: {
searchEmptyText,
searchLimit,
searchSimilarity
}
});
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex pt={3} pb={5}>
<Box flex={'0 0 100px'}>
<MyTooltip label={'高相似度推荐0.8及以上。'}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Box>
<MySlider
markList={[
{ label: '0', value: 0 },
{ label: '1', value: 1 }
]}
min={0}
max={1}
step={0.01}
value={getValues('searchSimilarity')}
onChange={(val) => {
setValue('searchSimilarity', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex py={8}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
]}
min={1}
max={20}
value={getValues('searchLimit')}
onChange={(val) => {
setValue('searchLimit', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex pt={3}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<Textarea
rows={5}
maxLength={500}
placeholder={
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
}
{...register('searchEmptyText')}
></Textarea>
</Box>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button
onClick={() => {
onClose();
handleSubmit(onChange)();
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

View File

@@ -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 (
<Flex h={'100%'} flexDirection={'column'} position={'relative'}>
<Box w={'100%'} pt={[0, 7]} px={[3, 5, 8]}>
<Grid gridTemplateColumns={['1fr', 'repeat(2,1fr)']} gridGap={[2, 4, 6]}>
<Box>
<Box mb={2} fontSize={['md', 'xl']}>
</Box>
<Box
border={theme.borders.base}
borderRadius={'lg'}
px={5}
py={4}
bg={'myBlue.100'}
position={'relative'}
>
<Flex alignItems={'center'} py={2}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
{appDetail.name}
</Box>
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'sm'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
_hover={{
bg: 'myGray.100',
color: 'red.600'
}}
onClick={openConfirm(handleDelModel)}
/>
</Flex>
<Box className={'textEllipsis3'} py={3} wordBreak={'break-all'} color={'myGray.600'}>
{appDetail.intro || '快来给应用一个介绍~'}
</Box>
<Flex>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
</Button>
<Button
mx={3}
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
onClick={() => {
router.replace({
query: {
appId,
currentTab: 'share'
}
});
}}
>
</Button>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
onClick={() => setSettingAppInfo(appDetail)}
>
</Button>
</Flex>
</Box>
<Grid
gridTemplateColumns={['1fr', 'repeat(2,1fr)']}
gridGap={[2, 4, 6]}
pt={[0, 7]}
px={[3, 5, 8]}
>
<Box>
<Box mb={2} fontSize={['md', 'xl']}>
</Box>
<Flex flexDirection={'column'}>
<Box mb={2} fontSize={['md', 'xl']}>
14
<Box
border={theme.borders.base}
borderRadius={'lg'}
px={5}
py={4}
bg={'myBlue.100'}
position={'relative'}
>
<Flex alignItems={'center'} py={2}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
{appDetail.name}
</Box>
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'sm'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
_hover={{
bg: 'myGray.100',
color: 'red.600'
}}
onClick={openConfirm(handleDelModel)}
/>
</Flex>
<Box className={'textEllipsis3'} py={3} wordBreak={'break-all'} color={'myGray.600'}>
{appDetail.intro || '快来给应用一个介绍~'}
</Box>
<TotalUsage appId={appId} />
</Flex>
</Grid>
<Flex>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
</Button>
<Button
mx={3}
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
onClick={() => {
router.replace({
query: {
appId,
currentTab: 'share'
}
});
}}
>
</Button>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
onClick={() => setSettingAppInfo(appDetail)}
>
</Button>
</Flex>
</Box>
</Box>
<Flex flexDirection={'column'}>
<Box mb={2} fontSize={['md', 'xl']}>
14
</Box>
<TotalUsage appId={appId} />
</Flex>
</Grid>
<Box flex={'1 0 0'} h={0} mt={4} borderTop={theme.borders.base} px={[3, 5, 8]}>
<BasicEdit appId={appId} />
</Box>
{settingAppInfo && (
<InfoModal
defaultApp={settingAppInfo}
onClose={() => setSettingAppInfo(undefined)}
onSuccess={refetch}
/>
<InfoModal defaultApp={settingAppInfo} onClose={() => setSettingAppInfo(undefined)} />
)}
<ConfirmChild />
<Loading fixed={false} />
</Flex>

View File

@@ -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<VariableFormType>(
{
defaultValues: {
variable: defaultVariable
}
}
);
const {
fields: selectEnums,
append: appendEnums,
remove: removeEnums
} = useFieldArray({
control,
name: 'variable.enums'
});
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent maxW={'Min(400px,90vw)'}>
<ModalHeader display={'flex'}>
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box w={'70px'}></Box>
<Switch {...register('variable.required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}></Box>
<Input {...register('variable.label', { required: '变量名不能为空' })} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}> key</Box>
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
</Flex>
<Box mt={5} mb={2}>
</Box>
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
{VariableTypeList.map((item) => (
<Flex
key={item.key}
px={4}
py={1}
border={theme.borders.base}
borderRadius={'md'}
cursor={'pointer'}
{...(item.key === getValues('variable.type')
? {
bg: 'myWhite.600'
}
: {
_hover: {
boxShadow: 'md'
},
onClick: () => {
setValue('variable.type', item.key);
setRefresh(!refresh);
}
})}
>
<MyIcon name={item.icon as any} w={'16px'} />
<Box ml={3}>{item.label}</Box>
</Flex>
))}
</Grid>
{getValues('variable.type') === VariableInputEnum.input && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
<NumberInput max={100} min={1} step={1} position={'relative'}>
<NumberInputField
{...register('variable.maxLen', {
min: 1,
max: 100,
valueAsNumber: true
})}
max={100}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Box>
</>
)}
{getValues('variable.type') === VariableInputEnum.select && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
{selectEnums.map((item, i) => (
<Flex key={item.id} mb={2} alignItems={'center'}>
<FormControl>
<Input
{...register(`variable.enums.${i}.value`, {
required: '选项内容不能为空'
})}
/>
</FormControl>
<MyIcon
ml={3}
name={'delete'}
w={'16px'}
cursor={'pointer'}
p={2}
borderRadius={'lg'}
_hover={{ bg: 'red.100' }}
onClick={() => removeEnums(i)}
/>
</Flex>
))}
</Box>
<Button
variant={'solid'}
w={'100%'}
textAlign={'left'}
leftIcon={<SmallAddIcon />}
bg={'myGray.100 !important'}
onClick={() => appendEnums({ value: '' })}
>
</Button>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
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;
};

View File

@@ -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}` }) => {
<Tabs
mx={'auto'}
mt={2}
w={'300px'}
w={'100%'}
list={tabList}
size={'sm'}
activeId={currentTab}

View File

@@ -79,7 +79,7 @@ const ChatHistorySlider = ({
cursor={appId ? 'pointer' : 'default'}
onClick={() =>
appId &&
router.push({
router.replace({
pathname: '/app/detail',
query: { appId }
})

View File

@@ -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()}
>
<IconButton
mr={3}

View File

@@ -239,7 +239,7 @@ const Chat = () => {
top: item.top
}))}
onChangeChat={(chatId) => {
router.push({
router.replace({
query: {
chatId: chatId || '',
appId

View File

@@ -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<null>;
appDetail: AppSchema;
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
clearAppModules(): void;
// kb
myKbList: KbListItemType[];
@@ -79,6 +80,15 @@ export const useUserStore = create<State>()(
});
return res;
},
async updateAppDetail(appId: string, data: AppUpdateParams) {
await putAppById(appId, data);
set((state) => {
state.appDetail = {
...state.appDetail,
...data
};
});
},
clearAppModules() {
set((state) => {
state.appDetail = {

View File

@@ -1,5 +1,7 @@
import type { kbSchema } from './mongoSchema';
export type SelectedKbType = { kbId: string }[];
export type KbListItemType = {
_id: string;
avatar: string;

611
client/src/utils/app.ts Normal file
View File

@@ -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[];
};