feat: agent and ui

This commit is contained in:
archer
2023-07-06 19:35:02 +08:00
parent 46f20c7dc3
commit 23642af6e2
44 changed files with 588 additions and 1148 deletions

View File

@@ -79,7 +79,7 @@ export async function classifyQuestion({
properties: {
type: {
type: 'string',
description: agents.map((item) => `${item.desc},返回: '${item.key}'`).join('; '),
description: agents.map((item) => `${item.value},返回: '${item.key}'`).join('; '),
enum: agents.map((item) => item.key)
}
},
@@ -106,7 +106,10 @@ export async function classifyQuestion({
if (!arg.type) {
throw new Error('');
}
console.log(arg.type);
console.log(
'意图结果',
agents.findIndex((item) => item.key === arg.type)
);
return {
[arg.type]: 1

View File

@@ -10,6 +10,7 @@ import type { ChatItemType } from '@/types/chat';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt';
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
import { SpecificInputEnum } from '@/constants/app';
export type Props = {
model: `${OpenAiChatEnum}`;
@@ -22,7 +23,7 @@ export type Props = {
systemPrompt?: string;
limitPrompt?: string;
};
export type Response = { answer: string };
export type Response = { [SpecificInputEnum.answerText]: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -132,7 +133,8 @@ export async function chatCompletion({
const chatAPI = getOpenAIApi();
/* count response max token */
const promptsToken = modelToolMap[model].countTokens({
const promptsToken = modelToolMap.countTokens({
model,
messages: filterMessages
});
maxToken = maxToken + promptsToken > modelTokenLimit ? modelTokenLimit - promptsToken : maxToken;
@@ -143,8 +145,8 @@ export async function chatCompletion({
temperature: Number(temperature || 0),
max_tokens: maxToken,
messages: adaptMessages,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
// frequency_penalty: 0.5, // 越大,重复内容越少
// presence_penalty: -0.5, // 越大,越容易出现新内容
stream
},
{
@@ -184,7 +186,7 @@ export async function chatCompletion({
})();
return {
answer
answerText: answer
};
}

View File

@@ -92,8 +92,9 @@ export async function kbSearch({
const searchRes: QuoteItemType[] = res?.[2]?.rows || [];
// filter part quote by maxToken
const sliceResult = modelToolMap['gpt-3.5-turbo']
const sliceResult = modelToolMap
.tokenSlice({
model: 'gpt-3.5-turbo',
maxToken,
messages: searchRes.map((item, i) => ({
obj: ChatRoleEnum.System,

View File

@@ -10,12 +10,7 @@ import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import {
kbChatAppDemo,
chatAppDemo,
SpecificInputEnum,
AppModuleItemTypeEnum
} from '@/constants/app';
import { SpecificInputEnum, AppModuleItemTypeEnum } from '@/constants/app';
import { model, Types } from 'mongoose';
import { moduleFetch } from '@/service/api/request';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
@@ -42,7 +37,6 @@ export type ChatResponseType = {
quoteLen?: number;
};
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
@@ -117,7 +111,6 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
},
stream
});
console.log(responseData, answerText);
// save chat
if (typeof chatId === 'string') {
@@ -354,7 +347,7 @@ function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
})),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.type === FlowOutputItemTypeEnum.answer,
answer: item.key === SpecificInputEnum.answerText,
response: item.response,
value: undefined,
targets: item.targets

View File

@@ -1,394 +0,0 @@
import React, { useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import {
Card,
Flex,
Box,
Button,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
ModalFooter,
ModalCloseButton,
Grid,
useTheme,
IconButton,
Tooltip,
Textarea
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import Avatar from '@/components/Avatar';
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { putAppById } from '@/api/app';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useForm } from 'react-hook-form';
import MyIcon from '@/components/Icon';
import MySlider from '@/components/Slider';
const Kb = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const router = useRouter();
const { toast } = useToast();
const { appDetail, loadKbList, loadAppDetail } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
const [refresh, setRefresh] = useState(false);
const { register, reset, getValues, setValue } = useForm({
defaultValues: {
searchSimilarity: appDetail.chat.searchSimilarity,
searchLimit: appDetail.chat.searchLimit,
searchEmptyText: appDetail.chat.searchEmptyText
}
});
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenEditParams,
onOpen: onOpenEditParams,
onClose: onCloseEditParams
} = useDisclosure();
const onchangeKb = useCallback(
async (
data: {
relatedKbs?: string[];
searchSimilarity?: number;
searchLimit?: number;
searchEmptyText?: string;
} = {}
) => {
setIsLoading(true);
try {
await putAppById(modelId, {
chat: {
...appDetail.chat,
...data
}
});
loadAppDetail(modelId, true);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setIsLoading(false);
},
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
);
// init kb select list
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
return (
<Box position={'relative'} px={5} minH={'50vh'}>
<Box fontWeight={'bold'}>({appDetail.chat?.relatedKbs.length})</Box>
{(() => {
const kbs =
appDetail.chat?.relatedKbs
?.map((id) => kbList.find((kb) => kb._id === id))
.filter((item) => item) || [];
return (
<Grid
mt={2}
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={[3, 4]}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
reset({
searchSimilarity: appDetail.chat.searchSimilarity,
searchLimit: appDetail.chat.searchLimit,
searchEmptyText: appDetail.chat.searchEmptyText
});
onOpenEditParams();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<MyIcon name={'edit'} w={'14px'} color={'myGray.600'} />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
: {appDetail.chat.searchSimilarity}, :{' '}
{appDetail.chat.searchLimit}, :{' '}
{appDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
</Flex>
</Card>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
setSelectedIdList(
appDetail.chat?.relatedKbs ? [...appDetail.chat?.relatedKbs] : []
);
onOpenKbSelect();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<AddIcon />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
AI
</Flex>
</Card>
{kbs.map((item) =>
item ? (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
_hover={{
boxShadow: 'lg',
'& .detailBtn': {
display: 'block'
},
'& .delete': {
display: 'block'
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['26px', '32px', '38px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
<Flex mt={3} alignItems={'flex-end'} justifyContent={'flex-end'} h={'30px'}>
<Button
mr={3}
className="detailBtn"
display={['flex', 'none']}
variant={'base'}
size={'sm'}
onClick={() => router.push(`/kb?kbId=${item._id}`)}
>
</Button>
<IconButton
className="delete"
display={['flex', 'none']}
icon={<DeleteIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
_hover={{ color: 'red.600' }}
onClick={() => {
const ids = appDetail.chat?.relatedKbs
? [...appDetail.chat.relatedKbs]
: [];
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
onchangeKb({ relatedKbs: ids });
}}
/>
</Flex>
</Card>
) : null
)}
</Grid>
);
})()}
{/* select kb modal */}
<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}
>
{kbList.map((item) => (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
order={appDetail.chat?.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) => kbList.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();
onchangeKb({ relatedKbs: selectedIdList });
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* edit mode */}
<Modal isOpen={isOpenEditParams} onClose={onCloseEditParams}>
<ModalOverlay />
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex pt={3} pb={5}>
<Box flex={'0 0 100px'}>
<Tooltip label={'高相似度推荐0.8及以上。'}>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</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={onCloseEditParams}>
</Button>
<Button
onClick={() => {
onCloseEditParams();
onchangeKb({
searchSimilarity: getValues('searchSimilarity'),
searchLimit: getValues('searchLimit'),
searchEmptyText: getValues('searchEmptyText')
});
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Kb;

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import {
Flex,
Box,
Tooltip,
Button,
TableContainer,
Table,
@@ -37,6 +36,7 @@ import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
import { useForm } from 'react-hook-form';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/app';
import MyTooltip from '@/components/MyTooltip';
const Share = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
@@ -112,9 +112,9 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Tooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<MyTooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<QuestionOutlineIcon ml={1} />
</Tooltip>
</MyTooltip>
</Box>
<Button
variant={'base'}

View File

@@ -10,13 +10,7 @@ const NodeAnswer = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard
minW={'400px'}
logo={'/icon/logo.png'}
name={'SSE 响应'}
moduleId={moduleId}
{...props}
>
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />

View File

@@ -0,0 +1,136 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import { Box, Input, Button, Flex } from '@chakra-ui/react';
import NodeCard from './modules/NodeCard';
import { FlowModuleItemType } from '@/types/flow';
import Divider from './modules/Divider';
import Container from './modules/Container';
import RenderInput from './render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { Handle, Position } from 'reactflow';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import { FlowOutputItemTypeEnum } from '@/constants/flow';
const NodeCQNode = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
agents: ({
key: agentKey,
value: agents = []
}: {
key: string;
value?: ClassifyQuestionAgentItemType[];
}) => (
<Box>
{agents.map((item, i) => (
<Flex key={item.key} mb={4} alignItems={'center'}>
<MyIcon
mr={2}
name={'minus'}
w={'14px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'myGray.900' }}
onClick={() => {
const newInputValue = agents.filter((input) => input.key !== item.key);
const newOutputVal = outputs.filter((output) => output.key !== item.key);
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: newInputValue
});
onChangeNode({
moduleId,
type: 'outputs',
key: agentKey,
value: newOutputVal
});
}}
/>
<Box flex={1}>
<Box flex={1}>{i + 1}</Box>
<Box position={'relative'}>
<Input
mt={1}
defaultValue={item.value}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
moduleId,
key: agentKey,
value: newVal
});
}}
/>
<Handle
style={{
top: '50%',
right: '-14px',
transform: 'translate(50%,-50%)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
type="source"
id={item.key}
position={Position.Right}
/>
</Box>
</Box>
</Flex>
))}
<Button
onClick={() => {
const key = nanoid();
const newInputValue = agents.concat({ value: '', key });
const newOutputValue = outputs.concat({
key,
label: '',
type: FlowOutputItemTypeEnum.hidden,
targets: []
});
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: newInputValue
});
onChangeNode({
moduleId,
type: 'outputs',
key: agentKey,
value: newOutputValue
});
}}
>
</Button>
</Box>
)
}}
/>
</Container>
</NodeCard>
);
};
export default React.memo(NodeCQNode);

View File

@@ -26,7 +26,7 @@ const NodeKbSearch = ({
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
kb_ids: ({ key, value, onChangeNode }) => (
kb_ids: ({ key, value }) => (
<KBSelect
relatedKbs={value}
onChange={(e) => {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
import React, { useRef } from 'react';
import { Box, Flex, useOutsideClick } from '@chakra-ui/react';
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
import type { AppModuleTemplateItemType } from '@/types/app';
import type { XYPosition } from 'reactflow';
@@ -7,62 +7,85 @@ import Avatar from '@/components/Avatar';
const ModuleStoreList = ({
isOpen,
onAddNode
onAddNode,
onClose
}: {
isOpen: boolean;
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
onClose: () => void;
}) => {
const ContextMenuRef = useRef(null);
useOutsideClick({
ref: ContextMenuRef,
handler: () => {
onClose();
}
});
return (
<Flex
flexDirection={'column'}
position={'absolute'}
top={'65px'}
left={0}
h={isOpen ? '90%' : '0'}
w={isOpen ? '360px' : '0'}
bg={'white'}
zIndex={1}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'20px'}
overflow={'hidden'}
transition={'.2s ease'}
px={'15px'}
userSelect={'none'}
>
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
{ModuleTemplates.map((item) =>
item.list.map((item) => (
<Flex
key={item.name}
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'md'}
draggable
onDragEnd={(e) => {
if (e.clientX < 400) return;
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
>
<Avatar src={item.logo} w={'34px'} />
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{item.name}</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
{item.intro}
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
right={0}
bottom={0}
></Box>
<Flex
zIndex={3}
ref={ContextMenuRef}
flexDirection={'column'}
position={'absolute'}
top={'65px'}
left={0}
h={isOpen ? '90%' : '0'}
w={isOpen ? '360px' : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'20px'}
overflow={'hidden'}
transition={'.2s ease'}
px={'15px'}
userSelect={'none'}
>
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
{ModuleTemplates.map((item) =>
item.list.map((item) => (
<Flex
key={item.name}
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'md'}
draggable
onDragEnd={(e) => {
// if (e.clientX < 400) return;
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
>
<Avatar src={item.logo} w={'34px'} borderRadius={'0'} />
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{item.name}</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
{item.intro}
</Box>
</Box>
</Box>
</Flex>
))
)}
</Box>
</Flex>
</Flex>
))
)}
</Box>
</Flex>
</>
);
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Box, Tooltip } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
const Label = ({
required = false,
@@ -19,9 +20,9 @@ const Label = ({
</Box>
)}
{description && (
<Tooltip label={description}>
<MyTooltip label={description}>
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
</Tooltip>
</MyTooltip>
)}
</Box>
);

View File

@@ -4,7 +4,6 @@ import {
Box,
Textarea,
Input,
Tooltip,
NumberInput,
NumberInputField,
NumberInputStepper,
@@ -16,6 +15,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Handle, Position } from 'reactflow';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
const Label = ({
required = false,
@@ -34,9 +34,9 @@ const Label = ({
</Box>
)}
{description && (
<Tooltip label={description}>
<MyTooltip label={description}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</Tooltip>
</MyTooltip>
)}
</Box>
);
@@ -49,14 +49,7 @@ const RenderBody = ({
}: {
flowInputList: FlowInputItemType[];
moduleId: string;
CustomComponent?: Record<
string,
(e: {
key: string;
value: any;
onChangeNode: FlowModuleItemType['onChangeNode'];
}) => React.ReactNode
>;
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
onChangeNode: FlowModuleItemType['onChangeNode'];
}) => {
return (
@@ -65,9 +58,11 @@ const RenderBody = ({
(item) =>
item.type !== FlowInputItemTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
<Label required={item.required} description={item.description}>
{item.label}
</Label>
{!!item.label && (
<Label required={item.required} description={item.description}>
{item.label}
</Label>
)}
<Box mt={2} className={'nodrag'}>
{item.type === FlowInputItemTypeEnum.numberInput && (
<NumberInput
@@ -151,9 +146,7 @@ const RenderBody = ({
</Box>
)}
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
<>
{CustomComponent[item.key]({ key: item.key, value: item.value, onChangeNode })}
</>
<>{CustomComponent[item.key]({ ...item })}</>
)}
{item.type === FlowInputItemTypeEnum.target && (
<Handle

View File

@@ -1,9 +1,10 @@
import React from 'react';
import type { FlowOutputItemType } from '@/types/flow';
import { Box, Tooltip, Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { FlowOutputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Handle, Position } from 'reactflow';
import MyTooltip from '@/components/MyTooltip';
const Label = ({
children,
@@ -14,9 +15,9 @@ const Label = ({
}) => (
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
{description && (
<Tooltip label={description}>
<MyTooltip label={description}>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</Tooltip>
</MyTooltip>
)}
{children}
</Flex>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import ReactFlow, {
Background,
Controls,
@@ -7,7 +7,8 @@ import ReactFlow, {
useNodesState,
useEdgesState,
XYPosition,
Connection
Connection,
useViewport
} from 'reactflow';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
@@ -47,6 +48,9 @@ const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput')
const TemplateList = dynamic(() => import('./components/TemplateList'), {
ssr: false
});
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
ssr: false
});
import 'reactflow/dist/style.css';
import styles from './index.module.scss';
@@ -60,14 +64,18 @@ const nodeTypes = {
[FlowModuleTypeEnum.chatNode]: NodeChat,
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
[FlowModuleTypeEnum.answerNode]: NodeAnswer
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestionNode]: NodeCQNode
};
const edgeTypes = {
buttonedge: ButtonEdge
};
type Props = { app: AppSchema; onBack: () => void };
const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
const AppEdit = ({ app, onBack }: Props) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const theme = useTheme();
const { x, y, zoom } = useViewport();
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const {
@@ -77,24 +85,33 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
} = useDisclosure();
const onChangeNode = useCallback(
({ moduleId, key, value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
if (type === 'inputs') {
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.map((item) => {
if (item.key === key) {
return {
...item,
[valueKey]: value
};
}
return item;
})
}
};
}
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.map((item) => {
if (item.key === key) {
return {
...item,
[valueKey]: value
};
}
return item;
})
outputs: value
}
};
})
@@ -111,12 +128,17 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
);
const onAddNode = useCallback(
({ template, position }: { template: AppModuleTemplateItemType; position: XYPosition }) => {
if (!reactFlowWrapper.current) return;
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
setNodes((state) =>
state.concat(
appModule2FlowNode({
item: {
...template,
position,
position: { x: mouseX, y: mouseY },
moduleId: nanoid()
},
onChangeNode,
@@ -125,7 +147,7 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
)
);
},
[onChangeNode, onDelNode, setNodes]
[onChangeNode, onDelNode, setNodes, x, y, zoom]
);
const onDelConnect = useCallback(
@@ -245,6 +267,13 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
borderRadius={'lg'}
isLoading={isLoading}
aria-label={'save'}
bg={'myBlue.200'}
variant={'base'}
border={'none'}
color={'myGray.900'}
_hover={{
bg: 'myBlue.300'
}}
onClick={onclickSave}
/>
</Flex>
@@ -270,43 +299,50 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'1px 1px 6px #4e83fd'}
onClick={() => (isOpenTemplate ? onCloseTemplate() : onOpenTemplate())}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<ReactFlowProvider>
<ReactFlow
className={styles.panel}
nodes={nodes}
edges={edges}
minZoom={0.4}
maxZoom={1.5}
fitView
defaultEdgeOptions={edgeOptions}
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={(connect) => {
connect.sourceHandle &&
connect.targetHandle &&
onConnect({
connect
});
}}
>
<Background />
<Controls
position={'bottom-center'}
style={{ display: 'flex' }}
showInteractive={false}
/>
</ReactFlow>
</ReactFlowProvider>
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} />
<ReactFlow
ref={reactFlowWrapper}
className={styles.panel}
nodes={nodes}
edges={edges}
minZoom={0.4}
maxZoom={1.5}
fitView
defaultEdgeOptions={edgeOptions}
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={(connect) => {
connect.sourceHandle &&
connect.targetHandle &&
onConnect({
connect
});
}}
>
<Background />
<Controls
position={'bottom-center'}
style={{ display: 'flex' }}
showInteractive={false}
/>
</ReactFlow>
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
</Box>
</Flex>
);
};
export default AppEdit;
const Flow = (data: Props) => (
<ReactFlowProvider>
<AppEdit {...data} />
</ReactFlowProvider>
);
export default Flow;

View File

@@ -173,7 +173,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
boxShadow={'sm'}
{...(getValues('templateId') === item.id
? {
bg: 'myBlue.300'
bg: 'myWhite.600'
}
: {
_hover: {

View File

@@ -120,7 +120,13 @@ const MyApps = () => {
}}
/>
</Flex>
<Box className={styles.intro} py={2} fontSize={'sm'} color={'myGray.600'}>
<Box
className={styles.intro}
py={2}
wordBreak={'break-all'}
fontSize={'sm'}
color={'myGray.600'}
>
{app.intro || '这个应用还没写介绍~'}
</Box>
</Card>

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { Box, Flex, Button, Tooltip, Card } from '@chakra-ui/react';
import { Box, Flex, Button, Card } from '@chakra-ui/react';
import type { ShareAppItem } from '@/types/app';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import styles from '../index.module.scss';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
const ShareModelList = ({
models = [],
@@ -44,7 +45,7 @@ const ShareModelList = ({
{model.name}
</Box>
</Flex>
<Tooltip label={model.intro}>
<MyTooltip label={model.intro}>
<Box
className={styles.intro}
flex={1}
@@ -55,7 +56,7 @@ const ShareModelList = ({
>
{model.intro || '这个应用还没有介绍~'}
</Box>
</Tooltip>
</MyTooltip>
<Flex justifyContent={'space-between'}>
<Flex

View File

@@ -23,7 +23,6 @@ import {
DrawerOverlay,
DrawerContent,
Card,
Tooltip,
useOutsideClick,
useTheme
} from '@chakra-ui/react';
@@ -48,6 +47,7 @@ import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal';
import { HUMAN_ICON } from '@/constants/chat';
import MyTooltip from '@/components/MyTooltip';
const Markdown = dynamic(async () => await import('@/components/Markdown'));
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
@@ -701,7 +701,7 @@ const Chat = () => {
{item.obj === 'Human' && <Box flex={1} />}
{/* avatar */}
<Menu autoSelect={false} isLazy>
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
<MyTooltip label={item.obj === 'AI' ? '应用详情' : ''}>
<MenuButton
as={Box}
{...(item.obj === 'AI'
@@ -730,7 +730,7 @@ const Chat = () => {
h={['20px', '34px']}
/>
</MenuButton>
</Tooltip>
</MyTooltip>
{!isPc && <RenderContextMenu history={item} index={index} AiDetail />}
</Menu>
{/* message */}

View File

@@ -22,7 +22,6 @@ import {
DrawerOverlay,
DrawerContent,
Card,
Tooltip,
useOutsideClick,
useTheme,
Input,
@@ -49,6 +48,7 @@ import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import { HUMAN_ICON } from '@/constants/chat';
import MyTooltip from '@/components/MyTooltip';
const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
loading: () => <Loading fixed={false} />,
@@ -619,7 +619,7 @@ const Chat = () => {
{item.obj === 'Human' && <Box flex={1} />}
{/* avatar */}
<Menu autoSelect={false} isLazy>
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
<MyTooltip label={item.obj === 'AI' ? '应用详情' : ''}>
<MenuButton
as={Box}
{...(item.obj === 'AI'
@@ -642,7 +642,7 @@ const Chat = () => {
h={['20px', '34px']}
/>
</MenuButton>
</Tooltip>
</MyTooltip>
{!isPc && <RenderContextMenu history={item} index={index} />}
</Menu>
{/* message */}

View File

@@ -7,7 +7,7 @@ import React, {
ForwardedRef
} from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, Button, FormControl, IconButton, Tooltip, Input, Card } from '@chakra-ui/react';
import { Box, Flex, Button, FormControl, IconButton, Input, Card } from '@chakra-ui/react';
import { QuestionOutlineIcon, DeleteIcon } from '@chakra-ui/icons';
import { delKbById, putKbById } from '@/api/plugins/kb';
import { useSelectFile } from '@/hooks/useSelectFile';
@@ -19,6 +19,7 @@ import { compressImg } from '@/utils/file';
import type { KbItemType } from '@/types/plugin';
import Avatar from '@/components/Avatar';
import Tag from '@/components/Tag';
import MyTooltip from '@/components/MyTooltip';
export interface ComponentRef {
initInput: (tags: string) => void;
@@ -173,9 +174,9 @@ const Info = (
<Flex mt={8} alignItems={'center'} w={'100%'} maxW={'350px'} flexWrap={'wrap'}>
<Box flex={'0 0 90px'} w={0}>
<Tooltip label={'用空格隔开多个标签,便于搜索'}>
<MyTooltip label={'用空格隔开多个标签,便于搜索'}>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</MyTooltip>
</Box>
<Input
flex={1}

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, useTheme, Input, IconButton, Tooltip } from '@chakra-ui/react';
import { Box, Flex, useTheme, Input, IconButton } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import { postCreateKb } from '@/api/plugins/kb';
@@ -10,6 +10,7 @@ import { useUserStore } from '@/store/user';
import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar';
import Tag from '@/components/Tag';
import MyTooltip from '@/components/MyTooltip';
const KbList = ({ kbId }: { kbId: string }) => {
const theme = useTheme();
@@ -78,7 +79,7 @@ const KbList = ({ kbId }: { kbId: string }) => {
/>
)}
</Flex>
<Tooltip label={'新建一个知识库'}>
<MyTooltip label={'新建一个知识库'}>
<IconButton
h={'32px'}
icon={<AddIcon />}
@@ -86,7 +87,7 @@ const KbList = ({ kbId }: { kbId: string }) => {
variant={'base'}
onClick={handleCreateModel}
/>
</Tooltip>
</MyTooltip>
</Flex>
<Box flex={'1 0 0'} h={0} pl={[0, 2]} overflowY={'scroll'} userSelect={'none'}>
{kbs.map((item) => (

View File

@@ -1,388 +0,0 @@
import React, { useCallback, useState, useMemo } from 'react';
import {
Box,
Flex,
Button,
FormControl,
Input,
Textarea,
Divider,
Tooltip
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { delModelById, putAppById } from '@/api/app';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { useConfirm } from '@/hooks/useConfirm';
import { ChatModelMap, chatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import type { AppSchema } from '@/types/mongoSchema';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
const systemPromptTip =
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。';
const limitPromptTip =
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
const Settings = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { Loading, setIsLoading } = useLoading();
const { userInfo, appDetail, myApps, loadAppDetail, refreshModel, setLastModelId } =
useUserStore();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该应用?'
});
const [btnLoading, setBtnLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const {
register,
setValue,
getValues,
formState: { errors },
reset,
handleSubmit
} = useForm({
defaultValues: appDetail
});
const isOwner = useMemo(
() => appDetail.userId === userInfo?._id,
[appDetail.userId, userInfo?._id]
);
const tokenLimit = useMemo(() => {
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
if (max < getValues('chat.maxToken')) {
setValue('chat.maxToken', max);
}
return max;
}, [getValues, setValue, refresh]);
// 提交保存模型修改
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
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
},
[refreshModel, toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [errors, toast]);
const saveUpdateModel = useCallback(
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[handleSubmit, saveSubmitError, saveSubmitSuccess]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!appDetail) return;
setIsLoading(true);
try {
await delModelById(appDetail._id);
toast({
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(appDetail._id);
router.replace(`/model?modelId=${myApps[1]?._id}`);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, '头像选择异常'),
status: 'warning'
});
}
},
[setValue, toast]
);
// load model data
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
onSuccess(res) {
res && reset(res);
modelId && setLastModelId(modelId);
setRefresh(!refresh);
},
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});
return (
<Box
pb={3}
px={[5, '25px', '50px']}
fontSize={['sm', 'lg']}
maxW={['auto', '800px']}
position={'relative'}
>
<Flex alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Avatar
src={getValues('avatar')}
w={['32px', '40px']}
h={['32px', '40px']}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Textarea
rows={4}
maxLength={500}
placeholder={'给你的 AI 应用一个介绍'}
{...register('intro')}
></Textarea>
</Flex>
<Divider mt={5} />
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<MySelect
width={['100%', '300px']}
value={getValues('chat.chatModel')}
list={chatModelList.map((item) => ({
value: item.chatModel,
label: `${item.name} (${formatPrice(
ChatModelMap[item.chatModel]?.price,
1000
)} 元/1k tokens)`
}))}
onchange={(val: any) => {
setValue('chat.chatModel', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex alignItems={'center'} my={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '严谨', value: 0 },
{ label: '发散', value: 10 }
]}
width={['95%', '280px']}
min={0}
max={10}
value={getValues('chat.temperature')}
onChange={(val) => {
setValue('chat.temperature', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={12} mb={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: `${tokenLimit}`, value: tokenLimit }
]}
width={['95%', '280px']}
min={100}
max={tokenLimit}
step={50}
value={getValues('chat.maxToken')}
onChange={(val) => {
setValue('chat.maxToken', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex mt={10} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
<Tooltip label={systemPromptTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</Tooltip>
</Box>
<Textarea
rows={8}
placeholder={systemPromptTip}
{...register('chat.systemPrompt')}
></Textarea>
</Flex>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
<Tooltip label={limitPromptTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</Tooltip>
</Box>
<Textarea
rows={5}
placeholder={limitPromptTip}
{...register('chat.limitPrompt')}
></Textarea>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
<Button
mr={3}
w={'120px'}
size={['sm', 'md']}
isLoading={btnLoading}
isDisabled={!isOwner}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
{isOwner ? '保存' : '仅读,无法修改'}
</Button>
<Button
mr={3}
w={'100px'}
size={['sm', 'md']}
variant={'base'}
color={'myBlue.600'}
borderColor={'myBlue.600'}
isLoading={btnLoading}
onClick={async () => {
try {
router.prefetch('/chat');
await saveUpdateModel();
} catch (error) {}
router.push(`/chat?modelId=${modelId}`);
}}
>
</Button>
{isOwner && (
<Button
colorScheme={'gray'}
variant={'base'}
size={['sm', 'md']}
isLoading={btnLoading}
_hover={{ color: 'red.600' }}
onClick={openConfirm(handleDelModel)}
>
</Button>
)}
</Flex>
<File onSelect={onSelectFile} />
<ConfirmChild />
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Settings;