This commit is contained in:
archer
2023-07-05 23:29:28 +08:00
parent 8e9816d648
commit 46f20c7dc3
18 changed files with 352 additions and 92 deletions

View File

@@ -1,8 +1,9 @@
import { GET, POST, DELETE, PUT } from './request';
import type { AppSchema } from '@/types/mongoSchema';
import type { AppUpdateParams } from '@/types/app';
import type { AppModuleItemType, AppUpdateParams } from '@/types/app';
import { RequestPaging } from '../types/index';
import type { AppListResponse } from './response/app';
import type { Props as CreateAppProps } from '@/pages/api/app/create';
/**
* 获取模型列表
@@ -12,7 +13,7 @@ export const getMyModels = () => GET<AppListResponse>('/app/list');
/**
* 创建一个模型
*/
export const postCreateModel = (data: { name: string }) => POST<string>('/app/create', data);
export const postCreateApp = (data: CreateAppProps) => POST<string>('/app/create', data);
/**
* 根据 ID 删除模型

View File

@@ -20,8 +20,8 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
{
label: '应用',
icon: 'tabbarModel',
link: `/model`,
activeLink: ['/model'],
link: `/app/list`,
activeLink: ['/app/list'],
unread: 0
},
{

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Box, useTheme, type BoxProps } from '@chakra-ui/react';
const PageContainer = ({ children, ...props }: BoxProps) => {
const theme = useTheme();
return (
<Box bg={'myGray.100'} h={'100%'} p={[0, 5]} {...props}>
<Box
flex={1}
h={'100%'}
bg={'white'}
borderRadius={['', '2xl']}
border={['', theme.borders.lg]}
overflowY={'auto'}
>
{children}
</Box>
</Box>
);
};
export default PageContainer;

View File

@@ -42,13 +42,9 @@ const SlideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) =
px={3}
mb={2}
alignItems={'center'}
_hover={{
bg: 'myWhite.600'
}}
{...(activeId === item.id
? {
// backgroundImage: 'linear-gradient(to right, #85b1ff 0%, #EBF7FD 100%)',
bg: ' myBlue.300',
bg: ' myBlue.300 !important',
fontWeight: 'bold',
color: 'myBlue.700 ',
cursor: 'default'
@@ -56,6 +52,9 @@ const SlideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) =
: {
cursor: 'pointer'
})}
_hover={{
bg: 'myWhite.600'
}}
onClick={() => {
if (activeId === item.id) return;
onChange(item.id);

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
const NonePage = () => {
const router = useRouter();
useEffect(() => {
router.push('/model');
router.push('/app/list');
}, [router]);
return <div></div>;

View File

@@ -50,10 +50,10 @@ function App({ Component, pageProps }: AppProps) {
<link rel="icon" href="/favicon.ico" />
</Head>
<Script src="/js/particles.js"></Script>
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="afterInteractive"></Script>
{baiduTongji && <Script src="/js/baidutongji.js" strategy="afterInteractive"></Script>}
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
{baiduTongji && <Script src="/js/baidutongji.js" strategy="lazyOnload"></Script>}
{googleVerKey && (
<>
<Script

View File

@@ -4,14 +4,19 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { App } from '@/service/models/app';
import { AppModuleItemType } from '@/types/app';
export type Props = {
name: string;
avatar?: string;
modules: AppModuleItemType[];
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name } = req.body as {
name: string;
};
const { name, avatar, modules } = req.body as Props;
if (!name) {
if (!name || !Array.isArray(modules)) {
throw new Error('缺少参数');
}
@@ -30,8 +35,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 创建模型
const response = await App.create({
avatar,
name,
userId
userId,
modules
});
jsonRes(res, {

View File

@@ -20,8 +20,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { Loading, setIsLoading } = useLoading();
const { userInfo, appDetail, myApps, loadAppDetail, refreshModel, setLastModelId } =
useUserStore();
const { userInfo, appDetail, myApps, loadAppDetail, setLastModelId } = useUserStore();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
@@ -61,8 +60,6 @@ const Settings = ({ modelId }: { modelId: string }) => {
chat: data.chat,
share: data.share
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
@@ -71,7 +68,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
}
setBtnLoading(false);
},
[refreshModel, toast]
[toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
@@ -106,8 +103,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(appDetail._id);
router.replace(`/model?modelId=${myApps[1]?._id}`);
router.replace(`/app/list`);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
@@ -115,7 +111,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
});
}
setIsLoading(false);
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
}, [appDetail, setIsLoading, toast, router]);
const onSelectFile = useCallback(
async (e: File[]) => {
@@ -152,7 +148,6 @@ const Settings = ({ modelId }: { modelId: string }) => {
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});

View File

@@ -9,6 +9,7 @@ import SlideTabs from '@/components/SlideTabs';
import Settings from './components/Settings';
import { defaultApp } from '@/constants/model';
import Avatar from '@/components/Avatar';
import PageContainer from '@/components/PageContainer';
const EditApp = dynamic(() => import('./components/edit'), {
ssr: false
@@ -78,14 +79,8 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
}, [appId, loadAppDetail]);
return (
<Flex flexDirection={'column'} bg={'myGray.100'} h={'100%'} p={[0, 5]}>
<Box
display={['block', 'flex']}
flex={1}
bg={'white'}
borderRadius={['', '2xl']}
border={['', theme.borders.lg]}
>
<PageContainer>
<Box display={['block', 'flex']} h={'100%'}>
{/* pc tab */}
<Box display={['none', 'block']} p={4} w={'200px'} borderRight={theme.borders.base}>
<Flex mb={4}>
@@ -141,7 +136,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
{currentTab === TabEnum.share && <Share modelId={appId} />}
</Box>
</Box>
</Flex>
</PageContainer>
);
};

