feat: new app page

This commit is contained in:
archer
2023-06-17 17:04:47 +08:00
parent df2fda6176
commit 61447c60ac
45 changed files with 1652 additions and 1338 deletions

View File

@@ -8,9 +8,9 @@ import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router';
import 'nprogress/nprogress.css';
import '../styles/reset.scss';
import { useGlobalStore } from '@/store/global';
import 'nprogress/nprogress.css';
import '@/styles/reset.scss';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
@@ -28,7 +28,7 @@ const queryClient = new QueryClient({
}
});
export default function App({ Component, pageProps }: AppProps) {
function App({ Component, pageProps }: AppProps) {
const {
loadInitData,
initData: { googleVerKey }
@@ -78,6 +78,5 @@ export default function App({ Component, pageProps }: AppProps) {
);
}
// export function reportWebVitals(metric: NextWebVitalsMetric) {
// console.log(metric);
// }
// @ts-ignore
export default App;

View File

@@ -4,7 +4,7 @@ import { authChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { appKbSearch } from '../openapi/kb/appKbSearch';
@@ -48,36 +48,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 读取对话内容
const prompts = [...content, prompt[0]];
const {
code = 200,
systemPrompts = [],
quote = [],
guidePrompt = ''
rawSearch = [],
userSystemPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts, rawSearch, guidePrompt } = await appKbSearch({
const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
model,
userId,
fixedQuote: content[content.length - 1]?.quote || [],
prompt: prompt[0],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
});
return {
code,
quote: rawSearch,
systemPrompts: searchPrompts,
guidePrompt
rawSearch: rawSearch,
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
guidePrompt: model.chat.systemPrompt,
systemPrompts: [
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
@@ -92,13 +87,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const conversationId = chatId || String(new Types.ObjectId());
!chatId && res.setHeader(NEW_CHATID_HEADER, conversationId);
if (showModelDetail) {
guidePrompt && res.setHeader(GUIDE_PROMPT_HEADER, encodeURIComponent(guidePrompt));
res.setHeader(QUOTE_LEN_HEADER, quote.length);
userSystemPrompt[0] &&
res.setHeader(GUIDE_PROMPT_HEADER, encodeURIComponent(userSystemPrompt[0].value));
res.setHeader(QUOTE_LEN_HEADER, rawSearch.length);
}
// search result is empty
if (code === 201) {
const response = systemPrompts[0]?.value;
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = model.chat.searchEmptyText;
await saveChat({
chatId,
newChatId: conversationId,
@@ -116,11 +112,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.end(response);
}
prompts.unshift(...systemPrompts);
// 读取对话内容
const prompts = [...quotePrompt, ...content, ...userSystemPrompt, prompt[0]];
// content check
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)
{
...prompt[1],
value: responseContent,
quote: showModelDetail ? quote : [],
systemPrompt: showModelDetail ? guidePrompt : ''
quote: showModelDetail ? rawSearch : [],
systemPrompt: showModelDetail ? userSystemPrompt[0]?.value : ''
}
],
userId

View File

@@ -6,7 +6,6 @@ import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { authModel } from '@/service/utils/auth';
import mongoose from 'mongoose';
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema } from '@/types/mongoSchema';
/* 初始化我的聊天框,需要身份验证 */
@@ -29,8 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!myModel) {
const { _id } = await Model.create({
name: '应用1',
userId,
status: ModelStatusEnum.running
userId
});
model = (await Model.findById(_id)) as ModelSchema;
} else {
@@ -95,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: {
name: model.name,
avatar: model.avatar,
intro: model.share.intro,
intro: model.intro,
canUse: model.share.isShare || String(model.userId) === userId
},
chatModel: model.chat.chatModel,

View File

@@ -4,7 +4,7 @@ import { authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { ChatRoleEnum } from '@/constants/chat';
@@ -40,26 +40,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
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) {
const { code, searchPrompts } = await appKbSearch({
const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
prompt: prompt,
similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
});
return {
code,
systemPrompts: searchPrompts
rawSearch: rawSearch,
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
systemPrompts: [
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
@@ -71,15 +78,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})();
// search result is empty
if (code === 201) {
return res.send(systemPrompts[0]?.value);
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = model.chat.searchEmptyText;
return res.end(response);
}
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('')
input: [...quotePrompt, ...userSystemPrompt, prompt].map((item) => item.value).join('')
});
// 计算温度
@@ -93,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
messages: completePrompts,
stream: true,
res,
chatId: historyId

View File

@@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: {
name: model.name,
avatar: model.avatar,
intro: model.share.intro
intro: model.intro
},
chatModel: model.chat.chatModel
}

View File

@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { ModelStatusEnum } from '@/constants/model';
import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -32,8 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 创建模型
const response = await Model.create({
name,
userId,
status: ModelStatusEnum.running
userId
});
jsonRes(res, {

View File

@@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
$and: [
{ '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<
avatar: { $ifNull: ['$avatar', '/icon/logo.png'] },
name: 1,
userId: 1,
intro: 1,
share: 1,
isCollection: {
$cond: {

View File

@@ -9,10 +9,10 @@ import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
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 };
if (!name || !chat || !modelId) {
if (!modelId) {
throw new Error('参数错误');
}
@@ -35,10 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
{
name,
avatar,
intro,
chat,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro
...(share && {
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail
})
}
);

View File

@@ -4,12 +4,11 @@ import { authUser, authModel, getApiKey } from '@/service/utils/auth';
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '../../openapi/text/sensitiveCheck';
import { NEW_CHATID_HEADER } from '@/constants/chat';
import { Types } from 'mongoose';
import { appKbSearch } from '../kb/appKbSearch';
@@ -66,48 +65,46 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1];
let systemPrompts: {
obj: ChatRoleEnum;
value: string;
}[] = [];
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
});
// 使用了知识库搜索
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
});
return {
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
systemPrompts = searchPrompts;
} else if (model.chat.systemPrompt) {
systemPrompts = [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
];
// search result is empty
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = model.chat.searchEmptyText;
return res.end(response);
}
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 读取对话内容
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt];
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
@@ -123,7 +120,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await modelServiceToolMap[model.chat.chatModel].chatCompletion({
apiKey,
temperature: +temperature,
messages: prompts,
messages: completePrompts,
stream: isStream,
res,
chatId: conversationId

View File

@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
({ _id, apiKey, createTime, lastUsedTime }) => {
return {
id: _id,
apiKey: `${apiKey.substring(0, 2)}******${apiKey.substring(apiKey.length - 2)}`,
apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
createTime,
lastUsedTime
};

View File

@@ -5,7 +5,6 @@ import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools';
import type { ChatItemSimpleType } from '@/types/chat';
import type { ModelSchema } from '@/types/mongoSchema';
import { appVectorSearchModeEnum } from '@/constants/model';
import { authModel } from '@/service/utils/auth';
import { ChatModelMap } from '@/constants/model';
import { ChatRoleEnum } from '@/constants/chat';
@@ -21,16 +20,19 @@ export type QuoteItemType = {
type Props = {
prompts: ChatItemSimpleType[];
similarity: number;
limit: number;
appId: string;
};
type Response = {
code: 200 | 201;
rawSearch: QuoteItemType[];
guidePrompt: string;
searchPrompts: {
userSystemPrompt: {
obj: ChatRoleEnum;
value: string;
}[];
};
quotePrompt: {
obj: ChatRoleEnum;
value: string;
};
};
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
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) {
throw new Error('params is error');
@@ -58,7 +60,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity
similarity,
limit
});
jsonRes<Response>(res, {
@@ -78,13 +81,15 @@ export async function appKbSearch({
userId,
fixedQuote,
prompt,
similarity
similarity = 0.8,
limit = 5
}: {
model: ModelSchema;
userId: string;
fixedQuote: QuoteItemType[];
prompt: ChatItemSimpleType;
similarity: number;
limit: number;
}): Promise<Response> {
const modelConstantsData = ChatModelMap[model.chat.chatModel];
@@ -103,7 +108,7 @@ export async function appKbSearch({
.map((item) => `'${item}'`)
.join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${
promptVector[0]
}]' limit 10;
}]' limit ${limit};
COMMIT;`
);
@@ -115,7 +120,7 @@ export async function appKbSearch({
...searchRes.slice(0, 3),
...fixedQuote.slice(0, 2),
...searchRes.slice(3),
...fixedQuote.slice(2, 5)
...fixedQuote.slice(2, 4)
].filter((item) => {
if (idSet.has(item.id)) {
return false;
@@ -125,86 +130,44 @@ export async function appKbSearch({
});
// 计算固定提示词的 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
}
: model.chat.searchMode === appVectorSearchModeEnum.noContext
? {
obj: ChatRoleEnum.System,
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
}
: {
obj: ChatRoleEnum.System,
value: `玩一个问答游戏,规则为:
1.你完全忘记你已有的知识
2.你只回答关于"${model.name}"的问题
3.你只从知识库中选择内容进行回答
4.如果问题不在知识库中,你会回答:"我不知道。"
请务必遵守规则`
obj: ChatRoleEnum.Human,
value: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。`
};
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [guidePrompt]
messages: [userSystemPrompt]
});
// filter part quote by maxToken
const sliceResult = modelToolMap[model.chat.chatModel]
.tokenSlice({
maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens,
messages: filterSearch.map((item) => ({
messages: filterSearch.map((item, i) => ({
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
const rawSearch = filterSearch.slice(0, sliceResult.length);
// system prompt
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
}
]
: []
};
}
const quoteText = sliceResult ? `知识库:\n${sliceResult}` : '';
return {
code: 200,
rawSearch,
guidePrompt: guidePrompt.value || '',
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: `知识库:<${systemPrompt}>`
},
guidePrompt
]
userSystemPrompt,
quotePrompt: {
obj: ChatRoleEnum.System,
value: quoteText
}
};
}

View File

@@ -4,7 +4,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890');
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -14,11 +14,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const count = await OpenApi.find({ userId }).countDocuments();
if (count >= 5) {
throw new Error('最多 5 组API Key');
if (count >= 10) {
throw new Error('最多 10 API 秘钥');
}
const apiKey = `${userId}-${nanoid()}`;
const apiKey = `fastgpt-${nanoid()}`;
await OpenApi.create({
userId,

View File

@@ -83,7 +83,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
<Input
h={'32px'}
placeholder="搜索 AI 应用"
placeholder="根据名字和介绍搜索 AI 应用"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
@@ -111,7 +111,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
/>
</Tooltip>
</Flex>
<Flex mb={3} userSelect={'none'}>
<Flex userSelect={'none'}>
<Box flex={1}></Box>
<Tabs
w={'130px'}
@@ -129,7 +129,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex
key={item._id}
position={'relative'}
alignItems={['flex-start', 'center']}
alignItems={'center'}
p={3}
mb={[2, 0]}
cursor={'pointer'}
@@ -154,9 +154,6 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
<Box className="textEllipsis" color={'myGray.400'} fontSize={'sm'}>
{item.systemPrompt || '这个 应用 没有设置提示词~'}
</Box>
</Box>
</Flex>
))}

View File

@@ -0,0 +1,82 @@
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;

View File

@@ -0,0 +1,394 @@
import React, { useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import {
Card,
Flex,
Box,
Button,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
ModalFooter,
ModalCloseButton,
Grid,
useTheme,
IconButton,
Tooltip,
Textarea
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import Avatar from '@/components/Avatar';
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { putModelById } from '@/api/model';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useForm } from 'react-hook-form';
import MyIcon from '@/components/Icon';
import MySlider from '@/components/Slider';
const Kb = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const router = useRouter();
const { toast } = useToast();
const { modelDetail, loadKbList, loadModelDetail } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
const [refresh, setRefresh] = useState(false);
const { register, reset, getValues, setValue } = useForm({
defaultValues: {
searchSimilarity: modelDetail.chat.searchSimilarity,
searchLimit: modelDetail.chat.searchLimit,
searchEmptyText: modelDetail.chat.searchEmptyText
}
});
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenEditParams,
onOpen: onOpenEditParams,
onClose: onCloseEditParams
} = useDisclosure();
const onchangeKb = useCallback(
async (
data: {
relatedKbs?: string[];
searchSimilarity?: number;
searchLimit?: number;
searchEmptyText?: string;
} = {}
) => {
setIsLoading(true);
try {
await putModelById(modelId, {
chat: {
...modelDetail.chat,
...data
}
});
loadModelDetail(modelId, true);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setIsLoading(false);
},
[setIsLoading, modelId, modelDetail.chat, loadModelDetail, toast]
);
// init kb select list
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
return (
<Box position={'relative'} px={5} minH={'50vh'}>
<Box fontWeight={'bold'}>({modelDetail.chat?.relatedKbs.length})</Box>
{(() => {
const kbs =
modelDetail.chat?.relatedKbs
?.map((id) => kbList.find((kb) => kb._id === id))
.filter((item) => item) || [];
return (
<Grid
mt={2}
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={[3, 4]}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
reset({
searchSimilarity: modelDetail.chat.searchSimilarity,
searchLimit: modelDetail.chat.searchLimit,
searchEmptyText: modelDetail.chat.searchEmptyText
});
onOpenEditParams();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<MyIcon name={'edit'} w={'14px'} color={'myGray.600'} />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
: {modelDetail.chat.searchSimilarity}, :{' '}
{modelDetail.chat.searchLimit}, :{' '}
{modelDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
</Flex>
</Card>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
setSelectedIdList(
modelDetail.chat?.relatedKbs ? [...modelDetail.chat?.relatedKbs] : []
);
onOpenKbSelect();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<AddIcon />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
AI
</Flex>
</Card>
{kbs.map((item) =>
item ? (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
_hover={{
boxShadow: 'lg',
'& .detailBtn': {
display: 'block'
},
'& .delete': {
display: 'block'
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['26px', '32px', '38px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
<Flex mt={3} alignItems={'flex-end'} justifyContent={'flex-end'} h={'30px'}>
<Button
mr={3}
className="detailBtn"
display={['flex', 'none']}
variant={'base'}
size={'sm'}
onClick={() => router.push(`/kb?kbId=${item._id}`)}
>
</Button>
<IconButton
className="delete"
display={['flex', 'none']}
icon={<DeleteIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
_hover={{ color: 'red.600' }}
onClick={() => {
const ids = modelDetail.chat?.relatedKbs
? [...modelDetail.chat.relatedKbs]
: [];
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
onchangeKb({ relatedKbs: ids });
}}
/>
</Flex>
</Card>
) : null
)}
</Grid>
);
})()}
{/* select kb modal */}
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent
display={'flex'}
flexDirection={'column'}
w={'800px'}
maxW={'90vw'}
h={['90vh', 'auto']}
>
<ModalHeader>({selectedIdList.length})</ModalHeader>
<ModalCloseButton />
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{kbList.map((item) => (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
order={modelDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
_hover={{
boxShadow: 'md'
}}
{...(selectedIdList.includes(item._id)
? {
bg: 'myBlue.300'
}
: {})}
onClick={() => {
let ids = [...selectedIdList];
if (!selectedIdList.includes(item._id)) {
ids = ids.concat(item._id);
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
}
ids = ids.filter((id) => kbList.find((item) => item._id === id));
setSelectedIdList(ids);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
onCloseKbSelect();
onchangeKb({ relatedKbs: selectedIdList });
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* edit mode */}
<Modal isOpen={isOpenEditParams} onClose={onCloseEditParams}>
<ModalOverlay />
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex pt={3} pb={5}>
<Box flex={'0 0 100px'}>
<Tooltip label={'高相似度推荐0.8及以上。'}>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Box>
<MySlider
markList={[
{ label: '0', value: 0 },
{ label: '1', value: 1 }
]}
min={0}
max={1}
step={0.01}
activeVal={getValues('searchSimilarity')}
setVal={(val) => {
setValue('searchSimilarity', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex py={8}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
]}
min={1}
max={20}
activeVal={getValues('searchLimit')}
setVal={(val) => {
setValue('searchLimit', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex pt={3}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<Textarea
rows={5}
maxLength={500}
placeholder={
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
}
{...register('searchEmptyText')}
></Textarea>
</Box>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseEditParams}>
</Button>
<Button
onClick={() => {
onCloseEditParams();
onchangeKb({
searchSimilarity: getValues('searchSimilarity'),
searchLimit: getValues('searchLimit'),
searchEmptyText: getValues('searchEmptyText')
});
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Kb;

View File

@@ -1,619 +0,0 @@
import React, { useState, useCallback } from 'react';
import {
Box,
Card,
Flex,
FormControl,
Input,
Textarea,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Tooltip,
Button,
Select,
Switch,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Checkbox
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useForm, UseFormReturn } from 'react-hook-form';
import { ChatModelMap, ModelVectorSearchModeMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useToast } from '@/hooks/useToast';
import { compressImg } from '@/utils/file';
import { useQuery } from '@tanstack/react-query';
import { getShareChatList, createShareChat, delShareChatById } from '@/api/chat';
import { useRouter } from 'next/router';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
const ModelEditForm = ({
formHooks,
isOwner,
canRead,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
isOwner: boolean;
canRead: boolean;
handleDelModel: () => void;
}) => {
const router = useRouter();
const { modelId } = router.query as { modelId: string };
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const { setLoading } = useGlobalStore();
const { loadKbList } = useUserStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该应用?'
});
const { copyData } = useCopyData();
const { register, setValue, getValues } = formHooks;
const {
register: registerShareChat,
getValues: getShareChatValues,
setValue: setShareChatValues,
handleSubmit: submitShareChat,
reset: resetShareChat
} = useForm({
defaultValues: defaultShareChat
});
const {
isOpen: isOpenCreateShareChat,
onOpen: onOpenCreateShareChat,
onClose: onCloseCreateShareChat
} = useDisclosure();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, '头像选择异常'),
status: 'warning'
});
}
},
[setValue, toast]
);
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
const { data: shareChatList = [], refetch: refetchShareChatList } = useQuery(
['initShareChatList', modelId],
() => getShareChatList(modelId)
);
const onclickCreateShareChat = useCallback(
async (e: ShareChatEditType) => {
try {
setLoading(true);
const id = await createShareChat({
...e,
modelId
});
onCloseCreateShareChat();
refetchShareChatList();
const url = `你可以与 ${getValues('name')} 进行对话。
对话地址为:${location.origin}/chat/share?shareId=${id}
${e.password ? `密码为: ${e.password}` : ''}`;
copyData(url, '已复制分享地址');
resetShareChat(defaultShareChat);
} catch (err) {
toast({
title: getErrText(err, '创建分享链接异常'),
status: 'warning'
});
console.log(err);
}
setLoading(false);
},
[
copyData,
getValues,
modelId,
onCloseCreateShareChat,
refetchShareChatList,
resetShareChat,
setLoading,
toast
]
);
// format share used token
const formatTokens = (tokens: number) => {
if (tokens < 10000) return tokens;
return `${(tokens / 10000).toFixed(2)}`;
};
// init kb select list
const { data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
return (
<>
{/* basic info */}
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 80px'} w={0}>
AppId
</Box>
<Box userSelect={'all'}>{getValues('_id')}</Box>
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Avatar
src={getValues('avatar')}
w={['28px', '36px']}
h={['28px', '36px']}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.chatModel', {
onChange() {
setRefresh((state) => !state);
}
})}
>
{chatModelList.map((item) => (
<option key={item.chatModel} value={item.chatModel}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Box>
{formatPrice(ChatModelMap[getValues('chat.chatModel')]?.price, 1000)}
/1K tokens()
</Box>
</Flex>
<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 80px'}></Box>
<Button
colorScheme={'gray'}
variant={'base'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
)}
</Card>
{/* model effect */}
{canRead && (
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
<Box as={'span'} mr={2}>
</Box>
<Tooltip label={'温度越高,模型的发散能力越强;温度越低,内容越严谨。'}>
<QuestionOutlineIcon />
</Tooltip>
</Box>
<Slider
aria-label="slider-ex-1"
min={0}
max={10}
step={1}
value={getValues('chat.temperature')}
isDisabled={!isOwner}
onChange={(e) => {
setValue('chat.temperature', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getValues('chat.temperature')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getValues('chat.temperature')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
{getValues('chat.relatedKbs')?.length > 0 && (
<Flex mt={4} alignItems={'center'}>
<Box mr={4} whiteSpace={'nowrap'}>
&emsp;
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.searchMode', {
required: '搜索模式不能为空'
})}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
</option>
))}
</Select>
</Flex>
)}
<Box mt={4}>
<Box mb={1}></Box>
<Textarea
rows={8}
maxLength={-1}
isDisabled={!isOwner}
placeholder={
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索没有填写该内容时系统会自动补充提示词如果填写了内容则以填写的内容为准。'
}
{...register('chat.systemPrompt')}
/>
</Box>
</Card>
)}
{isOwner && (
<>
{/* model share setting */}
{/* <Card p={4}>
<Box fontWeight={'bold'}>分享设置</Box>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={1} fontSize={['sm', 'md']}>
模型分享:
</Box>
<Tooltip label="开启模型分享后,你的模型将会出现在共享市场,可供 FastGpt 所有用户使用。用户使用时不会消耗你的 tokens而是消耗使用者的 tokens。">
<QuestionOutlineIcon mr={3} />
</Tooltip>
<Switch
isChecked={getValues('share.isShare')}
onChange={() => {
setValue('share.isShare', !getValues('share.isShare'));
setRefresh(!refresh);
}}
/>
</Flex>
<Box mt={5}>
<Box>模型介绍</Box>
<Textarea
mt={1}
rows={6}
maxLength={150}
{...register('share.intro')}
placeholder={'介绍模型的功能、场景等吸引更多人来使用最多150字。'}
/>
</Box>
</Box>
</Card> */}
<Card p={4}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}></Box>
<Button size={'sm'} variant={'base'} colorScheme={'myBlue'} onClick={onOpenKbSelect}>
</Button>
</Flex>
{(() => {
const kbs =
getValues('chat.relatedKbs')?.map((id) => kbList.find((kb) => kb._id === id)) || [];
return (
<>
{kbs.map((item) =>
item ? (
<Card
key={item._id}
p={3}
mt={3}
cursor={'pointer'}
onClick={() => router.push(`/kb?kbId=${item._id}`)}
>
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'20px'} h={'20px'}></Avatar>
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Card>
) : null
)}
</>
);
})()}
</Card>
</>
)}
{/* shareChat */}
<Card p={4}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Tooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<QuestionOutlineIcon ml={1} />
</Tooltip>
Beta
</Box>
<Button
size={'sm'}
variant={'base'}
colorScheme={'myBlue'}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: '最多创建10组'
}
: {})}
onClick={onOpenCreateShareChat}
>
</Button>
</Flex>
<TableContainer mt={1} minH={'100px'}>
<Table variant={'simple'} w={'100%'}>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th>tokens消耗</Th>
<Th>使</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
<Td>{item.maxContext}</Td>
<Td>{formatTokens(item.tokens)}</Td>
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
<Td>
<Flex>
<MyIcon
mr={3}
name="copy"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
onClick={() => {
const url = `${location.origin}/chat/share?shareId=${item._id}`;
copyData(url, '已复制分享地址');
}}
/>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red' }}
onClick={async () => {
setLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setLoading(false);
}}
/>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Card>
{/* create shareChat modal */}
<Modal isOpen={isOpenCreateShareChat} onClose={onCloseCreateShareChat}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input
placeholder="记录名字,仅用于展示"
maxLength={20}
{...registerShareChat('name', {
required: '记录名称不能为空'
})}
/>
</Flex>
</FormControl>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
</Flex>
<Box fontSize={'xs'} ml={'60px'}>
</Box>
</FormControl>
<FormControl mt={9}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
</Box>
<Slider
aria-label="slider-ex-1"
min={1}
max={20}
step={1}
value={getShareChatValues('maxContext')}
isDisabled={!isOwner}
onChange={(e) => {
setShareChatValues('maxContext', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getShareChatValues('maxContext')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getShareChatValues('maxContext')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
</Button>
<Button onClick={submitShareChat(onclickCreateShareChat)}></Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* select kb modal */}
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
{kbList.map((item) => (
<Card key={item._id} p={3} mb={3}>
<Checkbox
isChecked={getValues('chat.relatedKbs')?.includes(item._id)}
onChange={(e) => {
const ids = getValues('chat.relatedKbs');
if (e.target.checked) {
setValue('chat.relatedKbs', ids.concat(item._id));
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
setValue('chat.relatedKbs', ids);
}
setRefresh(!refresh);
}}
>
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'20px'} h={'20px'} />
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Checkbox>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button onClick={onCloseKbSelect}>,</Button>
</ModalFooter>
</ModalContent>
</Modal>
<File onSelect={onSelectFile} />
<ConfirmChild />
</>
);
};
export default ModelEditForm;

View File

@@ -0,0 +1,330 @@
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;

View File

@@ -0,0 +1,281 @@
import React, { useCallback, useState } from 'react';
import {
Flex,
Box,
Tooltip,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
FormControl,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Input
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyIcon from '@/components/Icon';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat';
import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
import { useForm } from 'react-hook-form';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/model';
const Share = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const { copyData } = useCopyData();
const {
isOpen: isOpenCreateShareChat,
onOpen: onOpenCreateShareChat,
onClose: onCloseCreateShareChat
} = useDisclosure();
const {
register: registerShareChat,
getValues: getShareChatValues,
setValue: setShareChatValues,
handleSubmit: submitShareChat,
reset: resetShareChat
} = useForm({
defaultValues: defaultShareChat
});
const [refresh, setRefresh] = useState(false);
const {
isFetching,
data: shareChatList = [],
refetch: refetchShareChatList
} = useQuery(['initShareChatList', modelId], () => getShareChatList(modelId));
const onclickCreateShareChat = useCallback(
async (e: ShareChatEditType) => {
try {
setIsLoading(true);
const id = await createShareChat({
...e,
modelId
});
onCloseCreateShareChat();
refetchShareChatList();
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}
${e.password ? `密码为: ${e.password}` : ''}`;
copyData(url, '已复制分享地址');
resetShareChat(defaultShareChat);
} catch (err) {
toast({
title: getErrText(err, '创建分享链接异常'),
status: 'warning'
});
console.log(err);
}
setIsLoading(false);
},
[
copyData,
modelId,
onCloseCreateShareChat,
refetchShareChatList,
resetShareChat,
setIsLoading,
toast
]
);
// format share used token
const formatTokens = (tokens: number) => {
if (tokens < 10000) return tokens;
return `${(tokens / 10000).toFixed(2)}`;
};
return (
<Box position={'relative'} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Tooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Box>
<Button
variant={'base'}
colorScheme={'myBlue'}
size={['sm', 'md']}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: '最多创建10组'
}
: {})}
onClick={onOpenCreateShareChat}
>
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'}>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th>tokens消耗</Th>
<Th>使</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
<Td>{item.maxContext}</Td>
<Td>{formatTokens(item.tokens)}</Td>
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
<Td>
<Flex>
<MyIcon
mr={3}
name="copy"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
onClick={() => {
const url = `${location.origin}/chat/share?shareId=${item._id}`;
copyData(url, '已复制分享地址');
}}
/>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red' }}
onClick={async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
/>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{shareChatList.length === 0 && !isFetching && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
</Box>
</Flex>
)}
{/* create shareChat modal */}
<Modal isOpen={isOpenCreateShareChat} onClose={onCloseCreateShareChat}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input
placeholder="记录名字,仅用于展示"
maxLength={20}
{...registerShareChat('name', {
required: '记录名称不能为空'
})}
/>
</Flex>
</FormControl>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
</Flex>
<Box fontSize={'xs'} ml={'60px'} color={'myGray.600'}>
</Box>
</FormControl>
<FormControl mt={9}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
</Box>
<Slider
aria-label="slider-ex-1"
min={1}
max={20}
step={1}
value={getShareChatValues('maxContext')}
onChange={(e) => {
setShareChatValues('maxContext', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getShareChatValues('maxContext')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getShareChatValues('maxContext')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
</Button>
<Button onClick={submitShareChat(onclickCreateShareChat)}></Button>
</ModalFooter>
</ModalContent>
</Modal>
<Loading loading={isFetching} fixed={false} />
</Box>
);
};
export default Share;

View File

@@ -1,130 +1,35 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { delModelById, putModelById } from '@/api/model';
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 { Box, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading';
import Loading from '@/components/Loading';
import { useGlobalStore } from '@/store/global';
import dynamic from 'next/dynamic';
import Tabs from '@/components/Tabs';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'), {
loading: () => <Loading fixed={false} />,
ssr: false
import Settings from './components/Settings';
const Kb = dynamic(() => import('./components/Kb'), {
ssr: true
});
const Share = dynamic(() => import('./components/Share'), {
ssr: true
});
const API = dynamic(() => import('./components/API'), {
ssr: true
});
const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { toast } = useToast();
enum TabEnum {
'settings' = 'settings',
'kb' = 'kb',
'share' = 'share',
'API' = 'API'
}
const ModelDetail = ({ modelId }: { modelId: string }) => {
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 handlePreviewChat = useCallback(async () => {
router.push(`/chat?modelId=${modelId}`);
}, [router, modelId]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
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);
} 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(formHooks.formState.errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [formHooks.formState.errors, toast]);
const saveUpdateModel = useCallback(
() => formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[formHooks, saveSubmitError, saveSubmitSuccess]
);
const { isPc } = useGlobalStore();
const { modelDetail } = useUserStore();
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
useEffect(() => {
window.onbeforeunload = (e) => {
@@ -137,86 +42,54 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
};
}, [router]);
useEffect(() => {
setCurrentTab(TabEnum.settings);
}, [modelId]);
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}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{modelDetail.name}
</Box>
<Box flex={1} />
<Button variant={'base'} onClick={handlePreviewChat}>
</Button>
{isOwner && (
<Button
isLoading={btnLoading}
ml={4}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
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}
<Box textAlign={['center', 'left']} px={5} mb={4}>
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
{modelDetail.name}
</Box>
<Tabs
mx={['auto', '0']}
mt={2}
w={['300px', '360px']}
list={[
{ label: '配置', id: TabEnum.settings },
{ label: '知识库', id: TabEnum.kb },
{ label: '分享', id: TabEnum.share },
{ label: 'API', id: TabEnum.API },
{ label: '立即对话', id: 'startChat' }
]}
size={isPc ? 'md' : 'sm'}
activeId={currentTab}
onChange={(e: any) => {
if (e === 'startChat') {
router.push(`/chat?modelId=${modelId}`);
} else {
setCurrentTab(e);
}
}}
/>
</Grid>
<Loading loading={isLoading} fixed={false} />
</Box>
</Box>
<Box flex={1}>
{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>
);
};

View File

@@ -34,7 +34,7 @@ const Model = ({ modelId }: { modelId: string }) => {
</SideBar>
)}
<Box flex={1} h={'100%'} position={'relative'}>
{modelId && <ModelDetail modelId={modelId} isPc={isPc} />}
{modelId && <ModelDetail modelId={modelId} />}
</Box>
</Flex>
);

View File

@@ -44,7 +44,7 @@ const ShareModelList = ({
{model.name}
</Box>
</Flex>
<Tooltip label={model.share.intro}>
<Tooltip label={model.intro}>
<Box
className={styles.intro}
flex={1}
@@ -53,7 +53,7 @@ const ShareModelList = ({
wordBreak={'break-all'}
color={'blackAlpha.600'}
>
{model.share.intro || '这个 应用 还没有介绍~'}
{model.intro || '这个应用还没有介绍~'}
</Box>
</Tooltip>

View File

@@ -5,8 +5,3 @@
overflow: hidden;
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;
}

View File

@@ -45,7 +45,7 @@ const modelList = () => {
return (
<Box px={[5, 10]} py={[4, 6]} position={'relative'} minH={'109vh'}>
<Flex alignItems={'center'} mb={2}>
<Box className={styles.textlg} fontWeight={'bold'} fontSize={'3xl'}>
<Box className={'textlg'} fontWeight={'bold'} fontSize={'3xl'}>
AI
</Box>
{/* <Box mt={[2, 0]} textAlign={'right'}>

View File

@@ -1,139 +0,0 @@
import React, { useState } from 'react';
import {
Card,
Box,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
IconButton,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody
} from '@chakra-ui/react';
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import { DeleteIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/utils/tools';
const OpenApi = () => {
const { Loading } = useLoading();
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
const [apiKey, setApiKey] = useState('');
const { copyData } = useCopyData();
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
mutationFn: () => createAOpenApiKey(),
onSuccess(res) {
setApiKey(res);
refetch();
}
});
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
return (
<Box py={[5, 10]} px={'5vw'}>
<Card px={6} py={4} position={'relative'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
FastGpt Api
</Box>
<Box fontSize={'sm'} mt={2}>
FastGpt Api Fast Gpt api
Api
Key
</Box>
<Box>使 Fast Api 使</Box>
<Box
my={1}
as="a"
href="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh"
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>
))}
</Tbody>
</Table>
</TableContainer>
<Button
maxW={'200px'}
mt={5}
isLoading={isCreating}
isDisabled={apiKeys.length >= 5}
title={apiKeys.length >= 5 ? '最多五组 Api Key' : ''}
onClick={() => onclickCreateApiKey()}
>
Api Key
</Button>
<Loading loading={isGetting || isDeleting} fixed={false} />
</Card>
<Modal isOpen={!!apiKey} onClose={() => setApiKey('')}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Api Key</ModalHeader>
<ModalCloseButton />
<ModalBody mb={5}>
Api Key
<Box userSelect={'all'} onClick={() => copyData(apiKey)}>
{apiKey}
</Box>
</ModalBody>
</ModalContent>
</Modal>
</Box>
);
};
export default OpenApi;

View File

@@ -15,11 +15,6 @@ const list = [
label: 'AI应用市场',
link: '/model/share'
},
{
icon: 'develop',
label: '开发',
link: '/openapi'
},
{
icon: 'git',
label: 'Git项目地址',