feat: model share market

This commit is contained in:
archer
2023-04-27 23:41:42 +08:00
parent 46eb96c72e
commit 3b8e5d2738
34 changed files with 574 additions and 199 deletions

View File

@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 获取 model 数据
const { model } = await authModel(modelId, userId);
const { model } = await authModel({ modelId, userId, authUser: false, authOwner: false });
// 历史记录
let history: ChatItemType[] = [];
@@ -53,6 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId: modelId,
name: model.name,
avatar: model.avatar,
intro: model.share.intro,
modelName: model.service.modelName,
chatModel: model.service.chatModel,
history

View File

@@ -27,9 +27,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
value: item.value
}));
await authModel({ modelId, userId, authOwner: false });
// 没有 chatId, 创建一个对话
if (!chatId) {
await authModel(modelId, userId);
const { _id } = await Chat.create({
userId,
modelId,

View File

@@ -4,6 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import type { PgModelDataItemType } from '@/types/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -36,9 +37,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { model } = await authModel({
userId,
modelId,
authOwner: false
});
const where: any = [
['user_id', userId],
'AND',
...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']),
['model_id', modelId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
];

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateVector } from '@/service/events/generateVector';
import { ModelDataStatusEnum } from '@/constants/model';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,15 +29,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 去重
const searchRes = await Promise.allSettled(
data.map(async ([q, a]) => {

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,15 +29,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 插入记录
await PgClient.insert('modelData', {
values: data.map((item) => [

View File

@@ -3,6 +3,7 @@ import { jsonRes } from '@/service/response';
import { Chat, Model, connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -21,18 +22,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
await authModel({
modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
await connectToDatabase();
// 删除 pg 中所有该模型的数据
await PgClient.delete('modelData', {
where: [['user_id', userId], 'AND', ['model_id', modelId]]

View File

@@ -2,8 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -14,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('无权操作');
}
const { modelId } = req.query;
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
@@ -25,16 +24,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 根据 userId 获取模型信息
const model = await Model.findOne<ModelSchema>({
const { model } = await authModel({
modelId,
userId,
_id: modelId
authOwner: false
});
if (!model) {
throw new Error('模型不存在');
}
jsonRes(res, {
data: model
});

View File

@@ -4,7 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
/* 获取我的模型 */
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { authorization } = req.headers;

View File

@@ -0,0 +1,54 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { PagingData } from '@/types';
import type { ShareModelItem } from '@/types/model';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
await authToken(req.headers.authorization);
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
await connectToDatabase();
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{ $or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }] }
]
};
// 根据分享的模型
const models = await Model.find(where, '_id avatar name userId share')
.sort({
'share.collection': -1
})
.limit(pageSize)
.skip((pageNum - 1) * pageSize);
jsonRes<PagingData<ShareModelItem>>(res, {
data: {
pageNum,
pageSize,
data: models,
total: await Model.countDocuments(where)
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,11 +4,12 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelUpdateParams } from '@/types/model';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, search, service, security, systemPrompt, temperature } =
const { name, search, share, service, security, systemPrompt, temperature } =
req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
@@ -26,6 +27,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
await authModel({
modelId,
userId
});
// 更新模型
await Model.updateOne(
{
@@ -36,6 +42,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
name,
systemPrompt,
temperature,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro,
search,
security
}

View File

@@ -3,12 +3,7 @@ import { Card, Box } from '@chakra-ui/react';
import { useMarkdown } from '@/hooks/useMarkdown';
import Markdown from '@/components/Markdown';
const Empty = ({ intro }: { intro: string }) => {
const Header = ({ children }: { children: string }) => (
<Box fontSize={'lg'} fontWeight={'bold'} textAlign={'center'} pb={2}>
{children}
</Box>
);
const Empty = ({ modelName, intro }: { modelName: string; intro: string }) => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
@@ -24,7 +19,9 @@ const Empty = ({ intro }: { intro: string }) => {
>
{!!intro && (
<Card p={4} mb={10}>
<Header></Header>
<Box fontSize={'xl'} fontWeight={'600'} textAlign={'center'} pb={2}>
{modelName}
</Box>
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}

View File

@@ -63,7 +63,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
chatId,
modelId,
name: '',
avatar: '',
avatar: '/icon/logo.png',
intro: '',
chatModel: '',
modelName: '',
history: []
@@ -155,7 +156,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
isClosable: true,
duration: 5000
});
router.replace('/model/list');
router.back();
}
setLoading(false);
return null;
@@ -469,7 +470,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<Menu>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
src={item.obj === 'Human' ? '/icon/human.png' : chatData.avatar}
alt="/icon/logo.png"
width={media(30, 20)}
height={media(30, 20)}
@@ -516,7 +517,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</Flex>
</Box>
))}
{chatData.history.length === 0 && <Empty intro={''} />}
{chatData.history.length === 0 && (
<Empty modelName={chatData.name} intro={chatData.intro} />
)}
</Box>
{/* 发送区 */}
<Box m={media('20px auto', '0 auto')} w={'100%'} maxW={media('min(750px, 100%)', 'auto')}>

View File

@@ -39,7 +39,7 @@ import InputModal, { FormData as InputDataType } from './InputDataModal';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const ModelDataCard = ({ modelId }: { modelId: string }) => {
const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => {
const { Loading, setIsLoading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
@@ -133,50 +133,53 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
: {total}
<Box as={'span'} fontSize={'sm'}>
</Box>
</Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
{isOwner && (
<>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</>
)}
</Flex>
<Flex mt={4}>
{splitDataLen > 0 && <Box fontSize={'xs'}>{splitDataLen}...</Box>}
{isOwner && splitDataLen > 0 && (
<Box fontSize={'xs'}>{splitDataLen}...</Box>
)}
<Box flex={1} />
<Input
maxW={'240px'}
@@ -207,7 +210,7 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Th>{'匹配的知识点'}</Th>
<Th></Th>
<Th></Th>
<Th></Th>
{isOwner && <Th></Th>}
</Tr>
</Thead>
<Tbody>
@@ -220,33 +223,35 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td>{ModelDataStatusMap[item.status]}</Td>
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
{isOwner && (
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
)}
</Tr>
))}
</Tbody>

View File

@@ -13,7 +13,9 @@ import {
SliderMark,
Tooltip,
Button,
Select
Select,
Grid,
Switch
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
@@ -25,10 +27,12 @@ import { useConfirm } from '@/hooks/useConfirm';
const ModelEditForm = ({
formHooks,
canTrain,
isOwner,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
canTrain: boolean;
isOwner: boolean;
handleDelModel: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
@@ -40,27 +44,26 @@ const ModelEditForm = ({
return (
<>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'}></Box>
</Flex>
<Box fontWeight={'bold'}></Box>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
@@ -79,17 +82,25 @@ const ModelEditForm = ({
/1K tokens()
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Box>{getValues('share.collection')}</Box>
</Flex>
{isOwner && (
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
)}
</Card>
<Card p={4}>
<Box fontWeight={'bold'}></Box>
@@ -110,6 +121,7 @@ const ModelEditForm = ({
max={10}
step={1}
value={getValues('temperature')}
isDisabled={!isOwner}
onChange={(e) => {
setValue('temperature', e);
setRefresh(!refresh);
@@ -139,7 +151,10 @@ const ModelEditForm = ({
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Select {...register('search.mode', { required: '搜索模式不能为空' })}>
<Select
isDisabled={!isOwner}
{...register('search.mode', { required: '搜索模式不能为空' })}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
@@ -154,15 +169,73 @@ const ModelEditForm = ({
<Textarea
rows={6}
maxLength={-1}
{...register('systemPrompt')}
isDisabled={!isOwner}
placeholder={
canTrain
? '训练的模型会根据知识库内容,生成一部分系统提示词,因此在对话时需要消耗更多的 tokens。你可以增加提示词让效果更符合预期。例如: \n1. 请根据知识库内容回答用户问题。\n2. 知识库是电影《铃芽之旅》的内容,根据知识库内容回答。无关问题,拒绝回复!'
: '模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n注意改功能会影响对话的整体朝向'
}
{...register('systemPrompt')}
/>
</Box>
</Card>
{isOwner && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<Box fontWeight={'bold'}></Box>
<Grid gridTemplateColumns={['1fr', '1fr 410px']} gridGap={5}>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={3}>:</Box>
<Switch
isChecked={getValues('share.isShare')}
onChange={() => {
setValue('share.isShare', !getValues('share.isShare'));
setRefresh(!refresh);
}}
/>
<Box ml={12} mr={3}>
:
</Box>
<Switch
isChecked={getValues('share.isShareDetail')}
onChange={() => {
setValue('share.isShareDetail', !getValues('share.isShareDetail'));
setRefresh(!refresh);
}}
/>
</Flex>
<Box mt={5}>
<Box></Box>
<Textarea
mt={1}
rows={6}
maxLength={150}
{...register('share.intro')}
placeholder={'介绍模型的功能、场景等吸引更多人来使用最多150字。'}
/>
</Box>
</Box>
<Box
textAlign={'justify'}
fontSize={'sm'}
border={'1px solid #f4f4f4'}
borderRadius={'sm'}
p={3}
>
<Box fontWeight={'bold'}>Tips</Box>
<Box mt={1} as={'ul'} pl={4}>
<li>
FastGpt
使使 tokens使 tokens
</li>
<li></li>
</Box>
</Box>
</Grid>
</Card>
)}
{/* <Card p={4}>
<Box fontWeight={'bold'}>安全策略</Box>
<FormControl mt={2}>

View File

@@ -5,11 +5,12 @@ import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
import { formatModelStatus, modelList, defaultModel } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'));
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
@@ -18,6 +19,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { userInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const [model, setModel] = useState<ModelSchema>(defaultModel);
@@ -30,21 +32,24 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
return !!(openai && openai.trainName);
}, [model]);
const isOwner = useMemo(() => model.userId === userInfo?._id, [model.userId, userInfo?._id]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
setLoading(true);
try {
const res = await getModelById(modelId);
// console.log(res);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
formHooks.reset(res);
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '获取模型异常',
status: 'error'
});
}
setLoading(false);
return null;
}, [formHooks, modelId, setLoading]);
}, [formHooks, modelId, setLoading, toast]);
useQuery([modelId], loadModel);
@@ -59,22 +64,19 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
status: 'success'
});
router.replace('/model/list');
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
setLoading(true);
try {
router.push(`/chat?modelId=${modelId}`);
} catch (err) {
console.log('error->', err);
}
setLoading(false);
}, [setLoading, router, modelId]);
router.push(`/chat?modelId=${modelId}`);
}, [router, modelId]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
@@ -86,6 +88,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
systemPrompt: data.systemPrompt,
temperature: data.temperature,
search: data.search,
share: data.share,
service: data.service,
security: data.security
});
@@ -93,11 +96,10 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
title: '更新成功',
status: 'success'
});
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err as string,
status: 'success'
title: err?.message || '更新失败',
status: 'error'
});
}
setLoading(false);
@@ -151,9 +153,11 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
)}
</Flex>
) : (
<>
@@ -169,19 +173,26 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
)}
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
<ModelEditForm
formHooks={formHooks}
handleDelModel={handleDelModel}
canTrain={canTrain}
isOwner={isOwner}
/>
{canTrain && !!model._id && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={model._id} />
<ModelDataCard modelId={model._id} isOwner={isOwner} />
</Card>
)}
</Grid>

View File

@@ -0,0 +1,62 @@
import React, { Dispatch, useCallback } from 'react';
import { Card, Box, Flex, Image, Button } from '@chakra-ui/react';
import type { ShareModelItem } from '@/types/model';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import styles from '../index.module.scss';
const ShareModelList = ({ models }: { models: ShareModelItem[] }) => {
const router = useRouter();
return (
<>
{models.map((model) => (
<Card key={model._id} p={4}>
<Flex alignItems={'center'}>
<Image
src={model.avatar}
alt={'avatar'}
width={'36px'}
height={'36px'}
objectFit={'contain'}
/>
<Box fontWeight={'bold'} fontSize={'lg'} ml={5}>
{model.name}
</Box>
</Flex>
<Box className={styles.intro} my={4} fontSize={'sm'}>
{model.share.intro || '这个模型没有介绍~'}
</Box>
<Flex justifyContent={'space-between'}>
<Flex alignItems={'center'} cursor={'pointer'}>
<MyIcon mr={1} name={'collectionLight'} w={'16px'} />
{model.share.collection}
</Flex>
<Box>
<Button
size={'sm'}
variant={'outline'}
w={'80px'}
onClick={() => router.push(`/chat?modelId=${model._id}`)}
>
</Button>
{model.share.isShareDetail && (
<Button
ml={4}
size={'sm'}
w={'80px'}
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
>
</Button>
)}
</Box>
</Flex>
</Card>
))}
</>
);
};
export default ShareModelList;

View File

@@ -0,0 +1,7 @@
.intro {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,73 @@
import React, { useState, useRef } from 'react';
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
import { useLoading } from '@/hooks/useLoading';
import { getShareModelList } from '@/api/model';
import { usePagination } from '@/hooks/usePagination';
import type { ShareModelItem } from '@/types/model';
import ShareModelList from './components/list';
const modelList = () => {
const { Loading, setIsLoading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
/* 加载模型 */
const {
data: models,
isLoading,
Pagination,
getData
} = usePagination<ShareModelItem>({
api: getShareModelList,
pageSize: 20,
params: {
searchText
}
});
return (
<Box position={'relative'}>
{/* 头部 */}
<Card px={6} py={3}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} fontSize={'xl'}>
</Box>
<Box flex={1}>(Beta)</Box>
<Input
maxW={'240px'}
size={'sm'}
value={searchText}
placeholder="搜索模型,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Flex>
</Card>
<Grid templateColumns={['1fr', '1fr 1fr']} gridGap={4} mt={4}>
<ShareModelList models={models} />
</Grid>
<Box mt={4}>
<Pagination />
</Box>
<Loading loading={isLoading} />
</Box>
);
};
export default modelList;