### What problem does this PR solve? Feat: Add dataset sidebar #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.14.1
| @@ -4,6 +4,8 @@ export enum KnowledgeRouteKey { | |||
| Configuration = 'configuration', | |||
| } | |||
| export const DatasetBaseKey = 'dataset'; | |||
| export enum RunningStatus { | |||
| UNSTART = '0', // need to run | |||
| RUNNING = '1', // need to cancel | |||
| @@ -0,0 +1,113 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Container } from '@/components/ui/container'; | |||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented '; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useNavigateWithFromState } from '@/hooks/route-hook'; | |||
| import { | |||
| ChevronDown, | |||
| Cpu, | |||
| Github, | |||
| Library, | |||
| MessageSquareText, | |||
| Search, | |||
| Star, | |||
| Zap, | |||
| } from 'lucide-react'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { useLocation } from 'umi'; | |||
| export function Header() { | |||
| const { t } = useTranslate('header'); | |||
| const { pathname } = useLocation(); | |||
| const navigate = useNavigateWithFromState(); | |||
| const [currentPath, setCurrentPath] = useState('/home'); | |||
| const tagsData = useMemo( | |||
| () => [ | |||
| { path: '/home', name: t('knowledgeBase'), icon: Library }, | |||
| { path: '/chat', name: t('chat'), icon: MessageSquareText }, | |||
| { path: '/search', name: t('search'), icon: Search }, | |||
| { path: '/flow', name: t('flow'), icon: Cpu }, | |||
| // { path: '/file', name: t('fileManager'), icon: FileIcon }, | |||
| ], | |||
| [t], | |||
| ); | |||
| const options = useMemo(() => { | |||
| return tagsData.map((tag) => { | |||
| const HeaderIcon = tag.icon; | |||
| return { | |||
| label: ( | |||
| <div className="flex items-center gap-1"> | |||
| <HeaderIcon className="size-5"></HeaderIcon> | |||
| <span>{tag.name}</span> | |||
| </div> | |||
| ), | |||
| value: tag.path, | |||
| }; | |||
| }); | |||
| }, [tagsData]); | |||
| // const currentPath = useMemo(() => { | |||
| // return tagsData.find((x) => pathname.startsWith(x.path))?.name || 'home'; | |||
| // }, [pathname, tagsData]); | |||
| const handleChange = (path: SegmentedValue) => { | |||
| // navigate(path as string); | |||
| setCurrentPath(path as string); | |||
| }; | |||
| const handleLogoClick = useCallback(() => { | |||
| navigate('/'); | |||
| }, [navigate]); | |||
| return ( | |||
| <section className="py-6 px-10 flex justify-between items-center border-b"> | |||
| <div className="flex items-center gap-4"> | |||
| <img | |||
| src={'/logo.svg'} | |||
| alt="logo" | |||
| className="w-[100] h-[100] mr-[12]" | |||
| onClick={handleLogoClick} | |||
| /> | |||
| <Button variant="secondary"> | |||
| <Github /> | |||
| 21.5k stars | |||
| <Star /> | |||
| </Button> | |||
| </div> | |||
| <div> | |||
| <Segmented | |||
| options={options} | |||
| value={currentPath} | |||
| onChange={handleChange} | |||
| className="bg-colors-background-inverse-standard text-backgroundInverseStandard-foreground" | |||
| ></Segmented> | |||
| </div> | |||
| <div className="flex items-center gap-4"> | |||
| <Container> | |||
| V 0.13.0 | |||
| <Button variant="secondary" className="size-8"> | |||
| <ChevronDown /> | |||
| </Button> | |||
| </Container> | |||
| <Container className="px-3 py-2"> | |||
| <Avatar className="w-[30px] h-[30px]"> | |||
| <AvatarImage src="https://github.com/shadcn.png" /> | |||
| <AvatarFallback>CN</AvatarFallback> | |||
| </Avatar> | |||
| yifanwu92@gmail.com | |||
| <Button | |||
| variant="destructive" | |||
| className="py-[2px] px-[8px] h-[23px] rounded-[4px]" | |||
| > | |||
| <Zap /> | |||
| Pro | |||
| </Button> | |||
| </Container> | |||
| </div> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| import { Outlet } from 'umi'; | |||
| import { Header } from './next-header'; | |||
| export default function NextLayout() { | |||
| return ( | |||
| <section> | |||
| <Header></Header> | |||
| <Outlet /> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export default function Dataset() { | |||
| return <div>Outset</div>; | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| import { Outlet } from 'umi'; | |||
| import { SideBar } from './sidebar'; | |||
| export default function DatasetWrapper() { | |||
| return ( | |||
| <div className="text-foreground flex"> | |||
| <SideBar></SideBar> | |||
| <div className="p-6"> | |||
| <Outlet /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export default function DatasetSettings() { | |||
| return <div>DatasetSettings</div>; | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| import { DatasetBaseKey, KnowledgeRouteKey } from '@/constants/knowledge'; | |||
| import { useCallback } from 'react'; | |||
| import { useNavigate } from 'umi'; | |||
| export const useHandleMenuClick = () => { | |||
| const navigate = useNavigate(); | |||
| const handleMenuClick = useCallback( | |||
| (key: KnowledgeRouteKey) => () => { | |||
| navigate(`/${DatasetBaseKey}/${key}`); | |||
| }, | |||
| [navigate], | |||
| ); | |||
| return { handleMenuClick }; | |||
| }; | |||
| @@ -0,0 +1,66 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { KnowledgeRouteKey } from '@/constants/knowledge'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Banknote, LayoutGrid, User } from 'lucide-react'; | |||
| import { useHandleMenuClick } from './hooks'; | |||
| const items = [ | |||
| { icon: User, label: 'Dataset', key: KnowledgeRouteKey.Dataset }, | |||
| { | |||
| icon: LayoutGrid, | |||
| label: 'Retrieval testing', | |||
| key: KnowledgeRouteKey.Testing, | |||
| }, | |||
| { icon: Banknote, label: 'Settings', key: KnowledgeRouteKey.Configuration }, | |||
| ]; | |||
| const dataset = { | |||
| id: 1, | |||
| title: 'Legal knowledge base', | |||
| files: '1,242 files', | |||
| size: '152 MB', | |||
| created: '12.02.2024', | |||
| image: 'https://github.com/shadcn.png', | |||
| }; | |||
| export function SideBar() { | |||
| const pathName = useSecondPathName(); | |||
| const { handleMenuClick } = useHandleMenuClick(); | |||
| return ( | |||
| <aside className="w-[303px]"> | |||
| <div className="p-6 space-y-2 border-b"> | |||
| <div | |||
| className="w-[70px] h-[70px] rounded-xl bg-cover" | |||
| style={{ backgroundImage: `url(${dataset.image})` }} | |||
| /> | |||
| <h3 className="text-lg font-semibold mb-2">{dataset.title}</h3> | |||
| <div className="text-sm opacity-80"> | |||
| {dataset.files} | {dataset.size} | |||
| </div> | |||
| <div className="text-sm opacity-80">Created {dataset.created}</div> | |||
| </div> | |||
| <div className="mt-4"> | |||
| {items.map((item, itemIdx) => { | |||
| const active = pathName === item.key; | |||
| return ( | |||
| <Button | |||
| key={itemIdx} | |||
| variant={active ? 'secondary' : 'ghost'} | |||
| className={cn('w-full justify-start gap-2.5 p-6 relative')} | |||
| onClick={handleMenuClick(item.key)} | |||
| > | |||
| <item.icon className="w-6 h-6" /> | |||
| <span>{item.label}</span> | |||
| {active && ( | |||
| <div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" /> | |||
| )} | |||
| </Button> | |||
| ); | |||
| })} | |||
| </div> | |||
| </aside> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export default function RetrievalTesting() { | |||
| return <div>Retrieval testing</div>; | |||
| } | |||
| @@ -134,7 +134,39 @@ const routes = [ | |||
| { | |||
| path: '/datasets', | |||
| layout: false, | |||
| component: '@/pages/datasets', | |||
| component: '@/layouts/next', | |||
| routes: [ | |||
| { | |||
| path: '/datasets', | |||
| component: '@/pages/datasets', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: '/dataset', | |||
| layout: false, | |||
| component: '@/layouts/next', | |||
| routes: [ | |||
| { path: '/dataset', redirect: '/dataset/dataset' }, | |||
| { | |||
| path: '/dataset', | |||
| component: '@/pages/dataset', | |||
| routes: [ | |||
| { | |||
| path: '/dataset/dataset', | |||
| component: '@/pages/dataset/dataset', | |||
| }, | |||
| { | |||
| path: '/dataset/configuration', | |||
| component: '@/pages/dataset/settings', | |||
| }, | |||
| { | |||
| path: '/dataset/testing', | |||
| component: '@/pages/dataset/testing', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: '/profile-setting', | |||