refactor: Add frontend code to DB-GPT (#912)

This commit is contained in:
katakuri
2023-12-11 00:05:42 +08:00
committed by GitHub
parent b8dc9cf11e
commit 43190ca333
189 changed files with 19179 additions and 16 deletions

View File

@@ -0,0 +1,333 @@
'use client';
import React, { useState, useEffect, useMemo, useContext } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Link from 'next/link';
import { Modal } from 'antd';
import {
Box,
List,
ListItem,
ListItemButton,
ListItemDecorator,
ListItemContent,
Typography,
Button,
useColorScheme,
IconButton,
Tooltip,
} from '@mui/joy';
import Article from '@mui/icons-material/Article';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import WbSunnyIcon from '@mui/icons-material/WbSunny';
import SmsOutlinedIcon from '@mui/icons-material/SmsOutlined';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import Image from 'next/image';
import classNames from 'classnames';
import MenuIcon from '@mui/icons-material/Menu';
import DatasetIcon from '@mui/icons-material/Dataset';
import ExpandIcon from '@mui/icons-material/Expand';
import LanguageIcon from '@mui/icons-material/Language';
import ChatIcon from '@mui/icons-material/Chat';
import ModelTrainingIcon from '@mui/icons-material/ModelTraining';
import { useTranslation } from 'react-i18next';
import { ChatContext } from '@/app/chat-context';
import { DialogueListResponse } from '@/types/chat';
import { apiInterceptors, delDialogue } from '@/client/api';
const LeftSide = () => {
const pathname = usePathname();
const { t, i18n } = useTranslation();
const router = useRouter();
const [logoPath, setLogoPath] = useState('/LOGO_1.png');
const { dialogueList, chatId, queryDialogueList, refreshDialogList, isMenuExpand, setIsMenuExpand } = useContext(ChatContext);
const { mode, setMode } = useColorScheme();
const menus = useMemo(() => {
return [
{
label: t('Prompt'),
route: '/prompt',
icon: <ChatIcon fontSize="small" />,
tooltip: t('Prompt'),
active: pathname === '/prompt',
},
{
label: t('Data_Source'),
route: '/database',
icon: <DatasetIcon fontSize="small" />,
tooltip: t('Data_Source'),
active: pathname === '/database',
},
{
label: t('Knowledge_Space'),
route: '/knowledge',
icon: <Article fontSize="small" />,
tooltip: t('Knowledge_Space'),
active: pathname === '/knowledge',
},
{
label: t('model_manage'),
route: '/models',
icon: <ModelTrainingIcon fontSize="small" />,
tooltip: t('model_manage'),
active: pathname === '/models',
},
];
}, [pathname, i18n.language]);
function handleChangeTheme() {
if (mode === 'light') {
setMode('dark');
} else {
setMode('light');
}
}
const handleChangeLanguage = () => {
const language = i18n.language === 'en' ? 'zh' : 'en';
i18n.changeLanguage(language);
window.localStorage.setItem('db_gpt_lng', language);
};
useEffect(() => {
if (mode === 'light') {
setLogoPath('/LOGO_1.png');
} else {
setLogoPath('/WHITE_LOGO.png');
}
}, [mode]);
useEffect(() => {
(async () => {
await queryDialogueList();
})();
}, []);
function expandMenu() {
return (
<>
<Box className="p-2 gap-2 flex flex-row justify-between items-center">
<div className="flex items-center gap-3">
<Link href={'/'}>
<Image src={logoPath} alt="DB-GPT" width={633} height={157} className="w-full max-w-full" />
</Link>
</div>
</Box>
<Box className="p-2">
<Link href={`/`}>
<Button
color="primary"
className="w-full bg-gradient-to-r from-[#31afff] to-[#1677ff] dark:bg-gradient-to-r dark:from-[#6a6a6a] dark:to-[#80868f]"
style={{
color: '#fff',
}}
>
+ New Chat
</Button>
</Link>
</Box>
<Box className="p-2 hidden xs:block sm:inline-block max-h-full overflow-auto">
<List size="sm" sx={{ '--ListItem-radius': '8px' }}>
<ListItem nested>
<List
size="sm"
aria-labelledby="nav-list-browse"
sx={{
'& .JoyListItemButton-root': { p: '8px' },
gap: '4px',
}}
>
{(dialogueList || []).map((dialogue: DialogueListResponse[0]) => {
const isSelect = (pathname === `/chat` || pathname === '/chat/') && chatId === dialogue.conv_uid;
return (
<ListItem key={dialogue.conv_uid}>
<ListItemButton
selected={isSelect}
variant={isSelect ? 'soft' : 'plain'}
sx={{
'&:hover .del-btn': {
visibility: 'visible',
},
}}
>
<ListItemContent>
<Link href={`/chat?id=${dialogue.conv_uid}&scene=${dialogue?.chat_mode}`} className="flex items-center justify-between">
<Typography fontSize={14} noWrap={true}>
<SmsOutlinedIcon style={{ marginRight: '0.5rem' }} />
{dialogue?.user_name || dialogue?.user_input || 'undefined'}
</Typography>
<IconButton
color="neutral"
variant="plain"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
Modal.confirm({
title: 'Delete Chat',
content: 'Are you sure delete this chat?',
width: '276px',
centered: true,
async onOk() {
await apiInterceptors(delDialogue(dialogue.conv_uid));
await refreshDialogList();
if (pathname === `/chat` && chatId === dialogue.conv_uid) {
router.push('/');
}
},
});
}}
className="del-btn invisible"
>
<DeleteOutlineOutlinedIcon />
</IconButton>
</Link>
</ListItemContent>
</ListItemButton>
</ListItem>
);
})}
</List>
</ListItem>
</List>
</Box>
<div className="flex flex-col justify-end flex-1">
<Box className="p-2 pt-3 pb-6 border-t border-divider xs:block sticky bottom-0 z-100">
<List size="sm" sx={{ '--ListItem-radius': '8px' }}>
<ListItem nested>
<List
size="sm"
aria-labelledby="nav-list-browse"
sx={{
'& .JoyListItemButton-root': { p: '8px' },
}}
>
{menus.map((menu) => (
<Link key={menu.route} href={menu.route}>
<ListItem>
<ListItemButton
color="neutral"
sx={{ marginBottom: 1, height: '2.5rem' }}
selected={menu.active}
variant={menu.active ? 'soft' : 'plain'}
>
<ListItemDecorator
sx={{
color: menu.active ? 'inherit' : 'neutral.500',
}}
>
{menu.icon}
</ListItemDecorator>
<ListItemContent>{menu.label}</ListItemContent>
</ListItemButton>
</ListItem>
</Link>
))}
</List>
</ListItem>
<ListItem>
<ListItemButton className="h-10" onClick={handleChangeTheme}>
<Tooltip title={t('Theme')}>
<ListItemDecorator>{mode === 'dark' ? <DarkModeIcon fontSize="small" /> : <WbSunnyIcon fontSize="small" />}</ListItemDecorator>
</Tooltip>
<ListItemContent>{t('Theme')}</ListItemContent>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton className="h-10" onClick={handleChangeLanguage}>
<Tooltip title={t('language')}>
<ListItemDecorator className="text-2xl">
<LanguageIcon fontSize="small" />
</ListItemDecorator>
</Tooltip>
<ListItemContent>{t('language')}</ListItemContent>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton
className="h-10"
onClick={() => {
setIsMenuExpand(false);
}}
>
<Tooltip title={t('Close_Sidebar')}>
<ListItemDecorator className="text-2xl">
<ExpandIcon className="transform rotate-90" fontSize="small" />
</ListItemDecorator>
</Tooltip>
<ListItemContent>{t('Close_Sidebar')}</ListItemContent>
</ListItemButton>
</ListItem>
</List>
</Box>
</div>
</>
);
}
function notExpandMenu() {
return (
<Box className="h-full py-6 flex flex-col justify-between">
<Box className="flex justify-center items-center">
<Tooltip title="Menu">
<MenuIcon
className="cursor-pointer text-2xl"
onClick={() => {
setIsMenuExpand(true);
}}
/>
</Tooltip>
</Box>
<Box className="flex flex-col gap-4 justify-center items-center">
{menus.map((menu, index) => (
<div className="flex justify-center text-2xl cursor-pointer" key={`menu_${index}`}>
<Tooltip title={menu.tooltip}>{menu.icon}</Tooltip>
</div>
))}
<ListItem>
<ListItemButton onClick={handleChangeTheme}>
<Tooltip title={t('Theme')}>
<ListItemDecorator className="text-2xl">
{mode === 'dark' ? <DarkModeIcon fontSize="small" /> : <WbSunnyIcon fontSize="small" />}
</ListItemDecorator>
</Tooltip>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton onClick={handleChangeLanguage}>
<Tooltip title={t('language')}>
<ListItemDecorator className="text-2xl">
<LanguageIcon fontSize="small" />
</ListItemDecorator>
</Tooltip>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton
onClick={() => {
setIsMenuExpand(true);
}}
>
<Tooltip title={t('Open_Sidebar')}>
<ListItemDecorator className="text-2xl">
<ExpandIcon className="transform rotate-90" fontSize="small" />
</ListItemDecorator>
</Tooltip>
</ListItemButton>
</ListItem>
</Box>
</Box>
);
}
return (
<>
<nav className={classNames('grid max-h-screen h-full max-md:hidden')}>
<Box className="flex flex-col border-r border-divider max-h-screen sticky left-0 top-0 overflow-hidden">
{isMenuExpand ? expandMenu() : notExpandMenu()}
</Box>
</nav>
</>
);
};
export default LeftSide;

