feat: chat ui

This commit is contained in:
archer 2023-07-26 11:01:25 +08:00
parent 2b993b926a
commit 248be38939
No known key found for this signature in database
GPG Key ID: 569A5660D2379E28
19 changed files with 153 additions and 54 deletions

View File

@ -2,7 +2,7 @@
"FeConfig": { "FeConfig": {
"show_emptyChat": true, "show_emptyChat": true,
"show_register": true, "show_register": true,
"show_appStore": true, "show_appStore": false,
"show_userDetail": true, "show_userDetail": true,
"show_git": true, "show_git": true,
"systemTitle": "FastAI", "systemTitle": "FastAI",
@ -35,6 +35,14 @@
"maxTemperature": 1.2, "maxTemperature": 1.2,
"price": 3 "price": 3
}, },
{
"model": "ERNIE-Bot",
"name": "文心一言",
"contextMaxToken": 3000,
"quoteMaxToken": 1500,
"maxTemperature": 1,
"price": 1.2
},
{ {
"model": "gpt-4", "model": "gpt-4",
"name": "FastAI-Plus", "name": "FastAI-Plus",

View File

@ -1,7 +1,16 @@
{ {
"Cancel": "No",
"Confirm": "Yes",
"Warning": "Warning",
"app": { "app": {
"App Detail": "App Detail",
"My Apps": "My Apps" "My Apps": "My Apps"
}, },
"chat": {
"Confirm to clear history": "Confirm to clear history?",
"New Chat": "New Chat",
"You need to a chat app": "You need to a chat app"
},
"home": { "home": {
"Quickly build AI question and answer library": "Quickly build AI question and answer library", "Quickly build AI question and answer library": "Quickly build AI question and answer library",
"Start Now": "Start Now", "Start Now": "Start Now",

View File

@ -1,7 +1,16 @@
{ {
"Cancel": "取消",
"Confirm": "确认",
"Warning": "提示",
"app": { "app": {
"App Detail": "应用详情",
"My Apps": "我的应用" "My Apps": "我的应用"
}, },
"chat": {
"Confirm to clear history": "确认清空该应用的聊天记录?",
"New Chat": "新对话",
"You need to a chat app": "你需要创建一个应用"
},
"home": { "home": {
"Quickly build AI question and answer library": "快速搭建 AI 问答系统", "Quickly build AI question and answer library": "快速搭建 AI 问答系统",
"Start Now": "立即开始", "Start Now": "立即开始",

View File

@ -23,12 +23,10 @@ export const getChatHistory = (data: RequestPaging & { appId?: string }) =>
* *
*/ */
export const delChatHistoryById = (chatId: string) => DELETE(`/chat/removeHistory`, { chatId }); export const delChatHistoryById = (chatId: string) => DELETE(`/chat/removeHistory`, { chatId });
/** /**
* get history quotes * clear all history by appid
*/ */
export const getHistoryQuote = (params: { chatId: string; contentId: string }) => export const clearChatHistoryByAppId = (appId: string) => DELETE(`/chat/removeHistory`, { appId });
GET<(QuoteItemType & { _id: string })[]>(`/chat/history/getHistoryQuote`, params);
/** /**
* update history quote status * update history quote status

View File

@ -37,7 +37,7 @@ const map = {
minus: require('./icons/minus.svg').default, minus: require('./icons/minus.svg').default,
chatLight: require('./icons/light/chat.svg').default, chatLight: require('./icons/light/chat.svg').default,
chatFill: require('./icons/fill/chat.svg').default, chatFill: require('./icons/fill/chat.svg').default,
clearLight: require('./icons/light/clear.svg').default, clear: require('./icons/light/clear.svg').default,
apiLight: require('./icons/light/appApi.svg').default, apiLight: require('./icons/light/appApi.svg').default,
overviewLight: require('./icons/light/overview.svg').default, overviewLight: require('./icons/light/overview.svg').default,
settingLight: require('./icons/light/setting.svg').default, settingLight: require('./icons/light/setting.svg').default,

View File

@ -15,9 +15,9 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
color: 'myBlue.700' color: 'myBlue.700'
}, },
green: { green: {
borderColor: '#54cd19', borderColor: '#67c13b',
bg: '#f2fcf2', bg: '#f8fff8',
color: '#54cd19' color: '#67c13b'
}, },
gray: { gray: {
borderColor: '#979797', borderColor: '#979797',

View File

@ -9,8 +9,11 @@ import {
useDisclosure, useDisclosure,
Button Button
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
export const useConfirm = ({ title = 'Warning', content }: { title?: string; content: string }) => {
const { t } = useTranslation();
export const useConfirm = ({ title = '提示', content }: { title?: string; content: string }) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef(null); const cancelRef = useRef(null);
const confirmCb = useRef<any>(); const confirmCb = useRef<any>();
@ -32,7 +35,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent maxW={'min(90vw,400px)'}> <AlertDialogContent maxW={'min(90vw,400px)'}>
<AlertDialogHeader fontSize="lg" fontWeight="bold"> <AlertDialogHeader fontSize="lg" fontWeight="bold">
{title} {t(title)}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody> <AlertDialogBody>{content}</AlertDialogBody>
@ -45,7 +48,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
typeof cancelCb.current === 'function' && cancelCb.current(); typeof cancelCb.current === 'function' && cancelCb.current();
}} }}
> >
{t('Cancel')}
</Button> </Button>
<Button <Button
ml={4} ml={4}
@ -54,7 +57,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
typeof confirmCb.current === 'function' && confirmCb.current(); typeof confirmCb.current === 'function' && confirmCb.current();
}} }}
> >
{t('Confirm')}
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View File

@ -3,18 +3,31 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
/* 获取历史记录 */ type Props = {
chatId?: string;
appId?: string;
};
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId } = req.query; const { chatId, appId } = req.query as Props;
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
await connectToDatabase(); await connectToDatabase();
await Chat.findOneAndRemove({ if (chatId) {
chatId, await Chat.findOneAndRemove({
userId chatId,
}); userId
});
}
if (appId) {
await Chat.deleteMany({
appId,
userId
});
}
jsonRes(res); jsonRes(res);
} catch (err) { } catch (err) {

View File

@ -96,7 +96,7 @@ const ChatTest = (
<IconButton <IconButton
className="chat" className="chat"
size={'sm'} size={'sm'}
icon={<MyIcon name={'clearLight'} w={'14px'} />} icon={<MyIcon name={'clear'} w={'14px'} />}
variant={'base'} variant={'base'}
borderRadius={'md'} borderRadius={'md'}
aria-label={'delete'} aria-label={'delete'}

View File

@ -596,7 +596,7 @@ const ChatTest = ({ appId }: { appId: string }) => {
<IconButton <IconButton
className="chat" className="chat"
size={'sm'} size={'sm'}
icon={<MyIcon name={'clearLight'} w={'14px'} />} icon={<MyIcon name={'clear'} w={'14px'} />}
variant={'base'} variant={'base'}
borderRadius={'md'} borderRadius={'md'}
aria-label={'delete'} aria-label={'delete'}

View File

@ -33,7 +33,7 @@ const MyApps = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const router = useRouter(); const router = useRouter();
const { myApps, loadMyModels } = useUserStore(); const { myApps, loadMyApps } = useUserStore();
const { openConfirm, ConfirmChild } = useConfirm({ const { openConfirm, ConfirmChild } = useConfirm({
title: '删除提示', title: '删除提示',
content: '确认删除该应用所有信息?' content: '确认删除该应用所有信息?'
@ -53,7 +53,7 @@ const MyApps = () => {
title: '删除成功', title: '删除成功',
status: 'success' status: 'success'
}); });
loadMyModels(); loadMyApps();
} catch (err: any) { } catch (err: any) {
toast({ toast({
title: err?.message || '删除失败', title: err?.message || '删除失败',
@ -61,11 +61,11 @@ const MyApps = () => {
}); });
} }
}, },
[toast, loadMyModels] [toast, loadMyApps]
); );
/* 加载模型 */ /* 加载模型 */
useQuery(['loadModels'], loadMyModels, { useQuery(['loadModels'], loadMyApps, {
refetchOnMount: true refetchOnMount: true
}); });
@ -166,7 +166,7 @@ const MyApps = () => {
))} ))}
</Grid> </Grid>
<ConfirmChild /> <ConfirmChild />
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} onSuccess={loadMyModels} />} {isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} onSuccess={loadMyApps} />}
</PageContainer> </PageContainer>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import { import {
Box, Box,
Button, Button,
@ -16,6 +16,8 @@ import { useRouter } from 'next/router';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip'; import MyTooltip from '@/components/MyTooltip';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import { useTranslation } from 'react-i18next';
import { useConfirm } from '@/hooks/useConfirm';
type HistoryItemType = { type HistoryItemType = {
id: string; id: string;
@ -32,6 +34,7 @@ const ChatHistorySlider = ({
activeChatId, activeChatId,
onChangeChat, onChangeChat,
onDelHistory, onDelHistory,
onClearHistory,
onSetHistoryTop, onSetHistoryTop,
onSetCustomTitle onSetCustomTitle
}: { }: {
@ -42,17 +45,22 @@ const ChatHistorySlider = ({
activeChatId: string; activeChatId: string;
onChangeChat: (chatId?: string) => void; onChangeChat: (chatId?: string) => void;
onDelHistory: (chatId: string) => void; onDelHistory: (chatId: string) => void;
onClearHistory: () => void;
onSetHistoryTop?: (e: { chatId: string; top: boolean }) => void; onSetHistoryTop?: (e: { chatId: string; top: boolean }) => void;
onSetCustomTitle?: (e: { chatId: string; title: string }) => void; onSetCustomTitle?: (e: { chatId: string; title: string }) => void;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
// custom title edit // custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({ const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
title: '自定义历史记录标题', title: '自定义历史记录标题',
placeholder: '如果设置为空,会自动跟随聊天记录。' placeholder: '如果设置为空,会自动跟随聊天记录。'
}); });
const { openConfirm, ConfirmChild } = useConfirm({
content: t('chat.Confirm to clear history')
});
const concatHistory = useMemo<HistoryItemType[]>( const concatHistory = useMemo<HistoryItemType[]>(
() => (!activeChatId ? [{ id: activeChatId, title: '新对话' }].concat(history) : history), () => (!activeChatId ? [{ id: activeChatId, title: '新对话' }].concat(history) : history),
@ -70,7 +78,7 @@ const ChatHistorySlider = ({
whiteSpace={'nowrap'} whiteSpace={'nowrap'}
> >
{isPc && ( {isPc && (
<MyTooltip label={appId ? '应用详情' : ''} offset={[0, 0]}> <MyTooltip label={appId ? t('app.App Detail') : ''} offset={[0, 0]}>
<Flex <Flex
pt={5} pt={5}
pb={2} pb={2}
@ -92,11 +100,11 @@ const ChatHistorySlider = ({
</Flex> </Flex>
</MyTooltip> </MyTooltip>
)} )}
{/* 新对话 */} {/* btn */}
<Box w={'100%'} px={[2, 5]} h={'36px'} my={5}> <Flex w={'100%'} px={[2, 5]} h={'36px'} my={5}>
<Button <Button
variant={'base'} variant={'base'}
w={'100%'} flex={1}
h={'100%'} h={'100%'}
color={'myBlue.700'} color={'myBlue.700'}
borderRadius={'xl'} borderRadius={'xl'}
@ -104,9 +112,20 @@ const ChatHistorySlider = ({
overflow={'hidden'} overflow={'hidden'}
onClick={() => onChangeChat()} onClick={() => onChangeChat()}
> >
{t('chat.New Chat')}
</Button> </Button>
</Box>
<IconButton
ml={3}
h={'100%'}
variant={'base'}
aria-label={''}
borderRadius={'xl'}
onClick={openConfirm(onClearHistory)}
>
<MyIcon name={'clear'} w={'16px'} />
</IconButton>
</Flex>
{/* chat history */} {/* chat history */}
<Box flex={'1 0 0'} h={0} px={[2, 5]} overflow={'overlay'}> <Box flex={'1 0 0'} h={0} px={[2, 5]} overflow={'overlay'}>
@ -230,6 +249,7 @@ const ChatHistorySlider = ({
</Flex> </Flex>
)} )}
<EditTitleModal /> <EditTitleModal />
<ConfirmChild />
</Flex> </Flex>
); );
}; };

View File

@ -8,9 +8,9 @@ import Avatar from '@/components/Avatar';
const SliderApps = ({ appId }: { appId: string }) => { const SliderApps = ({ appId }: { appId: string }) => {
const router = useRouter(); const router = useRouter();
const { myApps, loadMyModels } = useUserStore(); const { myApps, loadMyApps } = useUserStore();
useQuery(['loadModels'], loadMyModels); useQuery(['loadModels'], loadMyApps);
return ( return (
<> <>

View File

@ -19,6 +19,7 @@ import { useToast } from '@/hooks/useToast';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12); const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
import type { ChatHistoryItemType } from '@/types/chat'; import type { ChatHistoryItemType } from '@/types/chat';
import { useTranslation } from 'react-i18next';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox'; import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import PageContainer from '@/components/PageContainer'; import PageContainer from '@/components/PageContainer';
@ -33,6 +34,7 @@ import { serviceSideProps } from '@/utils/i18n';
const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => { const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
const router = useRouter(); const router = useRouter();
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
@ -47,10 +49,11 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
loadHistory, loadHistory,
updateHistory, updateHistory,
delHistory, delHistory,
clearHistory,
chatData, chatData,
setChatData setChatData
} = useChatStore(); } = useChatStore();
const { myApps, userInfo } = useUserStore(); const { myApps, loadMyApps, userInfo } = useUserStore();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
@ -200,6 +203,26 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
} }
}); });
} }
if (!appId) {
(async () => {
const apps = await loadMyApps();
if (apps.length === 0) {
toast({
status: 'error',
title: t('chat.You need to a chat app')
});
router.replace('/app/list');
} else {
router.replace({
query: {
appId: apps[0]._id,
chatId: lastChatId
}
});
}
})();
return;
}
// store id // store id
appId && setLastChatAppId(appId); appId && setLastChatAppId(appId);
@ -210,15 +233,11 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
return null; return null;
} }
if (appId) { return loadChatInfo({
return loadChatInfo({ appId,
appId, chatId,
chatId, loading: appId !== chatData.appId
loading: appId !== chatData.appId });
});
}
return null;
}); });
useQuery(['loadHistory', appId], () => (appId ? loadHistory({ appId }) : null)); useQuery(['loadHistory', appId], () => (appId ? loadHistory({ appId }) : null));
@ -268,6 +287,14 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
} }
}} }}
onDelHistory={delHistory} onDelHistory={delHistory}
onClearHistory={() => {
clearHistory(appId);
router.replace({
query: {
appId
}
});
}}
onSetHistoryTop={async (e) => { onSetHistoryTop={async (e) => {
try { try {
await putChatHistory(e); await putChatHistory(e);

View File

@ -99,8 +99,6 @@ const ShareChat = ({ shareId, chatId }: { shareId: string; chatId: string }) =>
const loadAppInfo = useCallback( const loadAppInfo = useCallback(
async (shareId: string, chatId: string) => { async (shareId: string, chatId: string) => {
console.log(shareId, chatId);
if (!shareId) return null; if (!shareId) return null;
const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory; const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory;
@ -183,6 +181,14 @@ const ShareChat = ({ shareId, chatId }: { shareId: string; chatId: string }) =>
} }
}} }}
onDelHistory={delOneShareHistoryByChatId} onDelHistory={delOneShareHistoryByChatId}
onClearHistory={() => {
delManyShareChatHistoryByShareId(shareId);
router.replace({
query: {
shareId
}
});
}}
/> />
)} )}

