Commit 61447c60 by archer

feat: new app page

parent df2fda61
...@@ -42,6 +42,12 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接 ...@@ -42,6 +42,12 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4) - [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/) - [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
## Powered by
- [TuShan 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
- [Laf 3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos 快速部署集群应用](https://github.com/labring/sealos)
## 🌟 Star History ## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=c121914yu/FastGPT&type=Date)](https://star-history.com/#c121914yu/FastGPT&Date) [![Star History Chart](https://api.star-history.com/svg?repos=c121914yu/FastGPT&type=Date)](https://star-history.com/#c121914yu/FastGPT&Date)
...@@ -38,13 +38,13 @@ export const useAppRoute = (app) => { ...@@ -38,13 +38,13 @@ export const useAppRoute = (app) => {
id: model._id.toString(), id: model._id.toString(),
userId: model.userId, userId: model.userId,
name: model.name, name: model.name,
intro: model.intro,
model: model.chat?.chatModel, model: model.chat?.chatModel,
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称 relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
systemPrompt: model.chat?.systemPrompt || '', systemPrompt: model.chat?.systemPrompt || '',
temperature: model.chat?.temperature || 0, temperature: model.chat?.temperature || 0,
'share.topNum': model.share?.topNum || 0, 'share.topNum': model.share?.topNum || 0,
'share.isShare': model.share?.isShare || false, 'share.isShare': model.share?.isShare || false,
'share.intro': model.share?.intro,
'share.collection': model.share?.collection || 0 'share.collection': model.share?.collection || 0
}; };
...@@ -66,14 +66,15 @@ export const useAppRoute = (app) => { ...@@ -66,14 +66,15 @@ export const useAppRoute = (app) => {
const _id = req.params.id; const _id = req.params.id;
let { let {
share: { isShare, intro, topNum } share: { isShare, topNum },
intro
} = req.body; } = req.body;
await Model.findByIdAndUpdate(_id, { await Model.findByIdAndUpdate(_id, {
$set: { $set: {
intro: intro,
'share.topNum': Number(topNum), 'share.topNum': Number(topNum),
'share.isShare': isShare === 'true', 'share.isShare': isShare === 'true' || isShare === true
'share.intro': intro
} }
}); });
......
...@@ -61,9 +61,9 @@ const modelSchema = new mongoose.Schema({ ...@@ -61,9 +61,9 @@ const modelSchema = new mongoose.Schema({
name: String, name: String,
avatar: String, avatar: String,
status: String, status: String,
intro: String,
chat: { chat: {
relatedKbs: [mongoose.Schema.Types.ObjectId], relatedKbs: [mongoose.Schema.Types.ObjectId],
searchMode: String,
systemPrompt: String, systemPrompt: String,
temperature: Number, temperature: Number,
chatModel: String chatModel: String
......
...@@ -2,7 +2,7 @@ import { createTextField, createNumberField } from 'tushan'; ...@@ -2,7 +2,7 @@ import { createTextField, createNumberField } from 'tushan';
export const userFields = [ export const userFields = [
createTextField('id', { label: 'ID' }), createTextField('id', { label: 'ID' }),
createTextField('username', { label: '用户名' }), createTextField('username', { label: '用户名', edit: { hidden: true } }),
createNumberField('balance', { label: '余额', list: { sort: true } }), createNumberField('balance', { label: '余额', list: { sort: true } }),
createTextField('createTime', { label: 'Create Time', list: { sort: true } }), createTextField('createTime', { label: 'Create Time', list: { sort: true } }),
createTextField('password', { label: '密码', list: { hidden: true } }) createTextField('password', { label: '密码', list: { hidden: true } })
...@@ -19,20 +19,20 @@ export const payFields = [ ...@@ -19,20 +19,20 @@ export const payFields = [
export const kbFields = [ export const kbFields = [
createTextField('id', { label: 'ID' }), createTextField('id', { label: 'ID' }),
createTextField('userId', { label: '所属用户' }), createTextField('userId', { label: '所属用户', edit: { hidden: true } }),
createTextField('name', { label: '知识库' }), createTextField('name', { label: '知识库' }),
createTextField('tags', { label: 'Tags' }) createTextField('tags', { label: 'Tags' })
]; ];
export const ModelFields = [ export const ModelFields = [
createTextField('id', { label: 'ID' }), createTextField('id', { label: 'ID' }),
createTextField('userId', { label: '所属用户', list: { hidden: true } }), createTextField('userId', { label: '所属用户', list: { hidden: true }, edit: { hidden: true } }),
createTextField('name', { label: '名字' }), createTextField('name', { label: '名字' }),
createTextField('model', { label: '模型' }), createTextField('model', { label: '模型', edit: { hidden: true } }),
createTextField('share.collection', { label: '收藏数', list: { sort: true } }), createTextField('share.collection', { label: '收藏数', list: { sort: true } }),
createTextField('share.topNum', { label: '置顶等级', list: { sort: true } }), createTextField('share.topNum', { label: '置顶等级', list: { sort: true } }),
createTextField('share.isShare', { label: '是否分享(true,false)' }), createTextField('share.isShare', { label: '是否分享(true,false)' }),
createTextField('share.intro', { label: '介绍', list: { width: 400 } }), createTextField('intro', { label: '介绍', list: { width: 400 } }),
createTextField('relatedKbs', { label: '引用的知识库', list: { hidden: true } }), createTextField('relatedKbs', { label: '引用的知识库', list: { hidden: true } }),
createTextField('temperature', { label: '温度' }), createTextField('temperature', { label: '温度' }),
createTextField('systemPrompt', { createTextField('systemPrompt', {
......
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Card,
Box, Box,
Button, Button,
Modal,
ModalOverlay,
ModalContent,
Flex,
ModalFooter,
ModalBody,
ModalCloseButton,
Table, Table,
Thead, Thead,
Tbody, Tbody,
...@@ -10,23 +16,20 @@ import { ...@@ -10,23 +16,20 @@ import {
Th, Th,
Td, Td,
TableContainer, TableContainer,
IconButton, IconButton
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi'; import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
import { useQuery, useMutation } from '@tanstack/react-query'; import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading'; import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { DeleteIcon } from '@chakra-ui/icons'; import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/utils/tools'; import { getErrText, useCopyData } from '@/utils/tools';
import { useToast } from '@/hooks/useToast';
import MyIcon from '../Icon';
const OpenApi = () => { const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
const { Loading } = useLoading(); const { Loading } = useLoading();
const { toast } = useToast();
const { const {
data: apiKeys = [], data: apiKeys = [],
isLoading: isGetting, isLoading: isGetting,
...@@ -40,6 +43,12 @@ const OpenApi = () => { ...@@ -40,6 +43,12 @@ const OpenApi = () => {
onSuccess(res) { onSuccess(res) {
setApiKey(res); setApiKey(res);
refetch(); refetch();
},
onError(err) {
toast({
status: 'warning',
title: getErrText(err)
});
} }
}); });
...@@ -51,89 +60,101 @@ const OpenApi = () => { ...@@ -51,89 +60,101 @@ const OpenApi = () => {
}); });
return ( return (
<Box py={[5, 10]} px={'5vw'}> <Modal isOpen onClose={onClose}>
<Card px={6} py={4} position={'relative'}> <ModalOverlay />
<Box fontSize={'xl'} fontWeight={'bold'}> <ModalContent w={'600px'} maxW={'90vw'} position={'relative'}>
FastGpt Api <Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API 秘钥管理
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
如果你不想 API 秘钥被滥用,请勿将秘钥直接放置在前端使用~
</Box>
</Box> </Box>
<Box fontSize={'sm'} mt={2}> <ModalCloseButton />
FastGpt Api 允许你将 Fast Gpt 的部分功能通过 api <ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
的形式接入到自己的应用中,例如:飞书、企业微信、客服助手。请注意保管你的 Api <TableContainer mt={2} position={'relative'}>
Key,不要泄露! <Table>
</Box> <Thead>
<Box>使用 Fast Api 功能仅能使用平台余额。</Box> <Tr>
<Box <Th>Api Key</Th>
my={1} <Th>创建时间</Th>
as="a" <Th>最后一次使用时间</Th>
href="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh" <Th />
color={'myBlue.800'}
textDecoration={'underline'}
target={'_blank'}
>
点击查看文档
</Box>
<TableContainer mt={2} position={'relative'}>
<Table>
<Thead>
<Tr>
<Th>Api Key</Th>
<Th>创建时间</Th>
<Th>最后一次使用时间</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
<Tr key={id}>
<Td>{apiKey}</Td>
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>
{lastUsedTime
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
: '没有使用过'}
</Td>
<Td>
<IconButton
icon={<DeleteIcon />}
size={'xs'}
aria-label={'delete'}
variant={'base'}
colorScheme={'gray'}
onClick={() => onclickRemove(id)}
/>
</Td>
</Tr> </Tr>
))} </Thead>
</Tbody> <Tbody fontSize={'sm'}>
</Table> {apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
</TableContainer> <Tr key={id}>
<Button <Td>{apiKey}</Td>
maxW={'200px'} <Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
mt={5} <Td>
isLoading={isCreating} {lastUsedTime
isDisabled={apiKeys.length >= 5} ? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
title={apiKeys.length >= 5 ? '最多五组 Api Key' : ''} : '没有使用过'}
onClick={() => onclickCreateApiKey()} </Td>
> <Td>
添加新的 Api Key <IconButton
</Button> icon={<DeleteIcon />}
<Loading loading={isGetting || isDeleting} fixed={false} /> size={'xs'}
</Card> aria-label={'delete'}
variant={'base'}
colorScheme={'gray'}
onClick={() => onclickRemove(id)}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter>
<Button
variant="base"
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
onClick={() => onclickCreateApiKey()}
>
新建秘钥
</Button>
</ModalFooter>
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
</ModalContent>
<Modal isOpen={!!apiKey} onClose={() => setApiKey('')}> <Modal isOpen={!!apiKey} onClose={() => setApiKey('')}>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent w={'400px'} maxW={'90vw'}>
<ModalHeader>Api Key</ModalHeader> <Box py={3} px={5}>
<ModalCloseButton /> <Box fontWeight={'bold'} fontSize={'2xl'}>
<ModalBody mb={5}> 新的 API 秘钥
请保管好你的Api Key </Box>
<Box userSelect={'all'} onClick={() => copyData(apiKey)}> <Box fontSize={'sm'} color={'myGray.600'}>
{apiKey} 请保管好你的秘钥,秘钥不会再次展示~
</Box> </Box>
</Box>
<ModalCloseButton />
<ModalBody>
<Flex
bg={'myGray.100'}
px={3}
py={2}
cursor={'pointer'}
onClick={() => copyData(apiKey)}
>
<Box flex={1}>{apiKey}</Box>
<MyIcon name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody> </ModalBody>
<ModalFooter>
<Button variant="base" onClick={() => setApiKey('')}>
好的
</Button>
</ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
</Box> </Modal>
); );
}; };
export default OpenApi; export default APIKeyModal;
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686969412308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3481" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M517.864056 487.834624c-56.774051-54.213739-58.850339-144.187937-4.6366-200.960964 54.212716-56.773028 144.187937-58.849316 200.960964-4.6366 56.775074 54.213739 58.850339 144.186913 4.6366 200.960964C664.613328 539.972075 574.639131 542.048363 517.864056 487.834624zM687.194626 452.994118c37.533848-39.308261 36.09508-101.596909-3.210112-139.128711-39.304168-37.531801-101.593839-36.094056-139.127687 3.211135-37.532825 39.307238-36.093033 101.593839 3.212158 139.125641C587.374176 493.736031 649.660778 492.302379 687.194626 452.994118zM479.104287 670.917406l-101.495602 106.289792c26.206872 25.024953 27.167756 66.540486 2.14178 92.749404-25.028023 26.209942-66.543555 27.16571-92.750427 2.140757l-58.361199 53.027727c0 0-68.750827 11.100826-100.379175-19.101033-31.630395-30.205952-37.865399-112.721271-37.865399-112.721271l246.37427-258.302951c-63.173808-117.608581-47.24707-267.162736 49.939389-368.939747 36.517705-38.242999 80.346933-65.156976 127.165238-81.040734l1.084705 46.269813c-35.443233 14.07967-68.566632 35.596729-96.618525 64.973804-80.271208 84.064604-96.099708 205.865671-49.433876 305.083393l23.075555 39.163975L146.090774 798.015106c0 0 0.593518 49.77873 17.242709 65.677838 14.888082 14.216793 61.832254 9.828856 61.832254 9.828856l60.407812-63.260789 31.631418 30.203906c8.741082 8.346085 22.570042 8.030907 30.91715-0.711198 8.347109-8.742105 8.026814-22.571065-0.713244-30.91715l-31.632441-30.207999 156.456355-163.846672 39.009456 22.481014c101.259218 42.039465 222.201731 20.61041 302.474986-63.453171 104.251366-109.178585 100.260471-282.211477-8.91709-386.464889-33.591049-32.075533-73.260537-53.829999-115.093295-65.49262l-1.030469-45.153386c53.197596 12.471033 103.945397 38.547944 146.323577 79.015611 126.645398 120.931257 131.277906 321.649698 10.344602 448.296119C748.158093 705.787588 599.500355 728.598106 479.104287 670.917406z" p-id="3482"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683254594671" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1491" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M46.95735957 106.20989621h930.08528086v158.0067668H46.95735957zM46.95735957 353.99323467v608.68515424h930.08528086V353.99323467H46.95735957z m346.5375657 418.35882335L328.85579413 835.19565715l-165.18889183-172.37101684 165.18889183-172.37101686 64.63913114 62.84359914-105.93635373 109.52741772 105.93635373 109.52741771z m127.48273175 62.84359913l-86.18550917-23.34190854 87.98104116-330.37778366 86.1855077 23.34191003L520.97765702 835.19565715z m193.91739489 0l-64.63913114-62.84359913 105.93635372-109.52741771-105.93635372-109.52741772 64.63913114-62.84359914 165.18889182 172.37101686-165.18889182 172.37101684z" p-id="1492"></path></svg>
\ No newline at end of file
...@@ -6,7 +6,6 @@ const map = { ...@@ -6,7 +6,6 @@ const map = {
model: require('./icons/model.svg').default, model: require('./icons/model.svg').default,
copy: require('./icons/copy.svg').default, copy: require('./icons/copy.svg').default,
chatSend: require('./icons/chatSend.svg').default, chatSend: require('./icons/chatSend.svg').default,
develop: require('./icons/develop.svg').default,
user: require('./icons/user.svg').default, user: require('./icons/user.svg').default,
delete: require('./icons/delete.svg').default, delete: require('./icons/delete.svg').default,
withdraw: require('./icons/withdraw.svg').default, withdraw: require('./icons/withdraw.svg').default,
...@@ -34,7 +33,8 @@ const map = { ...@@ -34,7 +33,8 @@ const map = {
text: require('./icons/text.svg').default, text: require('./icons/text.svg').default,
history: require('./icons/history.svg').default, history: require('./icons/history.svg').default,
kbTest: require('./icons/kbTest.svg').default, kbTest: require('./icons/kbTest.svg').default,
date: require('./icons/date.svg').default date: require('./icons/date.svg').default,
apikey: require('./icons/apikey.svg').default
}; };
export type IconName = keyof typeof map; export type IconName = keyof typeof map;
......
type TIconfont = {
name: string;
color?: string;
width?: number | string;
height?: number | string;
className?: string;
};
function Iconfont({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
const style = {
fill: color,
width,
height
};
return (
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Iconfont;
...@@ -45,12 +45,6 @@ const Navbar = ({ unread }: { unread: number }) => { ...@@ -45,12 +45,6 @@ const Navbar = ({ unread }: { unread: number }) => {
activeLink: ['/model/share'] activeLink: ['/model/share']
}, },
{ {
label: '开发',
icon: 'develop',
link: '/openapi',
activeLink: ['/openapi']
},
{
label: '账号', label: '账号',
icon: 'user', icon: 'user',
link: '/number', link: '/number',
......
import React from 'react';
import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure } from '@chakra-ui/react';
import type { ButtonProps } from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
interface Props extends ButtonProps {
value?: string;
placeholder?: string;
list: {
label: string;
id: string;
}[];
onchange?: (val: string) => void;
}
const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }: Props) => {
const menuItemStyles = {
borderRadius: 'sm',
py: 2,
display: 'flex',
alignItems: 'center',
_hover: {
backgroundColor: 'myWhite.600'
}
};
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Menu autoSelect={false} onOpen={onOpen} onClose={onClose}>
<MenuButton as={'span'}>
<Button
width={width}
px={3}
variant={'base'}
display={'flex'}
alignItems={'center'}
justifyContent={'space-between'}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
}
: {})}
{...props}
>
{list.find((item) => item.id === value)?.label || placeholder}
<ChevronDownIcon />
</Button>
</MenuButton>
<MenuList
minW={
Array.isArray(width) ? width.map((item) => `${item} !important`) : `${width} !important`
}
p={'6px'}
border={'1px solid #fff'}
boxShadow={'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'}
zIndex={99}
>
{list.map((item) => (
<MenuItem
key={item.id}
{...menuItemStyles}
{...(value === item.id
? {
color: 'myBlue.600'
}
: {})}
onClick={() => {
if (onchange && value !== item.id) {
onchange(item.id);
}
}}
>
{item.label}
</MenuItem>
))}
</MenuList>
</Menu>
);
};
export default MySelect;
...@@ -9,28 +9,30 @@ import { ...@@ -9,28 +9,30 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
const MySlider = ({ const MySlider = ({
markList, markList = [],
setVal, setVal,
activeVal, activeVal,
max = 100, max = 100,
min = 0, min = 0,
step = 1 step = 1,
width = '100%'
}: { }: {
markList: { markList?: {
label: string | number; label: string | number;
value: number; value: number;
}[]; }[];
activeVal?: number; activeVal: number;
setVal: (index: number) => void; setVal: (index: number) => void;
max?: number; max?: number;
min?: number; min?: number;
step?: number; step?: number;
width?: string | string[] | number | number[];
}) => { }) => {
const startEndPointStyle = { const startEndPointStyle = {
content: '""', content: '""',
borderRadius: '10px', borderRadius: '6px',
width: '10px', width: '6px',
height: '10px', height: '6px',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
border: '2px solid #D7DBE2', border: '2px solid #D7DBE2',
position: 'absolute', position: 'absolute',
...@@ -44,37 +46,62 @@ const MySlider = ({ ...@@ -44,37 +46,62 @@ const MySlider = ({
}, [activeVal, markList]); }, [activeVal, markList]);
return ( return (
<Slider max={max} min={min} step={step} size={'lg'} value={value} onChange={setVal}> <Slider
{markList.map((item, i) => ( max={max}
min={min}
step={step}
size={'lg'}
value={activeVal}
width={width}
onChange={setVal}
>
{markList?.map((item, i) => (
<SliderMark <SliderMark
key={item.value} key={item.value}
value={i} value={item.value}
mt={3}
fontSize={'sm'} fontSize={'sm'}
mt={3}
whiteSpace={'nowrap'}
transform={'translateX(-50%)'} transform={'translateX(-50%)'}
{...(activeVal === item.value ? { color: 'myBlue.500', fontWeight: 'bold' } : {})} color={'myGray.600'}
> >
<Box px={3} cursor={'pointer'}> <Box px={3} cursor={'pointer'}>
{item.label} {item.label}
</Box> </Box>
</SliderMark> </SliderMark>
))} ))}
<SliderMark
value={activeVal}
textAlign="center"
bg="myBlue.600"
color="white"
px={1}
minW={'18px'}
w={'auto'}
h={'18px'}
borderRadius={'18px'}
fontSize={'xs'}
transform={'translate(-50%, -170%)'}
boxSizing={'border-box'}
>
{activeVal}
</SliderMark>
<SliderTrack <SliderTrack
bg={'#EAEDF3'} bg={'#EAEDF3'}
overflow={'visible'} overflow={'visible'}
h={'4px'} h={'4px'}
_before={{ _before={{
...startEndPointStyle, ...startEndPointStyle,
left: '-5px' left: '-3px'
}} }}
_after={{ _after={{
...startEndPointStyle, ...startEndPointStyle,
right: '-5px' right: '-3px'
}} }}
> >
<SliderFilledTrack /> <SliderFilledTrack bg={'myBlue.600'} />
</SliderTrack> </SliderTrack>
<SliderThumb border={'2.5px solid'} borderColor={'myBlue.500'}></SliderThumb> <SliderThumb border={'3px solid'} borderColor={'myBlue.600'}></SliderThumb>
</Slider> </Slider>
); );
}; };
......
...@@ -24,13 +24,13 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { ...@@ -24,13 +24,13 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
return { return {
fontSize: 'md', fontSize: 'md',
outP: '4px', outP: '4px',
inlineP: 2 inlineP: 1
}; };
case 'lg': case 'lg':
return { return {
fontSize: 'lg', fontSize: 'lg',
outP: '5px', outP: '5px',
inlineP: 3 inlineP: 2
}; };
} }
}, [size]); }, [size]);
......
...@@ -31,7 +31,7 @@ export const ChatModelMap = { ...@@ -31,7 +31,7 @@ export const ChatModelMap = {
[OpenAiChatEnum.GPT35]: { [OpenAiChatEnum.GPT35]: {
chatModel: OpenAiChatEnum.GPT35, chatModel: OpenAiChatEnum.GPT35,
name: 'Gpt35-4k', name: 'Gpt35-4k',
contextMaxToken: 4096, contextMaxToken: 4000,
systemMaxToken: 2400, systemMaxToken: 2400,
maxTemperature: 1.2, maxTemperature: 1.2,
price: 2.2 price: 2.2
...@@ -80,70 +80,18 @@ export const getChatModelList = async () => { ...@@ -80,70 +80,18 @@ export const getChatModelList = async () => {
return list; return list;
}; };
export enum ModelStatusEnum {
running = 'running',
training = 'training',
pending = 'pending',
closed = 'closed'
}
export const formatModelStatus = {
[ModelStatusEnum.running]: {
colorTheme: 'green',
text: '运行中'
},
[ModelStatusEnum.training]: {
colorTheme: 'blue',
text: '训练中'
},
[ModelStatusEnum.pending]: {
colorTheme: 'gray',
text: '加载中'
},
[ModelStatusEnum.closed]: {
colorTheme: 'red',
text: '已关闭'
}
};
/* 知识库搜索时的配置 */
// 搜索方式
export enum appVectorSearchModeEnum {
hightSimilarity = 'hightSimilarity', // 高相似度+禁止回复
lowSimilarity = 'lowSimilarity', // 低相似度
noContext = 'noContex' // 高相似度+无上下文回复
}
export const ModelVectorSearchModeMap: Record<
`${appVectorSearchModeEnum}`,
{
text: string;
similarity: number;
}
> = {
[appVectorSearchModeEnum.hightSimilarity]: {
text: '高相似度, 无匹配时拒绝回复',
similarity: 0.8
},
[appVectorSearchModeEnum.noContext]: {
text: '高相似度,无匹配时直接回复',
similarity: 0.8
},
[appVectorSearchModeEnum.lowSimilarity]: {
text: '低相似度匹配',
similarity: 0.3
}
};
export const defaultModel: ModelSchema = { export const defaultModel: ModelSchema = {
_id: 'modelId', _id: 'modelId',
userId: 'userId', userId: 'userId',
name: '模型名称', name: '模型名称',
avatar: '/icon/logo.png', avatar: '/icon/logo.png',
status: ModelStatusEnum.pending, intro: '',
updateTime: Date.now(), updateTime: Date.now(),
chat: { chat: {
relatedKbs: [], relatedKbs: [],
searchMode: appVectorSearchModeEnum.hightSimilarity, searchSimilarity: 0.2,
searchLimit: 5,
searchEmptyText: '',
systemPrompt: '', systemPrompt: '',
temperature: 0, temperature: 0,
chatModel: OpenAiChatEnum.GPT35 chatModel: OpenAiChatEnum.GPT35
...@@ -151,7 +99,6 @@ export const defaultModel: ModelSchema = { ...@@ -151,7 +99,6 @@ export const defaultModel: ModelSchema = {
share: { share: {
isShare: false, isShare: false,
isShareDetail: false, isShareDetail: false,
intro: '',
collection: 0 collection: 0
} }
}; };
......
...@@ -15,9 +15,7 @@ const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } ...@@ -15,9 +15,7 @@ const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle }
// modal 弹窗 // modal 弹窗
const ModalTheme = defineMultiStyleConfig({ const ModalTheme = defineMultiStyleConfig({
baseStyle: definePartsStyle({ baseStyle: definePartsStyle({
dialog: { dialog: {}
width: '90%'
}
}) })
}); });
......
...@@ -8,9 +8,9 @@ import { theme } from '@/constants/theme'; ...@@ -8,9 +8,9 @@ import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router'; import Router from 'next/router';
import 'nprogress/nprogress.css';
import '../styles/reset.scss';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import 'nprogress/nprogress.css';
import '@/styles/reset.scss';
//Binding events. //Binding events.
Router.events.on('routeChangeStart', () => NProgress.start()); Router.events.on('routeChangeStart', () => NProgress.start());
...@@ -28,7 +28,7 @@ const queryClient = new QueryClient({ ...@@ -28,7 +28,7 @@ const queryClient = new QueryClient({
} }
}); });
export default function App({ Component, pageProps }: AppProps) { function App({ Component, pageProps }: AppProps) {
const { const {
loadInitData, loadInitData,
initData: { googleVerKey } initData: { googleVerKey }
...@@ -78,6 +78,5 @@ export default function App({ Component, pageProps }: AppProps) { ...@@ -78,6 +78,5 @@ export default function App({ Component, pageProps }: AppProps) {
); );
} }
// export function reportWebVitals(metric: NextWebVitalsMetric) { // @ts-ignore
// console.log(metric); export default App;
// }
...@@ -4,7 +4,7 @@ import { authChat } from '@/service/utils/auth'; ...@@ -4,7 +4,7 @@ import { authChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat'; import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model'; import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill'; import { pushChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat'; import { resStreamResponse } from '@/service/utils/chat';
import { appKbSearch } from '../openapi/kb/appKbSearch'; import { appKbSearch } from '../openapi/kb/appKbSearch';
...@@ -48,36 +48,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -48,36 +48,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 读取对话内容
const prompts = [...content, prompt[0]];
const { const {
code = 200, rawSearch = [],
systemPrompts = [], userSystemPrompt = [],
quote = [], quotePrompt = []
guidePrompt = ''
} = await (async () => { } = await (async () => {
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) { if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts, rawSearch, guidePrompt } = await appKbSearch({ const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
model, model,
userId, userId,
fixedQuote: content[content.length - 1]?.quote || [], fixedQuote: content[content.length - 1]?.quote || [],
prompt: prompt[0], prompt: prompt[0],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
}); });
return { return {
code, rawSearch: rawSearch,
quote: rawSearch, userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
systemPrompts: searchPrompts, quotePrompt: [quotePrompt]
guidePrompt
}; };
} }
if (model.chat.systemPrompt) { if (model.chat.systemPrompt) {
return { return {
guidePrompt: model.chat.systemPrompt, userSystemPrompt: [
systemPrompts: [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
...@@ -92,13 +87,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -92,13 +87,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const conversationId = chatId || String(new Types.ObjectId()); const conversationId = chatId || String(new Types.ObjectId());
!chatId && res.setHeader(NEW_CHATID_HEADER, conversationId); !chatId && res.setHeader(NEW_CHATID_HEADER, conversationId);
if (showModelDetail) { if (showModelDetail) {
guidePrompt && res.setHeader(GUIDE_PROMPT_HEADER, encodeURIComponent(guidePrompt)); userSystemPrompt[0] &&
res.setHeader(QUOTE_LEN_HEADER, quote.length); res.setHeader(GUIDE_PROMPT_HEADER, encodeURIComponent(userSystemPrompt[0].value));
res.setHeader(QUOTE_LEN_HEADER, rawSearch.length);
} }
// search result is empty // search result is empty
if (code === 201) { if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = systemPrompts[0]?.value; const response = model.chat.searchEmptyText;
await saveChat({ await saveChat({
chatId, chatId,
newChatId: conversationId, newChatId: conversationId,
...@@ -116,11 +112,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -116,11 +112,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.end(response); return res.end(response);
} }
prompts.unshift(...systemPrompts); // 读取对话内容
const prompts = [...quotePrompt, ...content, ...userSystemPrompt, prompt[0]];
// content check // content check
await sensitiveCheck({ await sensitiveCheck({
input: [...systemPrompts, prompt[0]].map((item) => item.value).join('') input: [...quotePrompt, ...userSystemPrompt, prompt[0]].map((item) => item.value).join('')
}); });
// 计算温度 // 计算温度
...@@ -162,8 +159,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -162,8 +159,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{ {
...prompt[1], ...prompt[1],
value: responseContent, value: responseContent,
quote: showModelDetail ? quote : [], quote: showModelDetail ? rawSearch : [],
systemPrompt: showModelDetail ? guidePrompt : '' systemPrompt: showModelDetail ? userSystemPrompt[0]?.value : ''
} }
], ],
userId userId
......
...@@ -6,7 +6,6 @@ import { authUser } from '@/service/utils/auth'; ...@@ -6,7 +6,6 @@ import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { authModel } from '@/service/utils/auth'; import { authModel } from '@/service/utils/auth';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema } from '@/types/mongoSchema'; import type { ModelSchema } from '@/types/mongoSchema';
/* 初始化我的聊天框,需要身份验证 */ /* 初始化我的聊天框,需要身份验证 */
...@@ -29,8 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -29,8 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!myModel) { if (!myModel) {
const { _id } = await Model.create({ const { _id } = await Model.create({
name: '应用1', name: '应用1',
userId, userId
status: ModelStatusEnum.running
}); });
model = (await Model.findById(_id)) as ModelSchema; model = (await Model.findById(_id)) as ModelSchema;
} else { } else {
...@@ -95,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -95,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: { model: {
name: model.name, name: model.name,
avatar: model.avatar, avatar: model.avatar,
intro: model.share.intro, intro: model.intro,
canUse: model.share.isShare || String(model.userId) === userId canUse: model.share.isShare || String(model.userId) === userId
}, },
chatModel: model.chat.chatModel, chatModel: model.chat.chatModel,
......
...@@ -4,7 +4,7 @@ import { authShareChat } from '@/service/utils/auth'; ...@@ -4,7 +4,7 @@ import { authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat'; import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat'; import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model'; import { ChatModelMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill'; import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat'; import { resStreamResponse } from '@/service/utils/chat';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
...@@ -40,26 +40,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -40,26 +40,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1];
const { code = 200, systemPrompts = [] } = await (async () => { const {
rawSearch = [],
userSystemPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) { if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts } = await appKbSearch({ const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
model, model,
userId, userId,
fixedQuote: [], fixedQuote: [],
prompt: prompts[prompts.length - 1], prompt: prompt,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
}); });
return { return {
code, rawSearch: rawSearch,
systemPrompts: searchPrompts userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
}; };
} }
if (model.chat.systemPrompt) { if (model.chat.systemPrompt) {
return { return {
systemPrompts: [ userSystemPrompt: [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
...@@ -71,15 +78,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -71,15 +78,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})(); })();
// search result is empty // search result is empty
if (code === 201) { if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
return res.send(systemPrompts[0]?.value); const response = model.chat.searchEmptyText;
return res.end(response);
} }
prompts.unshift(...systemPrompts); // 读取对话内容
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt];
// content check // content check
await sensitiveCheck({ await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('') input: [...quotePrompt, ...userSystemPrompt, prompt].map((item) => item.value).join('')
}); });
// 计算温度 // 计算温度
...@@ -93,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -93,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
].chatCompletion({ ].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey, apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature, temperature: +temperature,
messages: prompts, messages: completePrompts,
stream: true, stream: true,
res, res,
chatId: historyId chatId: historyId
......
...@@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: { model: {
name: model.name, name: model.name,
avatar: model.avatar, avatar: model.avatar,
intro: model.share.intro intro: model.intro
}, },
chatModel: model.chat.chatModel chatModel: model.chat.chatModel
} }
......
...@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; ...@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
import { ModelStatusEnum } from '@/constants/model';
import { Model } from '@/service/models/model'; import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
...@@ -32,8 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -32,8 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 创建模型 // 创建模型
const response = await Model.create({ const response = await Model.create({
name, name,
userId, userId
status: ModelStatusEnum.running
}); });
jsonRes(res, { jsonRes(res, {
......
...@@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
$and: [ $and: [
{ 'share.isShare': true }, { 'share.isShare': true },
{ {
$or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }] $or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
} }
] ]
}; };
...@@ -66,6 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -66,6 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
avatar: { $ifNull: ['$avatar', '/icon/logo.png'] }, avatar: { $ifNull: ['$avatar', '/icon/logo.png'] },
name: 1, name: 1,
userId: 1, userId: 1,
intro: 1,
share: 1, share: 1,
isCollection: { isCollection: {
$cond: { $cond: {
......
...@@ -9,10 +9,10 @@ import { authModel } from '@/service/utils/auth'; ...@@ -9,10 +9,10 @@ import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { name, avatar, chat, share } = req.body as ModelUpdateParams; const { name, avatar, chat, share, intro } = req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string }; const { modelId } = req.query as { modelId: string };
if (!name || !chat || !modelId) { if (!modelId) {
throw new Error('参数错误'); throw new Error('参数错误');
} }
...@@ -35,10 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -35,10 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
{ {
name, name,
avatar, avatar,
intro,
chat, chat,
'share.isShare': share.isShare, ...(share && {
'share.isShareDetail': share.isShareDetail, 'share.isShare': share.isShare,
'share.intro': share.intro 'share.isShareDetail': share.isShareDetail
})
} }
); );
......
...@@ -4,12 +4,11 @@ import { authUser, authModel, getApiKey } from '@/service/utils/auth'; ...@@ -4,12 +4,11 @@ import { authUser, authModel, getApiKey } from '@/service/utils/auth';
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat'; import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat'; import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model'; import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill'; import { pushChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools'; import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user'; import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '../../openapi/text/sensitiveCheck';
import { NEW_CHATID_HEADER } from '@/constants/chat'; import { NEW_CHATID_HEADER } from '@/constants/chat';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { appKbSearch } from '../kb/appKbSearch'; import { appKbSearch } from '../kb/appKbSearch';
...@@ -66,48 +65,46 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -66,48 +65,46 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}); });
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1];
const { userSystemPrompt = [], quotePrompt = [] } = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { userSystemPrompt, quotePrompt } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompt,
similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
});
let systemPrompts: { return {
obj: ChatRoleEnum; userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
value: string; quotePrompt: [quotePrompt]
}[] = []; };
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
// search result is empty
if (code === 201) {
return isStream
? res.send(searchPrompts[0]?.value)
: jsonRes(res, {
data: searchPrompts[0]?.value,
message: searchPrompts[0]?.value
});
} }
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
systemPrompts = searchPrompts; // search result is empty
} else if (model.chat.systemPrompt) { if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
systemPrompts = [ const response = model.chat.searchEmptyText;
{ return res.end(response);
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
];
} }
prompts.unshift(...systemPrompts); // 读取对话内容
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt];
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 计算温度 // 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed( const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
...@@ -123,7 +120,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -123,7 +120,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await modelServiceToolMap[model.chat.chatModel].chatCompletion({ await modelServiceToolMap[model.chat.chatModel].chatCompletion({
apiKey, apiKey,
temperature: +temperature, temperature: +temperature,
messages: prompts, messages: completePrompts,
stream: isStream, stream: isStream,
res, res,
chatId: conversationId chatId: conversationId
......
...@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
({ _id, apiKey, createTime, lastUsedTime }) => { ({ _id, apiKey, createTime, lastUsedTime }) => {
return { return {
id: _id, id: _id,
apiKey: `${apiKey.substring(0, 2)}******${apiKey.substring(apiKey.length - 2)}`, apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
createTime, createTime,
lastUsedTime lastUsedTime
}; };
......
...@@ -5,7 +5,6 @@ import { PgClient } from '@/service/pg'; ...@@ -5,7 +5,6 @@ import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools'; import { withNextCors } from '@/service/utils/tools';
import type { ChatItemSimpleType } from '@/types/chat'; import type { ChatItemSimpleType } from '@/types/chat';
import type { ModelSchema } from '@/types/mongoSchema'; import type { ModelSchema } from '@/types/mongoSchema';
import { appVectorSearchModeEnum } from '@/constants/model';
import { authModel } from '@/service/utils/auth'; import { authModel } from '@/service/utils/auth';
import { ChatModelMap } from '@/constants/model'; import { ChatModelMap } from '@/constants/model';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
...@@ -21,16 +20,19 @@ export type QuoteItemType = { ...@@ -21,16 +20,19 @@ export type QuoteItemType = {
type Props = { type Props = {
prompts: ChatItemSimpleType[]; prompts: ChatItemSimpleType[];
similarity: number; similarity: number;
limit: number;
appId: string; appId: string;
}; };
type Response = { type Response = {
code: 200 | 201;
rawSearch: QuoteItemType[]; rawSearch: QuoteItemType[];
guidePrompt: string; userSystemPrompt: {
searchPrompts: {
obj: ChatRoleEnum; obj: ChatRoleEnum;
value: string; value: string;
}[]; };
quotePrompt: {
obj: ChatRoleEnum;
value: string;
};
}; };
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
...@@ -41,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -41,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('userId is empty'); throw new Error('userId is empty');
} }
const { prompts, similarity, appId } = req.body as Props; const { prompts, similarity, limit, appId } = req.body as Props;
if (!similarity || !Array.isArray(prompts) || !appId) { if (!similarity || !Array.isArray(prompts) || !appId) {
throw new Error('params is error'); throw new Error('params is error');
...@@ -58,7 +60,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -58,7 +60,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
userId, userId,
fixedQuote: [], fixedQuote: [],
prompt: prompts[prompts.length - 1], prompt: prompts[prompts.length - 1],
similarity similarity,
limit
}); });
jsonRes<Response>(res, { jsonRes<Response>(res, {
...@@ -78,13 +81,15 @@ export async function appKbSearch({ ...@@ -78,13 +81,15 @@ export async function appKbSearch({
userId, userId,
fixedQuote, fixedQuote,
prompt, prompt,
similarity similarity = 0.8,
limit = 5
}: { }: {
model: ModelSchema; model: ModelSchema;
userId: string; userId: string;
fixedQuote: QuoteItemType[]; fixedQuote: QuoteItemType[];
prompt: ChatItemSimpleType; prompt: ChatItemSimpleType;
similarity: number; similarity: number;
limit: number;
}): Promise<Response> { }): Promise<Response> {
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
...@@ -103,7 +108,7 @@ export async function appKbSearch({ ...@@ -103,7 +108,7 @@ export async function appKbSearch({
.map((item) => `'${item}'`) .map((item) => `'${item}'`)
.join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${ .join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${
promptVector[0] promptVector[0]
}]' limit 10; }]' limit ${limit};
COMMIT;` COMMIT;`
); );
...@@ -115,7 +120,7 @@ export async function appKbSearch({ ...@@ -115,7 +120,7 @@ export async function appKbSearch({
...searchRes.slice(0, 3), ...searchRes.slice(0, 3),
...fixedQuote.slice(0, 2), ...fixedQuote.slice(0, 2),
...searchRes.slice(3), ...searchRes.slice(3),
...fixedQuote.slice(2, 5) ...fixedQuote.slice(2, 4)
].filter((item) => { ].filter((item) => {
if (idSet.has(item.id)) { if (idSet.has(item.id)) {
return false; return false;
...@@ -125,86 +130,44 @@ export async function appKbSearch({ ...@@ -125,86 +130,44 @@ export async function appKbSearch({
}); });
// 计算固定提示词的 token 数量 // 计算固定提示词的 token 数量
const guidePrompt = model.chat.systemPrompt // user system prompt const userSystemPrompt = model.chat.systemPrompt // user system prompt
? { ? {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.Human,
value: model.chat.systemPrompt value: model.chat.systemPrompt
} }
: model.chat.searchMode === appVectorSearchModeEnum.noContext
? {
obj: ChatRoleEnum.System,
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
}
: { : {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.Human,
value: `玩一个问答游戏,规则为: value: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。`
1.你完全忘记你已有的知识
2.你只回答关于"${model.name}"的问题
3.你只从知识库中选择内容进行回答
4.如果问题不在知识库中,你会回答:"我不知道。"
请务必遵守规则`
}; };
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({ const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [guidePrompt] messages: [userSystemPrompt]
}); });
// filter part quote by maxToken
const sliceResult = modelToolMap[model.chat.chatModel] const sliceResult = modelToolMap[model.chat.chatModel]
.tokenSlice({ .tokenSlice({
maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens, maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens,
messages: filterSearch.map((item) => ({ messages: filterSearch.map((item, i) => ({
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: `${item.q}\n${item.a}` value: `${i + 1}: [${item.q}\n${item.a}]`
})) }))
}) })
.map((item) => item.value); .map((item) => item.value)
.join('\n')
.trim();
// slice filterSearch // slice filterSearch
const rawSearch = filterSearch.slice(0, sliceResult.length); const rawSearch = filterSearch.slice(0, sliceResult.length);
// system prompt const quoteText = sliceResult ? `知识库:\n${sliceResult}` : '';
const systemPrompt = sliceResult.join('\n').trim();
/* 高相似度+不回复 */
if (!systemPrompt && model.chat.searchMode === appVectorSearchModeEnum.hightSimilarity) {
return {
code: 201,
rawSearch: [],
guidePrompt: '',
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: '对不起,你的问题不在知识库中。'
}
]
};
}
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
if (!systemPrompt && model.chat.searchMode === appVectorSearchModeEnum.noContext) {
return {
code: 200,
rawSearch: [],
guidePrompt: model.chat.systemPrompt || '',
searchPrompts: model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: []
};
}
return { return {
code: 200,
rawSearch, rawSearch,
guidePrompt: guidePrompt.value || '', userSystemPrompt,
searchPrompts: [ quotePrompt: {
{ obj: ChatRoleEnum.System,
obj: ChatRoleEnum.System, value: quoteText
value: `知识库:<${systemPrompt}>` }
},
guidePrompt
]
}; };
} }
...@@ -4,7 +4,7 @@ import { jsonRes } from '@/service/response'; ...@@ -4,7 +4,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo'; import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890'); const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
...@@ -14,11 +14,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -14,11 +14,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const count = await OpenApi.find({ userId }).countDocuments(); const count = await OpenApi.find({ userId }).countDocuments();
if (count >= 5) { if (count >= 10) {
throw new Error('最多 5 组API Key'); throw new Error('最多 10 组 API 秘钥');
} }
const apiKey = `${userId}-${nanoid()}`; const apiKey = `fastgpt-${nanoid()}`;
await OpenApi.create({ await OpenApi.create({
userId, userId,
......
...@@ -83,7 +83,7 @@ const ModelList = ({ modelId }: { modelId: string }) => { ...@@ -83,7 +83,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}> <Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
<Input <Input
h={'32px'} h={'32px'}
placeholder="搜索 AI 应用" placeholder="根据名字和介绍搜索 AI 应用"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
/> />
...@@ -111,7 +111,7 @@ const ModelList = ({ modelId }: { modelId: string }) => { ...@@ -111,7 +111,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
/> />
</Tooltip> </Tooltip>
</Flex> </Flex>
<Flex mb={3} userSelect={'none'}> <Flex userSelect={'none'}>
<Box flex={1}></Box> <Box flex={1}></Box>
<Tabs <Tabs
w={'130px'} w={'130px'}
...@@ -129,7 +129,7 @@ const ModelList = ({ modelId }: { modelId: string }) => { ...@@ -129,7 +129,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex <Flex
key={item._id} key={item._id}
position={'relative'} position={'relative'}
alignItems={['flex-start', 'center']} alignItems={'center'}
p={3} p={3}
mb={[2, 0]} mb={[2, 0]}
cursor={'pointer'} cursor={'pointer'}
...@@ -154,9 +154,6 @@ const ModelList = ({ modelId }: { modelId: string }) => { ...@@ -154,9 +154,6 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Box className="textEllipsis" color={'myGray.1000'}> <Box className="textEllipsis" color={'myGray.1000'}>
{item.name} {item.name}
</Box> </Box>
<Box className="textEllipsis" color={'myGray.400'} fontSize={'sm'}>
{item.systemPrompt || '这个 应用 没有设置提示词~'}
</Box>
</Box> </Box>
</Flex> </Flex>
))} ))}
......
import React, { useState } from 'react';
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
ssr: true
});
const baseUrl = 'https://fastgpt.run/api/openapi';
const API = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const { copyData } = useCopyData();
const {
isOpen: isOpenAPIModal,
onOpen: onOpenAPIModal,
onClose: onCloseAPIModal
} = useDisclosure();
const [isLoaded, setIsLoaded] = useState(false);
return (
<Flex flexDirection={'column'} h={'100%'}>
<Box display={['none', 'flex']} px={5} alignItems={'center'}>
<Box flex={1}>
AppId:
<Box
as={'span'}
ml={2}
fontWeight={'bold'}
cursor={'pointer'}
onClick={() => copyData(modelId, '已复制 AppId')}
>
{modelId}
</Box>
</Box>
<Flex
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
onClick={() => copyData(baseUrl, '已复制 API 地址')}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
API服务器
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Button
ml={3}
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
variant={'base'}
onClick={onOpenAPIModal}
>
API 秘钥
</Button>
</Box>
<Divider mt={3} />
<Box flex={1}>
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
<iframe
style={{
width: '100%',
height: '100%'
}}
src="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh"
frameBorder="0"
onLoad={() => setIsLoaded(true)}
onError={() => setIsLoaded(true)}
/>
</Skeleton>
</Box>
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
</Flex>
);
};
export default API;
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react';
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, putModelById } from '@/api/model';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { useConfirm } from '@/hooks/useConfirm';
import { ChatModelMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import type { ModelSchema } from '@/types/mongoSchema';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
const Settings = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { Loading, setIsLoading } = useLoading();
const { userInfo, modelDetail, loadModelDetail, 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 isOwner = useMemo(
() => modelDetail.userId === userInfo?._id,
[modelDetail.userId, userInfo?._id]
);
const {
register,
setValue,
getValues,
formState: { errors },
reset,
handleSubmit
} = useForm({
defaultValues: modelDetail
});
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
setBtnLoading(true);
try {
await putModelById(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 (!modelDetail) return;
setIsLoading(true);
try {
await delModelById(modelDetail._id);
toast({
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(modelDetail._id);
router.replace('/model');
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [modelDetail, setIsLoading, toast, refreshModel, router]);
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], () => loadModelDetail(modelId, true), {
onSuccess(res) {
res && reset(res);
modelId && setLastModelId(modelId);
},
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
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={5}
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={['200px', '240px']}
value={getValues('chat.chatModel')}
list={chatModelList.map((item) => ({
id: item.chatModel,
label: item.name
}))}
onchange={(val: any) => {
setValue('chat.chatModel', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
价格
</Box>
<Box fontSize={['sm', 'md']}>
{formatPrice(ChatModelMap[getValues('chat.chatModel')]?.price, 1000)}
元/1K tokens(包括上下文和回答)
</Box>
</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={['100%', '260px']}
min={0}
max={10}
activeVal={getValues('chat.temperature')}
setVal={(val) => {
setValue('chat.temperature', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex mt={10} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
提示词
</Box>
<Textarea
rows={8}
placeholder={
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索,没有填写该内容时,系统会自动补充提示词;如果填写了内容,则以填写的内容为准。'
}
{...register('chat.systemPrompt')}
></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}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
保存
</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>
<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;
import React, { useCallback, useState, useMemo, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { delModelById, putModelById } from '@/api/model'; import { Box, Flex } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading'; import { useGlobalStore } from '@/store/global';
import Loading from '@/components/Loading';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Tabs from '@/components/Tabs';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'), { import Settings from './components/Settings';
loading: () => <Loading fixed={false} />,
ssr: false
});
const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { toast } = useToast();
const router = useRouter();
const { userInfo, modelDetail, loadModelDetail, refreshModel, setLastModelId } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [btnLoading, setBtnLoading] = useState(false);
const formHooks = useForm({
defaultValues: modelDetail
});
// load model data
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId), {
onSuccess(res) {
res && formHooks.reset(res);
modelId && setLastModelId(modelId);
},
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});
const isOwner = useMemo(
() => modelDetail.userId === userInfo?._id,
[modelDetail.userId, userInfo?._id]
);
const canRead = useMemo(
() => isOwner || isLoading || modelDetail.share.isShareDetail,
[isLoading, isOwner, modelDetail.share.isShareDetail]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!modelDetail) return;
setIsLoading(true);
try {
await delModelById(modelDetail._id);
toast({
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(modelDetail._id);
router.replace('/model');
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [modelDetail, setIsLoading, toast, refreshModel, router]);
/* 点前往聊天预览页 */ const Kb = dynamic(() => import('./components/Kb'), {
const handlePreviewChat = useCallback(async () => { ssr: true
router.push(`/chat?modelId=${modelId}`); });
}, [router, modelId]); const Share = dynamic(() => import('./components/Share'), {
ssr: true
// 提交保存模型修改 });
const saveSubmitSuccess = useCallback( const API = dynamic(() => import('./components/API'), {
async (data: ModelSchema) => { ssr: true
setBtnLoading(true); });
try {
await putModelById(data._id, {
name: data.name,
avatar: data.avatar || '/icon/logo.png',
chat: data.chat,
share: data.share
});
refreshModel.updateModelDetail(data); enum TabEnum {
} catch (err: any) { 'settings' = 'settings',
toast({ 'kb' = 'kb',
title: err?.message || '更新失败', 'share' = 'share',
status: 'error' 'API' = 'API'
}); }
}
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(formHooks.formState.errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [formHooks.formState.errors, toast]);
const saveUpdateModel = useCallback( const ModelDetail = ({ modelId }: { modelId: string }) => {
() => formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)(), const router = useRouter();
[formHooks, saveSubmitError, saveSubmitSuccess] const { isPc } = useGlobalStore();
); const { modelDetail } = useUserStore();
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
useEffect(() => { useEffect(() => {
window.onbeforeunload = (e) => { window.onbeforeunload = (e) => {
...@@ -137,86 +42,54 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => { ...@@ -137,86 +42,54 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
}; };
}, [router]); }, [router]);
useEffect(() => {
setCurrentTab(TabEnum.settings);
}, [modelId]);
return ( return (
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}> <Flex
flexDirection={'column'}
h={'100%'}
maxW={'100vw'}
pt={4}
overflow={'overlay'}
position={'relative'}
bg={'white'}
>
{/* 头部 */} {/* 头部 */}
<Card px={6} py={3}> <Box textAlign={['center', 'left']} px={5} mb={4}>
{isPc ? ( <Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
<Flex alignItems={'center'}> {modelDetail.name}
<Box fontSize={'xl'} fontWeight={'bold'}> </Box>
{modelDetail.name} <Tabs
</Box> mx={['auto', '0']}
<Box flex={1} /> mt={2}
<Button variant={'base'} onClick={handlePreviewChat}> w={['300px', '360px']}
开始对话 list={[
</Button> { label: '配置', id: TabEnum.settings },
{isOwner && ( { label: '知识库', id: TabEnum.kb },
<Button { label: '分享', id: TabEnum.share },
isLoading={btnLoading} { label: 'API', id: TabEnum.API },
ml={4} { label: '立即对话', id: 'startChat' }
onClick={async () => { ]}
try { size={isPc ? 'md' : 'sm'}
await saveUpdateModel(); activeId={currentTab}
toast({ onChange={(e: any) => {
title: '更新成功', if (e === 'startChat') {
status: 'success' router.push(`/chat?modelId=${modelId}`);
}); } else {
} catch (error) { setCurrentTab(e);
console.log(error); }
error; }}
}
}}
>
保存修改
</Button>
)}
</Flex>
) : (
<>
<Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{modelDetail.name}
</Box>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'base'} size={'sm'} onClick={handlePreviewChat}>
开始对话
</Button>
{isOwner && (
<Button
ml={4}
size={'sm'}
isLoading={btnLoading}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
保存修改
</Button>
)}
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm
formHooks={formHooks}
handleDelModel={handleDelModel}
isOwner={isOwner}
canRead={canRead}
/> />
</Grid> </Box>
<Loading loading={isLoading} fixed={false} /> <Box flex={1}>
</Box> {currentTab === TabEnum.settings && <Settings modelId={modelId} />}
{currentTab === TabEnum.kb && <Kb modelId={modelId} />}
{currentTab === TabEnum.API && <API modelId={modelId} />}
{currentTab === TabEnum.share && <Share modelId={modelId} />}
</Box>
</Flex>
); );
}; };
......
...@@ -34,7 +34,7 @@ const Model = ({ modelId }: { modelId: string }) => { ...@@ -34,7 +34,7 @@ const Model = ({ modelId }: { modelId: string }) => {
</SideBar> </SideBar>
)} )}
<Box flex={1} h={'100%'} position={'relative'}> <Box flex={1} h={'100%'} position={'relative'}>
{modelId && <ModelDetail modelId={modelId} isPc={isPc} />} {modelId && <ModelDetail modelId={modelId} />}
</Box> </Box>
</Flex> </Flex>
); );
......
...@@ -44,7 +44,7 @@ const ShareModelList = ({ ...@@ -44,7 +44,7 @@ const ShareModelList = ({
{model.name} {model.name}
</Box> </Box>
</Flex> </Flex>
<Tooltip label={model.share.intro}> <Tooltip label={model.intro}>
<Box <Box
className={styles.intro} className={styles.intro}
flex={1} flex={1}
...@@ -53,7 +53,7 @@ const ShareModelList = ({ ...@@ -53,7 +53,7 @@ const ShareModelList = ({
wordBreak={'break-all'} wordBreak={'break-all'}
color={'blackAlpha.600'} color={'blackAlpha.600'}
> >
{model.share.intro || '这个 应用 还没有介绍~'} {model.intro || '这个应用还没有介绍~'}
</Box> </Box>
</Tooltip> </Tooltip>
......
...@@ -5,8 +5,3 @@ ...@@ -5,8 +5,3 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.textlg {
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
...@@ -45,7 +45,7 @@ const modelList = () => { ...@@ -45,7 +45,7 @@ const modelList = () => {
return ( return (
<Box px={[5, 10]} py={[4, 6]} position={'relative'} minH={'109vh'}> <Box px={[5, 10]} py={[4, 6]} position={'relative'} minH={'109vh'}>
<Flex alignItems={'center'} mb={2}> <Flex alignItems={'center'} mb={2}>
<Box className={styles.textlg} fontWeight={'bold'} fontSize={'3xl'}> <Box className={'textlg'} fontWeight={'bold'} fontSize={'3xl'}>
AI 应用市场 AI 应用市场
</Box> </Box>
{/* <Box mt={[2, 0]} textAlign={'right'}> {/* <Box mt={[2, 0]} textAlign={'right'}>
......
...@@ -16,11 +16,6 @@ const list = [ ...@@ -16,11 +16,6 @@ const list = [
link: '/model/share' link: '/model/share'
}, },
{ {
icon: 'develop',
label: '开发',
link: '/openapi'
},
{
icon: 'git', icon: 'git',
label: 'Git项目地址', label: 'Git项目地址',
link: 'https://github.com/c121914yu/FastGPT' link: 'https://github.com/c121914yu/FastGPT'
......
import { Schema, model, models, Model as MongoModel } from 'mongoose'; import { Schema, model, models, Model as MongoModel } from 'mongoose';
import { ModelSchema as ModelType } from '@/types/mongoSchema'; import { ModelSchema as ModelType } from '@/types/mongoSchema';
import { import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
ModelVectorSearchModeMap,
appVectorSearchModeEnum,
ChatModelMap,
OpenAiChatEnum
} from '@/constants/model';
const ModelSchema = new Schema({ const ModelSchema = new Schema({
userId: { userId: {
...@@ -21,10 +16,9 @@ const ModelSchema = new Schema({ ...@@ -21,10 +16,9 @@ const ModelSchema = new Schema({
type: String, type: String,
default: '/icon/logo.png' default: '/icon/logo.png'
}, },
status: { intro: {
type: String, type: String,
required: true, default: ''
enum: ['waiting', 'running', 'training', 'closed']
}, },
updateTime: { updateTime: {
type: Date, type: Date,
...@@ -36,11 +30,17 @@ const ModelSchema = new Schema({ ...@@ -36,11 +30,17 @@ const ModelSchema = new Schema({
ref: 'kb', ref: 'kb',
default: [] default: []
}, },
searchMode: { searchSimilarity: {
// knowledge base search mode type: Number,
default: 0.8
},
searchLimit: {
type: Number,
default: 5
},
searchEmptyText: {
type: String, type: String,
enum: Object.keys(ModelVectorSearchModeMap), default: ''
default: appVectorSearchModeEnum.hightSimilarity
}, },
systemPrompt: { systemPrompt: {
// 系统提示词 // 系统提示词
......
...@@ -83,6 +83,11 @@ textarea::placeholder { ...@@ -83,6 +83,11 @@ textarea::placeholder {
.grecaptcha-badge { .grecaptcha-badge {
display: none !important; display: none !important;
} }
.textlg {
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@media (max-width: 900px) { @media (max-width: 900px) {
html { html {
......
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema, kbSchema } from './mongoSchema'; import type { ModelSchema, kbSchema } from './mongoSchema';
import { ChatModelType, appVectorSearchModeEnum } from '@/constants/model'; import { ChatModelType } from '@/constants/model';
export type ModelListItemType = { export type ModelListItemType = {
_id: string; _id: string;
...@@ -10,16 +9,18 @@ export type ModelListItemType = { ...@@ -10,16 +9,18 @@ export type ModelListItemType = {
}; };
export interface ModelUpdateParams { export interface ModelUpdateParams {
name: string; name?: string;
avatar: string; avatar?: string;
chat: ModelSchema['chat']; intro?: string;
share: ModelSchema['share']; chat?: ModelSchema['chat'];
share?: ModelSchema['share'];
} }
export interface ShareModelItem { export interface ShareModelItem {
_id: string; _id: string;
avatar: string; avatar: string;
name: string; name: string;
intro: string;
userId: string; userId: string;
share: ModelSchema['share']; share: ModelSchema['share'];
isCollection: boolean; isCollection: boolean;
......
import type { ChatItemType } from './chat'; import type { ChatItemType } from './chat';
import { import { ModelNameEnum, ChatModelType, EmbeddingModelType } from '@/constants/model';
ModelStatusEnum,
ModelNameEnum,
appVectorSearchModeEnum,
ChatModelType,
EmbeddingModelType
} from '@/constants/model';
import type { DataType } from './data'; import type { DataType } from './data';
import { BillTypeEnum, InformTypeEnum } from '@/constants/user'; import { BillTypeEnum, InformTypeEnum } from '@/constants/user';
import { TrainingModeEnum } from '@/constants/plugin'; import { TrainingModeEnum } from '@/constants/plugin';
...@@ -41,11 +35,13 @@ export interface ModelSchema { ...@@ -41,11 +35,13 @@ export interface ModelSchema {
userId: string; userId: string;
name: string; name: string;
avatar: string; avatar: string;
status: `${ModelStatusEnum}`; intro: string;
updateTime: number; updateTime: number;
chat: { chat: {
relatedKbs: string[]; relatedKbs: string[];
searchMode: `${appVectorSearchModeEnum}`; searchSimilarity: number;
searchLimit: number;
searchEmptyText: string;
systemPrompt: string; systemPrompt: string;
temperature: number; temperature: number;
chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型 chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型
...@@ -53,7 +49,6 @@ export interface ModelSchema { ...@@ -53,7 +49,6 @@ export interface ModelSchema {
share: { share: {
isShare: boolean; isShare: boolean;
isShareDetail: boolean; isShareDetail: boolean;
intro: string;
collection: number; collection: number;
}; };
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment