mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 22:03:54 +00:00
app page
This commit is contained in:
@@ -30,7 +30,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
|||||||
label: '应用',
|
label: '应用',
|
||||||
icon: 'model',
|
icon: 'model',
|
||||||
link: `/app/list`,
|
link: `/app/list`,
|
||||||
activeLink: ['/app/list']
|
activeLink: ['/app/list', '/app/detail']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '知识库',
|
label: '知识库',
|
||||||
|
72
client/src/components/SlideTabs/index.tsx
Normal file
72
client/src/components/SlideTabs/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||||
|
import type { GridProps } from '@chakra-ui/react';
|
||||||
|
import MyIcon, { type IconName } from '../Icon';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export interface Props extends GridProps {
|
||||||
|
list: { id: string; label: string; icon: string }[];
|
||||||
|
activeId: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
onChange: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SlideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
|
||||||
|
const sizeMap = useMemo(() => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return {
|
||||||
|
fontSize: 'sm',
|
||||||
|
inlineP: 1
|
||||||
|
};
|
||||||
|
case 'md':
|
||||||
|
return {
|
||||||
|
fontSize: 'md',
|
||||||
|
inlineP: 2
|
||||||
|
};
|
||||||
|
case 'lg':
|
||||||
|
return {
|
||||||
|
fontSize: 'lg',
|
||||||
|
inlineP: 3
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [size]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box fontSize={sizeMap.fontSize} {...props}>
|
||||||
|
{list.map((item) => (
|
||||||
|
<Flex
|
||||||
|
key={item.id}
|
||||||
|
py={sizeMap.inlineP}
|
||||||
|
borderRadius={'md'}
|
||||||
|
px={3}
|
||||||
|
mb={2}
|
||||||
|
alignItems={'center'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'myWhite.600'
|
||||||
|
}}
|
||||||
|
{...(activeId === item.id
|
||||||
|
? {
|
||||||
|
// backgroundImage: 'linear-gradient(to right, #85b1ff 0%, #EBF7FD 100%)',
|
||||||
|
bg: ' myBlue.300',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'myBlue.700',
|
||||||
|
cursor: 'default'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
cursor: 'pointer'
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
if (activeId === item.id) return;
|
||||||
|
onChange(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MyIcon mr={2} name={item.icon as IconName} w={'14px'} />
|
||||||
|
{item.label}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(SlideTabs);
|
@@ -274,7 +274,8 @@ export const theme = extendTheme({
|
|||||||
borders: {
|
borders: {
|
||||||
sm: '1px solid #EFF0F1',
|
sm: '1px solid #EFF0F1',
|
||||||
base: '1px solid #DEE0E2',
|
base: '1px solid #DEE0E2',
|
||||||
md: '1px solid #DAE0E2'
|
md: '1px solid #DAE0E2',
|
||||||
|
lg: '1px solid #D0E0E2'
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
sm: '900px',
|
sm: '900px',
|
||||||
|
@@ -3,7 +3,7 @@ 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 { App } from '@/service/models/model';
|
import { App } from '@/service/models/app';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
|
@@ -2,7 +2,7 @@ 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 { App } from '@/service/models/model';
|
import { App } from '@/service/models/app';
|
||||||
import type { AppUpdateParams } from '@/types/app';
|
import type { AppUpdateParams } from '@/types/app';
|
||||||
import { authApp } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
|
|
||||||
|
@@ -1,15 +1,5 @@
|
|||||||
import React, { useCallback, useState, useMemo } from 'react';
|
import React, { useCallback, useState, useMemo } from 'react';
|
||||||
import {
|
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react';
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
Textarea,
|
|
||||||
Divider,
|
|
||||||
Tooltip
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -21,19 +11,10 @@ import { useSelectFile } from '@/hooks/useSelectFile';
|
|||||||
import { compressImg } from '@/utils/file';
|
import { compressImg } from '@/utils/file';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
import { useConfirm } from '@/hooks/useConfirm';
|
import { useConfirm } from '@/hooks/useConfirm';
|
||||||
import { ChatModelMap, getChatModelList } from '@/constants/model';
|
|
||||||
import { formatPrice } from '@/utils/user';
|
|
||||||
|
|
||||||
import type { AppSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import MySelect from '@/components/Select';
|
|
||||||
import MySlider from '@/components/Slider';
|
|
||||||
|
|
||||||
const systemPromptTip =
|
|
||||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。';
|
|
||||||
const limitPromptTip =
|
|
||||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
|
|
||||||
|
|
||||||
const Settings = ({ modelId }: { modelId: string }) => {
|
const Settings = ({ modelId }: { modelId: string }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -67,15 +48,6 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
() => appDetail.userId === userInfo?._id,
|
() => appDetail.userId === userInfo?._id,
|
||||||
[appDetail.userId, userInfo?._id]
|
[appDetail.userId, userInfo?._id]
|
||||||
);
|
);
|
||||||
const tokenLimit = useMemo(() => {
|
|
||||||
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
|
|
||||||
|
|
||||||
if (max < getValues('chat.maxToken')) {
|
|
||||||
setValue('chat.maxToken', max);
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}, [getValues, setValue, refresh]);
|
|
||||||
|
|
||||||
// 提交保存模型修改
|
// 提交保存模型修改
|
||||||
const saveSubmitSuccess = useCallback(
|
const saveSubmitSuccess = useCallback(
|
||||||
@@ -185,17 +157,17 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
pt={[0, 5]}
|
||||||
pb={3}
|
pb={3}
|
||||||
px={[5, '25px', '50px']}
|
px={[5, '25px', '50px']}
|
||||||
fontSize={['sm', 'lg']}
|
fontSize={['sm', 'lg']}
|
||||||
maxW={['auto', '800px']}
|
|
||||||
position={'relative'}
|
position={'relative'}
|
||||||
|
maxW={['auto', '800px']}
|
||||||
>
|
>
|
||||||
<Flex alignItems={'center'}>
|
<Box fontSize={['md', 'xl']}>基本信息</Box>
|
||||||
|
<Flex mt={5} alignItems={'center'}>
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||||
头像
|
头像
|
||||||
</Box>
|
</Box>
|
||||||
@@ -235,96 +207,6 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
|
|
||||||
<Divider mt={5} />
|
<Divider mt={5} />
|
||||||
|
|
||||||
<Flex alignItems={'center'} mt={5}>
|
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
|
||||||
对话模型
|
|
||||||
</Box>
|
|
||||||
<MySelect
|
|
||||||
width={['90%', '280px']}
|
|
||||||
value={getValues('chat.chatModel')}
|
|
||||||
list={chatModelList.map((item) => ({
|
|
||||||
value: item.chatModel,
|
|
||||||
label: `${item.name} (${formatPrice(
|
|
||||||
ChatModelMap[item.chatModel]?.price,
|
|
||||||
1000
|
|
||||||
)} 元/1k tokens)`
|
|
||||||
}))}
|
|
||||||
onchange={(val: any) => {
|
|
||||||
setValue('chat.chatModel', val);
|
|
||||||
setRefresh(!refresh);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex alignItems={'center'} my={10}>
|
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
|
||||||
温度
|
|
||||||
</Box>
|
|
||||||
<Box flex={1} ml={'10px'}>
|
|
||||||
<MySlider
|
|
||||||
markList={[
|
|
||||||
{ label: '严谨', value: 0 },
|
|
||||||
{ label: '发散', value: 10 }
|
|
||||||
]}
|
|
||||||
width={['90%', '260px']}
|
|
||||||
min={0}
|
|
||||||
max={10}
|
|
||||||
value={getValues('chat.temperature')}
|
|
||||||
onChange={(val) => {
|
|
||||||
setValue('chat.temperature', val);
|
|
||||||
setRefresh(!refresh);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<Flex alignItems={'center'} mt={12} mb={10}>
|
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
|
||||||
回复上限
|
|
||||||
</Box>
|
|
||||||
<Box flex={1} ml={'10px'}>
|
|
||||||
<MySlider
|
|
||||||
markList={[
|
|
||||||
{ label: '100', value: 100 },
|
|
||||||
{ label: `${tokenLimit}`, value: tokenLimit }
|
|
||||||
]}
|
|
||||||
width={['90%', '260px']}
|
|
||||||
min={100}
|
|
||||||
max={tokenLimit}
|
|
||||||
step={50}
|
|
||||||
value={getValues('chat.maxToken')}
|
|
||||||
onChange={(val) => {
|
|
||||||
setValue('chat.maxToken', val);
|
|
||||||
setRefresh(!refresh);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<Flex mt={10} alignItems={'flex-start'}>
|
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
|
||||||
提示词
|
|
||||||
<Tooltip label={systemPromptTip}>
|
|
||||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Textarea
|
|
||||||
rows={8}
|
|
||||||
placeholder={systemPromptTip}
|
|
||||||
{...register('chat.systemPrompt')}
|
|
||||||
></Textarea>
|
|
||||||
</Flex>
|
|
||||||
<Flex mt={5} alignItems={'flex-start'}>
|
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
|
||||||
限定词
|
|
||||||
<Tooltip label={limitPromptTip}>
|
|
||||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Textarea
|
|
||||||
rows={5}
|
|
||||||
placeholder={limitPromptTip}
|
|
||||||
{...register('chat.limitPrompt')}
|
|
||||||
></Textarea>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex mt={5} alignItems={'center'}>
|
<Flex mt={5} alignItems={'center'}>
|
||||||
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
|
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
|
||||||
<Button
|
<Button
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { useGlobalStore } from '@/store/global';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
import SlideTabs from '@/components/SlideTabs';
|
||||||
import Settings from './components/Settings';
|
import Settings from './components/Settings';
|
||||||
import { defaultApp } from '@/constants/model';
|
import { defaultApp } from '@/constants/model';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
const EditApp = dynamic(() => import('./components/edit'), {
|
const EditApp = dynamic(() => import('./components/edit'), {
|
||||||
ssr: false
|
ssr: false
|
||||||
@@ -28,8 +29,8 @@ enum TabEnum {
|
|||||||
|
|
||||||
const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const { appId } = router.query as { appId: string };
|
const { appId } = router.query as { appId: string };
|
||||||
const { isPc } = useGlobalStore();
|
|
||||||
const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore();
|
const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore();
|
||||||
|
|
||||||
const isOwner = useMemo(
|
const isOwner = useMemo(
|
||||||
@@ -50,6 +51,17 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
|||||||
[appId, router]
|
[appId, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tabList = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: '基础信息', id: TabEnum.settings, icon: 'text' },
|
||||||
|
...(isOwner ? [{ label: '编排', id: TabEnum.edit, icon: 'edit' }] : []),
|
||||||
|
{ label: '分享', id: TabEnum.share, icon: 'edit' },
|
||||||
|
{ label: 'API', id: TabEnum.API, icon: 'edit' },
|
||||||
|
{ label: '立即对话', id: 'startChat', icon: 'chat' }
|
||||||
|
],
|
||||||
|
[isOwner]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.onbeforeunload = (e) => {
|
window.onbeforeunload = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -66,51 +78,68 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
|||||||
}, [appId, loadAppDetail]);
|
}, [appId, loadAppDetail]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex flexDirection={'column'} bg={'myGray.100'} h={'100%'} p={[0, 5]}>
|
||||||
flexDirection={'column'}
|
<Box
|
||||||
h={'100%'}
|
display={['block', 'flex']}
|
||||||
maxW={'100vw'}
|
flex={1}
|
||||||
pt={4}
|
bg={'white'}
|
||||||
overflow={'overlay'}
|
borderRadius={['', '2xl']}
|
||||||
position={'relative'}
|
border={['', theme.borders.lg]}
|
||||||
bg={'white'}
|
>
|
||||||
>
|
{/* pc tab */}
|
||||||
{/* 头部 */}
|
<Box display={['none', 'block']} p={4} w={'200px'} borderRight={theme.borders.base}>
|
||||||
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
<Flex mb={4}>
|
||||||
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
<Avatar src={appDetail.avatar} w={'34px'} borderRadius={'lg'} />
|
||||||
{appDetail.name}
|
<Box ml={2} fontWeight={'bold'}>
|
||||||
|
{appDetail.name}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<SlideTabs
|
||||||
|
mx={'auto'}
|
||||||
|
mt={2}
|
||||||
|
w={'100%'}
|
||||||
|
list={tabList}
|
||||||
|
activeId={currentTab}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
if (e === 'startChat') {
|
||||||
|
router.push(`/chat?modelId=${appId}`);
|
||||||
|
} else {
|
||||||
|
setCurrentTab(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Tabs
|
{/* phone tab */}
|
||||||
mx={['auto', '0']}
|
<Box display={['block', 'none']} textAlign={'center'} px={5} py={3}>
|
||||||
mt={2}
|
<Box className="textlg" fontSize={'3xl'} fontWeight={'bold'}>
|
||||||
w={['300px', '360px']}
|
{appDetail.name}
|
||||||
list={[
|
|
||||||
{ label: '配置', id: TabEnum.settings },
|
|
||||||
...(isOwner ? [{ label: '编排', id: TabEnum.edit }] : []),
|
|
||||||
{ 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=${appId}`);
|
|
||||||
} else {
|
|
||||||
setCurrentTab(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box flex={1}>
|
|
||||||
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
|
||||||
{currentTab === TabEnum.edit && (
|
|
||||||
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
|
||||||
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
<Tabs
|
||||||
{currentTab === TabEnum.API && <API modelId={appId} />}
|
mx={'auto'}
|
||||||
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
mt={2}
|
||||||
|
w={'300px'}
|
||||||
|
list={tabList}
|
||||||
|
size={'sm'}
|
||||||
|
activeId={currentTab}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
if (e === 'startChat') {
|
||||||
|
router.push(`/chat?modelId=${appId}`);
|
||||||
|
} else {
|
||||||
|
setCurrentTab(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box flex={1}>
|
||||||
|
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
||||||
|
{currentTab === TabEnum.edit && (
|
||||||
|
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||||
|
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{currentTab === TabEnum.API && <API modelId={appId} />}
|
||||||
|
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user