View File

@ -72,16 +72,16 @@ export const dispatchChatCompletion = async (props: Record<string, any>): Promis
maxToken, maxToken,
filterMessages filterMessages
}); });
// console.log(messages);
// FastGpt temperature range: 1~10 // FastGpt temperature range: 1~10
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2); temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
temperature = Math.max(temperature, 0.01);
const chatAPI = getOpenAIApi(); const chatAPI = getOpenAIApi();
const response = await chatAPI.createChatCompletion( const response = await chatAPI.createChatCompletion(
{ {
model, model,
temperature: Number(temperature || 0), temperature,
max_tokens, max_tokens,
messages, messages,
// frequency_penalty: 0.5, // 越大,重复内容越少 // frequency_penalty: 0.5, // 越大,重复内容越少

View File

@ -4,12 +4,13 @@ import { immer } from 'zustand/middleware/immer';
import { ChatHistoryItemType } from '@/types/chat'; import { ChatHistoryItemType } from '@/types/chat';
import type { InitChatResponse } from '@/api/response/chat'; import type { InitChatResponse } from '@/api/response/chat';
import { delChatHistoryById, getChatHistory } from '@/api/chat'; import { delChatHistoryById, getChatHistory, clearChatHistoryByAppId } from '@/api/chat';
type State = { type State = {
history: ChatHistoryItemType[]; history: ChatHistoryItemType[];
loadHistory: (data: { appId?: string }) => Promise<null>; loadHistory: (data: { appId?: string }) => Promise<null>;
delHistory(history: string): Promise<void>; delHistory(history: string): Promise<void>;
clearHistory(appId: string): Promise<void>;
updateHistory: (history: ChatHistoryItemType) => void; updateHistory: (history: ChatHistoryItemType) => void;
chatData: InitChatResponse; chatData: InitChatResponse;
setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void; setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
@ -69,6 +70,12 @@ export const useChatStore = create<State>()(
}); });
await delChatHistoryById(chatId); await delChatHistoryById(chatId);
}, },
async clearHistory(appId) {
set((state) => {
state.history = [];
});
await clearChatHistoryByAppId(appId);
},
updateHistory(history) { updateHistory(history) {
const index = get().history.findIndex((item) => item.chatId === history.chatId); const index = get().history.findIndex((item) => item.chatId === history.chatId);
set((state) => { set((state) => {

View File

@ -29,7 +29,6 @@ export const clientInitData = async (): Promise<InitDateResponse> => {
beianText = res.systemEnv?.beianText; beianText = res.systemEnv?.beianText;
googleClientVerKey = res.systemEnv?.googleClientVerKey; googleClientVerKey = res.systemEnv?.googleClientVerKey;
baiduTongji = res.systemEnv?.baiduTongji; baiduTongji = res.systemEnv?.baiduTongji;
console.log(res.feConfigs);
return res; return res;
} catch (error) { } catch (error) {

View File

@ -19,7 +19,7 @@ type State = {
updateUserInfo: (user: UserUpdateParams) => void; updateUserInfo: (user: UserUpdateParams) => void;
myApps: AppListItemType[]; myApps: AppListItemType[];
myCollectionApps: AppListItemType[]; myCollectionApps: AppListItemType[];
loadMyModels: () => Promise<null>; loadMyApps: () => Promise<AppListItemType[]>;
appDetail: AppSchema; appDetail: AppSchema;
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>; loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>; updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
@ -63,12 +63,12 @@ export const useUserStore = create<State>()(
}, },
myApps: [], myApps: [],
myCollectionApps: [], myCollectionApps: [],
async loadMyModels() { async loadMyApps() {
const res = await getMyModels(); const res = await getMyModels();
set((state) => { set((state) => {
state.myApps = res; state.myApps = res;
}); });
return null; return res;
}, },
appDetail: defaultApp, appDetail: defaultApp,
async loadAppDetail(id: string, init = false) { async loadAppDetail(id: string, init = false) {