View File

@@ -0,0 +1,333 @@
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, delDialogue } from '@/client/api';
import { STORAGE_LANG_KEY, STORAGE_THEME_KEY } from '@/utils';
import { DarkSvg, SunnySvg, ModelSvg } from '@/components/icons';
import { useColorScheme } from '@mui/joy';
import { IChatDialogueSchema } from '@/types/chat';
import Icon, {
ConsoleSqlOutlined,
PartitionOutlined,
DeleteOutlined,
MessageOutlined,
GlobalOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
PlusOutlined,
ShareAltOutlined,
MenuOutlined,
SettingOutlined,
BuildOutlined,
} from '@ant-design/icons';
import { Modal, message, Tooltip, Dropdown } from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems';
import copy from 'copy-to-clipboard';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
type SettingItem = {
key: string;
name: string;
icon: ReactNode;
noDropdownItem?: boolean;
onClick: () => void;
};
type RouteItem = {
key: string;
name: string;
icon: ReactNode;
path: string;
};
function menuItemStyle(active?: boolean) {
return `flex items-center px-2 h-8 hover:bg-slate-100 dark:hover:bg-[#353539] text-base w-full my-2 rounded transition-colors whitespace-nowrap ${
active ? 'bg-slate-100 dark:bg-[#353539]' : ''
}`;
}
function smallMenuItemStyle(active?: boolean) {
return `flex items-center justify-center mx-auto w-12 h-12 text-xl rounded hover:bg-slate-100 dark:hover:bg-[#353539] cursor-pointer ${
active ? 'bg-slate-100 dark:bg-[#353539]' : ''
}`;
}
function SideBar() {
const { chatId, scene, isMenuExpand, dialogueList, queryDialogueList, refreshDialogList, setIsMenuExpand } = useContext(ChatContext);
const { pathname, replace } = useRouter();
const { t, i18n } = useTranslation();
const { mode, setMode } = useColorScheme();
const [logo, setLogo] = useState<string>('/LOGO_1.png');
const routes = useMemo(() => {
const items: RouteItem[] = [
{
key: 'prompt',
name: t('Prompt'),
icon: <MessageOutlined />,
path: '/prompt',
},
{
key: 'database',
name: t('Database'),
icon: <ConsoleSqlOutlined />,
path: '/database',
},
{
key: 'knowledge',
name: t('Knowledge_Space'),
icon: <PartitionOutlined />,
path: '/knowledge',
},
{
key: 'models',
name: t('model_manage'),
path: '/models',
icon: <Icon component={ModelSvg} />,
},
{
key: 'agent',
name: t('Plugins'),
path: '/agent',
icon: <BuildOutlined />,
},
];
return items;
}, [i18n.language]);
const handleToggleMenu = () => {
setIsMenuExpand(!isMenuExpand);
};
const handleToggleTheme = useCallback(() => {
const theme = mode === 'light' ? 'dark' : 'light';
setMode(theme);
localStorage.setItem(STORAGE_THEME_KEY, theme);
}, [mode]);
const handleChangeLang = useCallback(() => {
const language = i18n.language === 'en' ? 'zh' : 'en';
i18n.changeLanguage(language);
localStorage.setItem(STORAGE_LANG_KEY, language);
}, [i18n.language, i18n.changeLanguage]);
const settings = useMemo(() => {
const items: SettingItem[] = [
{
key: 'theme',
name: t('Theme'),
icon: mode === 'dark' ? <Icon component={DarkSvg} /> : <Icon component={SunnySvg} />,
onClick: handleToggleTheme,
},
{
key: 'language',
name: t('language'),
icon: <GlobalOutlined />,
onClick: handleChangeLang,
},
{
key: 'fold',
name: t(isMenuExpand ? 'Close_Sidebar' : 'Show_Sidebar'),
icon: isMenuExpand ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />,
onClick: handleToggleMenu,
noDropdownItem: true,
},
];
return items;
}, [mode, handleChangeLang, handleToggleMenu, handleChangeLang]);
const dropDownRoutes: ItemType[] = useMemo(() => {
return routes.map<ItemType>((item) => ({
key: item.key,
label: (
<Link href={item.path} className="text-base">
{item.icon}
<span className="ml-2 text-sm">{item.name}</span>
</Link>
),
}));
}, [routes]);
const dropDownSettings: ItemType[] = useMemo(() => {
return settings
.filter((item) => !item.noDropdownItem)
.map<ItemType>((item) => ({
key: item.key,
label: (
<div className="text-base" onClick={item.onClick}>
{item.icon}
<span className="ml-2 text-sm">{item.name}</span>
</div>
),
}));
}, [settings]);
const handleDelChat = useCallback(
(dialogue: IChatDialogueSchema) => {
Modal.confirm({
title: 'Delete Chat',
content: 'Are you sure delete this chat?',
width: '276px',
centered: true,
onOk() {
return new Promise<void>(async (resolve, reject) => {
try {
const [err] = await apiInterceptors(delDialogue(dialogue.conv_uid));
if (err) {
reject();
return;
}
message.success('success');
refreshDialogList();
dialogue.chat_mode === scene && dialogue.conv_uid === chatId && replace('/');
resolve();
} catch (e) {
reject();
}
});
},
});
},
[refreshDialogList],
);
const copyLink = useCallback((item: IChatDialogueSchema) => {
const success = copy(`${location.origin}/chat/${item.chat_mode}/${item.conv_uid}`);
message[success ? 'success' : 'error'](success ? 'Copy success' : 'Copy failed');
}, []);
useEffect(() => {
queryDialogueList();
}, []);
useEffect(() => {
setLogo(mode === 'dark' ? '/WHITE_LOGO.png' : '/LOGO_1.png');
}, [mode]);
if (!isMenuExpand) {
return (
<div className="flex flex-col justify-between h-screen border-r dark:bg-[#1A1E26] animate-fade animate-duration-300">
<Link href="/" className="px-2 py-3">
<Image src="/LOGO_SMALL.png" alt="DB-GPT" width={63} height={46} className="w-[63px] h-[46px]" />
</Link>
<div className="border-t border-dashed">
<Link
href="/"
className="flex items-center justify-center my-4 mx-auto w-12 h-12 bg-gradient-to-r from-[#31afff] to-[#1677ff] dark:bg-gradient-to-r dark:from-[#6a6a6a] dark:to-[#80868f] border-none rounded-full text-white"
>
<PlusOutlined className="text-lg" />
</Link>
</div>
{/* Chat List */}
<div className="flex-1 overflow-y-scroll py-4 border-t border-dashed space-y-2">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
return (
<Tooltip key={item.conv_uid} title={item.user_name || item.user_input} placement="right">
<Link href={`/chat?scene=${item.chat_mode}&id=${item.conv_uid}`} className={smallMenuItemStyle(active)}>
<MessageOutlined />
</Link>
</Tooltip>
);
})}
</div>
<div className="py-4 space-y-2 border-t">
<Dropdown menu={{ items: dropDownRoutes }} placement="topRight">
<div className={smallMenuItemStyle()}>
<MenuOutlined />
</div>
</Dropdown>
<Dropdown menu={{ items: dropDownSettings }} placement="topRight">
<div className={smallMenuItemStyle()}>
<SettingOutlined />
</div>
</Dropdown>
{settings
.filter((item) => item.noDropdownItem)
.map((item) => (
<Tooltip key={item.key} title={item.name} placement="right">
<div className={smallMenuItemStyle()} onClick={item.onClick}>
{item.icon}
</div>
</Tooltip>
))}
</div>
</div>
);
}
return (
<div className="flex flex-col h-screen border-r dark:border-gray-700">
{/* LOGO */}
<Link href="/" className="p-2">
<Image src={logo} alt="DB-GPT" width={239} height={60} className="w-full h-full" />
</Link>
<Link
href="/"
className="flex items-center justify-center mb-4 mx-4 h-11 bg-gradient-to-r from-[#31afff] to-[#1677ff] dark:bg-gradient-to-r dark:from-[#6a6a6a] dark:to-[#80868f] border-none rounded text-white"
>
<PlusOutlined className="mr-2" />
<span>New Chat</span>
</Link>
{/* Chat List */}
<div className="flex-1 overflow-y-scroll py-4 px-2 border-t dark:border-gray-700">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
return (
<Link key={item.conv_uid} href={`/chat?scene=${item.chat_mode}&id=${item.conv_uid}`} className={`group/item ${menuItemStyle(active)}`}>
<MessageOutlined className="text-base" />
<div className="flex-1 line-clamp-1 mx-2 text-sm">{item.user_name || item.user_input}</div>
<div
className="group-hover/item:opacity-100 cursor-pointer opacity-0 mr-1"
onClick={(e) => {
e.preventDefault();
copyLink(item);
}}
>
<ShareAltOutlined />
</div>
<div
className="group-hover/item:opacity-100 cursor-pointer opacity-0"
onClick={(e) => {
e.preventDefault();
handleDelChat(item);
}}
>
<DeleteOutlined />
</div>
</Link>
);
})}
</div>
{/* Settings */}
<div className="py-2 border-t dark:border-gray-700">
<div className="px-2">
{routes.map((item) => (
<Link key={item.key} href={item.path} className={`${menuItemStyle(pathname === item.path)}`}>
<>
{item.icon}
<span className="ml-2 text-sm">{item.name}</span>
</>
</Link>
))}
</div>
<div className="flex items-center justify-around py-4 border-t border-dashed dark:border-gray-700">
{settings.map((item) => (
<Tooltip key={item.key} title={item.name}>
<div className="flex-1 flex items-center justify-center cursor-pointer text-xl" onClick={item.onClick}>
{item.icon}
</div>
</Tooltip>
))}
</div>
</div>
</div>
);
}
export default SideBar;

View File

@@ -0,0 +1,61 @@
import Router from 'next/router';
import NProgress from 'nprogress';
let timer: any;
let state: any;
let activeRequests = 0;
const delay = 250;
function load() {
if (state === 'loading') {
return;
}
state = 'loading';
timer = setTimeout(function () {
NProgress.start();
}, delay); // only show progress bar if it takes longer than the delay
}
function stop() {
if (activeRequests > 0) {
return;
}
state = 'stop';
clearTimeout(timer);
NProgress.done();
}
Router.events.on('routeChangeStart', load);
Router.events.on('routeChangeComplete', stop);
Router.events.on('routeChangeError', stop);
if (typeof window !== 'undefined' && typeof window?.fetch === 'function') {
const originalFetch = window.fetch;
window.fetch = async function (...args) {
if (activeRequests === 0) {
load();
}
activeRequests++;
try {
const response = await originalFetch(...args);
return response;
} catch (error) {
return Promise.reject(error);
} finally {
activeRequests -= 1;
if (activeRequests === 0) {
stop();
}
}
};
}
export default function TopProgressBar() {
return null;
}