feat(web): 🎨 Unified color theme, AntV light/dark theme switching, Antd first-screen style loading. (#1020)

Co-authored-by: 黄振洪 <hzh01509324@alibaba-inc.com>
Co-authored-by: Aralhi <xiaoping0501@gmail.com>
This commit is contained in:
Hzh_97
2024-01-12 09:44:52 +08:00
committed by GitHub
parent 2706e27ae5
commit 99ea6ac1a4
99 changed files with 1125 additions and 1410 deletions

View File

@@ -1,333 +0,0 @@
'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

@@ -2,7 +2,6 @@ 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,
@@ -43,22 +42,21 @@ type RouteItem = {
};
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]' : ''
return `flex items-center h-12 hover:bg-[#F1F5F9] dark:hover:bg-theme-dark text-base w-full transition-colors whitespace-nowrap px-4 ${
active ? 'bg-[#F1F5F9] dark:bg-theme-dark' : ''
}`;
}
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]' : ''
return `flex items-center justify-center mx-auto rounded w-14 h-14 text-xl hover:bg-[#F1F5F9] dark:hover:bg-theme-dark transition-colors cursor-pointer ${
active ? 'bg-[#F1F5F9] dark:bg-theme-dark' : ''
}`;
}
function SideBar() {
const { chatId, scene, isMenuExpand, dialogueList, queryDialogueList, refreshDialogList, setIsMenuExpand } = useContext(ChatContext);
const { chatId, scene, isMenuExpand, dialogueList, queryDialogueList, refreshDialogList, setIsMenuExpand, mode, setMode } = useContext(ChatContext);
const { pathname, replace } = useRouter();
const { t, i18n } = useTranslation();
const { mode, setMode } = useColorScheme();
const [logo, setLogo] = useState<string>('/LOGO_1.png');
@@ -195,7 +193,7 @@ function SideBar() {
);
const copyLink = useCallback((item: IChatDialogueSchema) => {
const success = copy(`${location.origin}/chat/${item.chat_mode}/${item.conv_uid}`);
const success = copy(`${location.origin}/chat?scene=${item.chat_mode}&id=${item.conv_uid}`);
message[success ? 'success' : 'error'](success ? 'Copy success' : 'Copy failed');
}, []);
@@ -209,20 +207,17 @@ function SideBar() {
if (!isMenuExpand) {
return (
<div className="flex flex-col justify-between h-screen border-r dark:bg-[#1A1E26] animate-fade animate-duration-300">
<div className="flex flex-col justify-between h-screen bg-white dark:bg-[#232734] 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"
>
<div>
<Link href="/" className="flex items-center justify-center my-4 mx-auto w-12 h-12 bg-theme-primary 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">
<div className="flex-1 overflow-y-scroll py-4 space-y-2">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
@@ -235,7 +230,7 @@ function SideBar() {
);
})}
</div>
<div className="py-4 space-y-2 border-t">
<div className="py-4">
<Dropdown menu={{ items: dropDownRoutes }} placement="topRight">
<div className={smallMenuItemStyle()}>
<MenuOutlined />
@@ -261,20 +256,17 @@ function SideBar() {
}
return (
<div className="flex flex-col h-screen border-r dark:border-gray-700">
<div className="flex flex-col h-screen bg-white dark:bg-[#232734]">
{/* 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"
>
<Link href="/" className="flex items-center justify-center mb-4 mx-4 h-11 bg-theme-primary rounded text-white">
<PlusOutlined className="mr-2" />
<span>New Chat</span>
<span>{t('new_chat')}</span>
</Link>
{/* Chat List */}
<div className="flex-1 overflow-y-scroll py-4 px-2 border-t dark:border-gray-700">
<div className="flex-1 overflow-y-scroll">
{dialogueList?.map((item) => {
const active = item.conv_uid === chatId && item.chat_mode === scene;
@@ -305,18 +297,18 @@ function SideBar() {
})}
</div>
{/* Settings */}
<div className="py-2 border-t dark:border-gray-700">
<div className="px-2">
<div className="pt-4">
<div className="max-h-52 overflow-y-auto">
{routes.map((item) => (
<Link key={item.key} href={item.path} className={`${menuItemStyle(pathname === item.path)}`}>
<Link key={item.key} href={item.path} className={`${menuItemStyle(pathname === item.path)} overflow-hidden`}>
<>
{item.icon}
<span className="ml-2 text-sm">{item.name}</span>
<span className="ml-3 text-sm">{item.name}</span>
</>
</Link>
))}
</div>
<div className="flex items-center justify-around py-4 border-t border-dashed dark:border-gray-700">
<div className="flex items-center justify-around py-4 mt-2">
{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}>