View File

@@ -0,0 +1,217 @@
import React, { useCallback, useState } from 'react';
import {
Box,
Flex,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
Input,
Grid,
useTheme,
Card
} from '@chakra-ui/react';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { useToast } from '@/hooks/useToast';
import { postCreateApp } from '@/api/app';
import { useRouter } from 'next/router';
import { chatAppDemo } from '@/constants/app';
import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
type FormType = {
avatar: string;
name: string;
templateId: number;
};
const templates = [
{
id: 0,
icon: 'settings',
name: '简单的对话',
intro: '一个极其简单的 AI 对话应用',
modules: chatAppDemo.modules
},
{
id: 1,
icon: 'settings',
name: '基础知识库',
intro: '每次提问时进行一次知识库搜索,将搜索结果注入 LLM 模型进行参考回答',
modules: chatAppDemo.modules
},
{
id: 2,
icon: 'settings',
name: '问答前引导',
intro: '可以在每次对话开始前提示用户填写一些内容,作为本次对话的永久内容',
modules: chatAppDemo.modules
},
{
id: 3,
icon: 'settings',
name: '意图识别 + 知识库',
intro: '先对用户的问题进行分类,再根据不同类型问题,执行不同的操作',
modules: chatAppDemo.modules
}
];
const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => {
const [refresh, setRefresh] = useState(false);
const [creating, setCreating] = useState(false);
const { toast } = useToast();
const router = useRouter();
const theme = useTheme();
const { register, setValue, getValues, handleSubmit } = useForm<FormType>({
defaultValues: {
avatar: '/icon/logo.png',
name: '',
templateId: 0
}
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
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]
);
const onclickCreate = useCallback(
async (data: FormType) => {
setCreating(true);
try {
const id = await postCreateApp({
avatar: data.avatar,
name: data.name,
modules: templates.find((item) => item.id === data.templateId)?.modules || []
});
toast({
title: '创建成功',
status: 'success'
});
router.push(`/app/detail?appId=${id}`);
onClose();
onSuccess();
} catch (error) {
toast({
title: getErrText(error, '创建应用异常'),
status: 'error'
});
}
setCreating(false);
},
[onClose, onSuccess, router, toast]
);
return (
<Modal isOpen onClose={onClose}>
<ModalOverlay />
<ModalContent w={'700px'} maxW={'90vw'}>
<ModalHeader fontSize={'2xl'}> AI </ModalHeader>
<ModalBody>
<Box color={'myGray.800'}></Box>
<Flex mt={3} alignItems={'center'}>
<Avatar
src={getValues('avatar')}
w={['32px', '36px']}
h={['32px', '36px']}
cursor={'pointer'}
title={'点击选择头像'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
<Input
ml={4}
bg={'myWhite.600'}
{...register('name', {
required: '应用名不能为空~'
})}
/>
</Flex>
<Box mt={7} mb={3} color={'myGray.800'}>
</Box>
<Grid
userSelect={'none'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)']}
gridGap={4}
>
{templates.map((item) => (
<Card
key={item.id}
border={theme.borders.base}
p={3}
borderRadius={'md'}
cursor={'pointer'}
boxShadow={'sm'}
{...(getValues('templateId') === item.id
? {
bg: 'myBlue.300'
}
: {
_hover: {
boxShadow: 'md'
}
})}
onClick={() => {
setValue('templateId', item.id);
setRefresh((state) => !state);
}}
>
<Flex alignItems={'center'}>
<MyIcon name={'apikey'} w={'16px'} />
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
<Box fontSize={'sm'} mt={4}>
{item.intro}
</Box>
</Card>
))}
</Grid>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={creating} onClick={handleSubmit(onclickCreate)}>
</Button>
</ModalFooter>
</ModalContent>
<File onSelect={onSelectFile} />
</Modal>
);
};
export default CreateModal;

View File

@@ -1,29 +1,76 @@
import React from 'react';
import { Box, Grid, Card, useTheme, Flex, IconButton, Button } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import {
Box,
Grid,
Card,
useTheme,
Flex,
IconButton,
Button,
useDisclosure
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import Avatar from '@/components/Avatar';
import styles from './index.module.scss';
import MyIcon from '@/components/Icon';
import { AddIcon } from '@chakra-ui/icons';
import { delModelById } from '@/api/app';
import { useToast } from '@/hooks/useToast';
import { useConfirm } from '@/hooks/useConfirm';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
const CreateModal = dynamic(() => import('./component/CreateModal'));
import styles from './index.module.scss';
const MyApps = () => {
const { toast } = useToast();
const theme = useTheme();
const router = useRouter();
const { myApps, loadMyModels } = useUserStore();
const { openConfirm, ConfirmChild } = useConfirm({
title: '删除提示',
content: '确认删除该应用所有信息?'
});
const {
isOpen: isOpenCreateModal,
onOpen: onOpenCreateModal,
onClose: onCloseCreateModal
} = useDisclosure();
/* 点击删除 */
const onclickDelApp = useCallback(
async (id: string) => {
try {
await delModelById(id);
toast({
title: '删除成功',
status: 'success'
});
loadMyModels();
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
},
[toast, loadMyModels]
);
/* 加载模型 */
useQuery(['loadModels'], () => loadMyModels(false));
useQuery(['loadModels'], loadMyModels, {
refetchOnMount: true
});
return (
<Box>
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
<PageContainer>
<Flex pt={3} px={5} alignItems={'center'}>
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
</Box>
<Button leftIcon={<AddIcon />} variant={'base'}>
<Button leftIcon={<AddIcon />} variant={'base'} onClick={onOpenCreateModal}>
</Button>
</Flex>
@@ -43,8 +90,7 @@ const MyApps = () => {
boxShadow={'none'}
userSelect={'none'}
_hover={{
boxShadow: 'xl',
transform: 'scale(1.03)',
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
borderColor: 'transparent',
'& .delete': {
display: 'block'
@@ -64,10 +110,14 @@ const MyApps = () => {
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
display={'none'}
display={['', 'none']}
_hover={{
bg: 'myGray.100'
}}
onClick={(e) => {
e.stopPropagation();
openConfirm(() => onclickDelApp(app._id))();
}}
/>
</Flex>
<Box className={styles.intro} py={2} fontSize={'sm'} color={'myGray.600'}>
@@ -76,7 +126,9 @@ const MyApps = () => {
</Card>
))}
</Grid>
</Box>
<ConfirmChild />
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} onSuccess={loadMyModels} />}
</PageContainer>
);
};

View File

@@ -94,7 +94,7 @@ const PcSliderBar = ({
[isPc]
);
useQuery(['loadModels'], () => loadMyModels(false));
useQuery(['loadModels'], loadMyModels);
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
loadHistory({ pageNum: 1 })

View File

@@ -39,7 +39,7 @@ const PhoneSliderBar = ({
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
useQuery(['loadModels'], () => loadMyModels(false));
useQuery(['loadModels'], loadMyModels);
const { history, loadHistory } = useChatStore();
useQuery(['loadingHistory'], () => loadHistory({ pageNum: 1 }));

View File

@@ -106,7 +106,7 @@ const Chat = () => {
const { copyData } = useCopyData();
const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
const { userInfo, loadMyModels } = useUserStore();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
// close contextMenu
@@ -232,7 +232,6 @@ const Chat = () => {
setTimeout(() => {
generatingMessage();
loadHistory({ pageNum: 1, init: true });
loadMyModels(true);
}, 100);
if (errMsg) {
@@ -252,7 +251,6 @@ const Chat = () => {
chatData.systemPrompt,
chatData.limitPrompt,
loadHistory,
loadMyModels,
toast
]
);

View File

@@ -205,7 +205,7 @@ const Home = () => {
fontSize={['xl', '3xl']}
h={'auto'}
py={[2, 3]}
onClick={() => router.push(`/model`)}
onClick={() => router.push(`/app/list`)}
>
</Button>

View File

@@ -7,7 +7,8 @@ import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useToast } from '@/hooks/useToast';
import { useRouter } from 'next/router';
import { postCreateModel } from '@/api/app';
import { postCreateApp } from '@/api/app';
import { chatAppDemo } from '@/constants/app';
interface Props {
loginSuccess: (e: ResLogin) => void;
@@ -64,8 +65,9 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
status: 'success'
});
// aut register a model
postCreateModel({
name: '应用1'
postCreateApp({
name: '应用1',
modules: chatAppDemo.modules
});
} catch (error: any) {
toast({

View File

@@ -17,7 +17,7 @@ const Login = () => {
const { lastRoute = '' } = router.query as { lastRoute: string };
const { isPc } = useGlobalStore();
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo, setLastModelId, loadMyModels, loadKbList, setLastKbId } = useUserStore();
const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore();
const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore();
const loginSuccess = useCallback(
@@ -27,7 +27,6 @@ const Login = () => {
setLastModelId('');
setLastChatModelId('');
setLastKbId('');
loadMyModels(true);
loadKbList(true);
loadHistory({ pageNum: 1, init: true });
@@ -40,7 +39,6 @@ const Login = () => {
lastRoute,
loadHistory,
loadKbList,
loadMyModels,
router,
setLastChatId,
setLastChatModelId,

View File

@@ -22,14 +22,9 @@ type State = {
setLastModelId: (id: string) => void;
myApps: AppListItemType[];
myCollectionApps: AppListItemType[];
loadMyModels: (init?: boolean) => Promise<null>;
loadMyModels: () => Promise<null>;
appDetail: AppSchema;
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
refreshModel: {
freshMyModels(): void;
updateModelDetail(model: AppSchema): void;
removeModelDetail(modelId: string): void;
};
// kb
lastKbId: string;
setLastKbId: (id: string) => void;
@@ -76,8 +71,7 @@ export const useUserStore = create<State>()(
},
myApps: [],
myCollectionApps: [],
async loadMyModels(init = false) {
if (get().myApps.length > 0 && !init) return null;
async loadMyModels() {
const res = await getMyModels();
set((state) => {
state.myApps = res.myApps;
@@ -95,26 +89,6 @@ export const useUserStore = create<State>()(
});
return res;
},
refreshModel: {
freshMyModels() {
get().loadMyModels(true);
},
updateModelDetail(model: AppSchema) {
set((state) => {
state.appDetail = model;
});
get().loadMyModels(true);
},
removeModelDetail(modelId: string) {
if (modelId === get().appDetail._id) {
set((state) => {
state.appDetail = defaultApp;
state.lastModelId = '';
});
}
get().loadMyModels(true);
}
},
lastKbId: '',
setLastKbId(id: string) {
set((state) => {