### What problem does this PR solve? Feat: Add ProfileSetting page #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.14.0
| @@ -13,7 +13,7 @@ const buttonVariants = cva( | |||
| destructive: | |||
| 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | |||
| outline: | |||
| 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', | |||
| 'border border-colors-outline-sentiment-primary bg-background hover:bg-accent hover:text-accent-foreground', | |||
| secondary: | |||
| 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | |||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | |||
| @@ -10,14 +10,19 @@ export enum UserSettingRouteKey { | |||
| Logout = 'logout', | |||
| } | |||
| export const UserSettingRouteMap = { | |||
| [UserSettingRouteKey.Profile]: 'Profile', | |||
| [UserSettingRouteKey.Password]: 'Password', | |||
| [UserSettingRouteKey.Model]: 'Model Providers', | |||
| [UserSettingRouteKey.System]: 'System Version', | |||
| [UserSettingRouteKey.Team]: 'Team', | |||
| [UserSettingRouteKey.Logout]: 'Log out', | |||
| }; | |||
| export const ProfileSettingBaseKey = 'profile-setting'; | |||
| export enum ProfileSettingRouteKey { | |||
| Profile = 'profile', | |||
| Plan = 'plan', | |||
| Model = 'model', | |||
| System = 'system', | |||
| Api = 'api', | |||
| Team = 'team', | |||
| Prompt = 'prompt', | |||
| Chunk = 'chunk', | |||
| Logout = 'logout', | |||
| } | |||
| // Please lowercase the file name | |||
| export const IconMap = { | |||
| @@ -1,6 +1,8 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented '; | |||
| import { ChevronRight, Cpu, MessageSquare, Search } from 'lucide-react'; | |||
| import { useMemo, useState } from 'react'; | |||
| const applications = [ | |||
| { | |||
| @@ -34,24 +36,42 @@ const applications = [ | |||
| ]; | |||
| export function Applications() { | |||
| const [val, setVal] = useState('all'); | |||
| const options = useMemo(() => { | |||
| return [ | |||
| { | |||
| label: 'All', | |||
| value: 'all', | |||
| }, | |||
| { | |||
| label: 'Chat', | |||
| value: 'chat', | |||
| }, | |||
| { | |||
| label: 'Search', | |||
| value: 'search', | |||
| }, | |||
| { | |||
| label: 'Agent', | |||
| value: 'agent', | |||
| }, | |||
| ]; | |||
| }, []); | |||
| const handleChange = (path: SegmentedValue) => { | |||
| setVal(path as string); | |||
| }; | |||
| return ( | |||
| <section className="mt-12"> | |||
| <div className="flex justify-between items-center mb-6"> | |||
| <h2 className="text-2xl font-bold">Applications</h2> | |||
| <div className="flex bg-colors-background-inverse-standard rounded-lg p-1"> | |||
| <Button variant="default" size="sm"> | |||
| All | |||
| </Button> | |||
| <Button variant="ghost" size="sm"> | |||
| Chat | |||
| </Button> | |||
| <Button variant="ghost" size="sm"> | |||
| Search | |||
| </Button> | |||
| <Button variant="ghost" size="sm"> | |||
| Agents | |||
| </Button> | |||
| </div> | |||
| <Segmented | |||
| options={options} | |||
| value={val} | |||
| onChange={handleChange} | |||
| className="bg-colors-background-inverse-standard" | |||
| ></Segmented> | |||
| </div> | |||
| <div className="grid grid-cols-4 gap-6"> | |||
| {[...Array(12)].map((_, i) => { | |||
| @@ -9,7 +9,7 @@ const datasets = [ | |||
| files: '1,242 files', | |||
| size: '152 MB', | |||
| created: '12.02.2024', | |||
| image: '/image-3.png', | |||
| image: 'https://github.com/shadcn.png', | |||
| }, | |||
| { | |||
| id: 2, | |||
| @@ -17,7 +17,7 @@ const datasets = [ | |||
| files: '1,242 files', | |||
| size: '152 MB', | |||
| created: '12.02.2024', | |||
| image: '/image.png', | |||
| image: 'https://github.com/shadcn.png', | |||
| }, | |||
| { | |||
| id: 3, | |||
| @@ -25,7 +25,7 @@ const datasets = [ | |||
| files: '1,242 files', | |||
| size: '152 MB', | |||
| created: '12.02.2024', | |||
| image: '/rectangle-86.png', | |||
| image: 'https://github.com/shadcn.png', | |||
| }, | |||
| { | |||
| id: 4, | |||
| @@ -33,7 +33,7 @@ const datasets = [ | |||
| files: '1,242 files', | |||
| size: '152 MB', | |||
| created: '12.02.2024', | |||
| image: '/image-2.png', | |||
| image: 'https://github.com/shadcn.png', | |||
| }, | |||
| ]; | |||
| @@ -83,7 +83,7 @@ export function HomeHeader() { | |||
| options={options} | |||
| value={currentPath} | |||
| onChange={handleChange} | |||
| className="bg-backgroundInverseStandard text-backgroundInverseStandard-foreground" | |||
| className="bg-colors-background-inverse-standard text-backgroundInverseStandard-foreground" | |||
| ></Segmented> | |||
| </div> | |||
| <div className="flex items-center gap-4"> | |||
| @@ -0,0 +1,20 @@ | |||
| import { ProfileSettingRouteKey } from '@/constants/setting'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| export const useGetPageTitle = (): string => { | |||
| const pathName = useSecondPathName(); | |||
| const LabelMap = { | |||
| [ProfileSettingRouteKey.Profile]: 'User profile', | |||
| [ProfileSettingRouteKey.Plan]: 'Plan & balance', | |||
| [ProfileSettingRouteKey.Model]: 'Model management', | |||
| [ProfileSettingRouteKey.System]: 'System', | |||
| [ProfileSettingRouteKey.Api]: 'Api', | |||
| [ProfileSettingRouteKey.Team]: 'Team management', | |||
| [ProfileSettingRouteKey.Prompt]: 'Prompt management', | |||
| [ProfileSettingRouteKey.Chunk]: 'Chunk method', | |||
| [ProfileSettingRouteKey.Logout]: 'Logout', | |||
| }; | |||
| return LabelMap[pathName as ProfileSettingRouteKey]; | |||
| }; | |||
| @@ -0,0 +1,34 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { ArrowLeft } from 'lucide-react'; | |||
| import { Outlet } from 'umi'; | |||
| import { useGetPageTitle } from './hooks'; | |||
| import { SideBar } from './sidebar'; | |||
| export default function ProfileSetting() { | |||
| const title = useGetPageTitle(); | |||
| return ( | |||
| <div className="flex flex-col w-full h-screen bg-background text-foreground"> | |||
| <header className="flex items-center border-b"> | |||
| <div className="flex items-center border-r p-1.5"> | |||
| <Button variant="ghost" size="icon"> | |||
| <ArrowLeft className="w-5 h-5" /> | |||
| </Button> | |||
| </div> | |||
| <div className="p-4"> | |||
| <h1 className="text-2xl font-semibold tracking-tight"> | |||
| Profile & settings | |||
| </h1> | |||
| </div> | |||
| </header> | |||
| <div className="flex flex-1 bg-muted/50"> | |||
| <SideBar></SideBar> | |||
| <main className="flex-1 p-10"> | |||
| <h1 className="text-3xl font-bold mb-6"> {title}</h1> | |||
| <Outlet></Outlet> | |||
| </main> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export default function Plan() { | |||
| return <div>plan</div>; | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { | |||
| Select, | |||
| SelectContent, | |||
| SelectItem, | |||
| SelectTrigger, | |||
| SelectValue, | |||
| } from '@/components/ui/select'; | |||
| export default function Profile() { | |||
| return ( | |||
| <section> | |||
| <Avatar className="w-[120px] h-[120px] mb-6"> | |||
| <AvatarImage | |||
| src={ | |||
| 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg' | |||
| } | |||
| alt="Profile" | |||
| /> | |||
| <AvatarFallback>YW</AvatarFallback> | |||
| </Avatar> | |||
| <div className="space-y-6 max-w-[600px]"> | |||
| <div className="space-y-2"> | |||
| <label className="text-sm text-muted-foreground">User name</label> | |||
| <Input | |||
| defaultValue="yifanwu92" | |||
| className="bg-colors-background-inverse-weak" | |||
| /> | |||
| </div> | |||
| <div className="space-y-2"> | |||
| <label className="text-sm text-muted-foreground">Email</label> | |||
| <Input | |||
| defaultValue="yifanwu92@gmail.com" | |||
| className="bg-colors-background-inverse-weak" | |||
| /> | |||
| </div> | |||
| <div className="space-y-2"> | |||
| <label className="text-sm text-muted-foreground">Language</label> | |||
| <Select defaultValue="english"> | |||
| <SelectTrigger className="bg-colors-background-inverse-weak"> | |||
| <SelectValue /> | |||
| </SelectTrigger> | |||
| <SelectContent> | |||
| <SelectItem value="english">English</SelectItem> | |||
| </SelectContent> | |||
| </Select> | |||
| </div> | |||
| <div className="space-y-2"> | |||
| <label className="text-sm text-muted-foreground">Timezone</label> | |||
| <Select defaultValue="utc9"> | |||
| <SelectTrigger className="bg-colors-background-inverse-weak"> | |||
| <SelectValue /> | |||
| </SelectTrigger> | |||
| <SelectContent> | |||
| <SelectItem value="utc9">UTC+9 Asia/Shanghai</SelectItem> | |||
| </SelectContent> | |||
| </Select> | |||
| </div> | |||
| <Button variant="outline" className="mt-4"> | |||
| Change password | |||
| </Button> | |||
| </div> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| import { | |||
| ProfileSettingBaseKey, | |||
| ProfileSettingRouteKey, | |||
| } from '@/constants/setting'; | |||
| import { useLogout } from '@/hooks/login-hooks'; | |||
| import { useCallback } from 'react'; | |||
| import { useNavigate } from 'umi'; | |||
| export const useHandleMenuClick = () => { | |||
| const navigate = useNavigate(); | |||
| const { logout } = useLogout(); | |||
| const handleMenuClick = useCallback( | |||
| (key: ProfileSettingRouteKey) => () => { | |||
| if (key === ProfileSettingRouteKey.Logout) { | |||
| logout(); | |||
| } else { | |||
| navigate(`/${ProfileSettingBaseKey}/${key}`); | |||
| } | |||
| }, | |||
| [logout, navigate], | |||
| ); | |||
| return { handleMenuClick }; | |||
| }; | |||
| @@ -0,0 +1,92 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Label } from '@/components/ui/label'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { ProfileSettingRouteKey } from '@/constants/setting'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { | |||
| AlignEndVertical, | |||
| Banknote, | |||
| Box, | |||
| FileCog, | |||
| LayoutGrid, | |||
| LogOut, | |||
| User, | |||
| } from 'lucide-react'; | |||
| import { useHandleMenuClick } from './hooks'; | |||
| const menuItems = [ | |||
| { | |||
| section: 'Account & collaboration', | |||
| items: [ | |||
| { icon: User, label: 'Profile', key: ProfileSettingRouteKey.Profile }, | |||
| { icon: LayoutGrid, label: 'Team', key: ProfileSettingRouteKey.Team }, | |||
| { icon: Banknote, label: 'Plan', key: ProfileSettingRouteKey.Plan }, | |||
| ], | |||
| }, | |||
| { | |||
| section: 'System configurations', | |||
| items: [ | |||
| { | |||
| icon: Box, | |||
| label: 'Model management', | |||
| key: ProfileSettingRouteKey.Model, | |||
| }, | |||
| { | |||
| icon: FileCog, | |||
| label: 'Prompt management', | |||
| key: ProfileSettingRouteKey.Prompt, | |||
| }, | |||
| { | |||
| icon: AlignEndVertical, | |||
| label: 'Chunking method', | |||
| key: ProfileSettingRouteKey.Chunk, | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| export function SideBar() { | |||
| const pathName = useSecondPathName(); | |||
| const { handleMenuClick } = useHandleMenuClick(); | |||
| return ( | |||
| <aside className="w-[303px] bg-background border-r"> | |||
| {menuItems.map((section, idx) => ( | |||
| <div key={idx}> | |||
| <h2 className="p-6 text-sm font-semibold">{section.section}</h2> | |||
| {section.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> | |||
| ))} | |||
| <div className="p-6 mt-auto border-t"> | |||
| <div className="flex items-center gap-2 mb-6"> | |||
| <Switch id="dark-mode" /> | |||
| <Label htmlFor="dark-mode" className="text-sm"> | |||
| Dark | |||
| </Label> | |||
| </div> | |||
| <Button variant="outline" className="w-full gap-3"> | |||
| <LogOut className="w-6 h-6" /> | |||
| Logout | |||
| </Button> | |||
| </div> | |||
| </aside> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| export default function Team() { | |||
| return <div>team</div>; | |||
| } | |||
| @@ -131,6 +131,26 @@ const routes = [ | |||
| layout: false, | |||
| component: '@/pages/home', | |||
| }, | |||
| { | |||
| path: '/profile-setting', | |||
| layout: false, | |||
| component: '@/pages/profile-setting', | |||
| routes: [ | |||
| { path: '/profile-setting', redirect: '/profile-setting/profile' }, | |||
| { | |||
| path: '/profile-setting/profile', | |||
| component: '@/pages/profile-setting/profile', | |||
| }, | |||
| { | |||
| path: '/profile-setting/team', | |||
| component: '@/pages/profile-setting/team', | |||
| }, | |||
| { | |||
| path: '/profile-setting/plan', | |||
| component: '@/pages/profile-setting/plan', | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| export default routes; | |||
| @@ -25,6 +25,10 @@ module.exports = { | |||
| background: 'var(--background)', | |||
| foreground: 'hsl(var(--foreground))', | |||
| buttonBlueText: 'var(--button-blue-text)', | |||
| 'colors-outline-sentiment-primary': | |||
| 'var(--colors-outline-sentiment-primary)', | |||
| primary: { | |||
| DEFAULT: 'hsl(var(--primary))', | |||
| foreground: 'hsl(var(--primary-foreground))', | |||
| @@ -39,6 +39,10 @@ | |||
| --background-inverse-standard-foreground: rgb(92, 81, 81); | |||
| --button-blue-text: rgb(22, 119, 255); | |||
| --colors-outline-sentiment-primary: rgba(127, 105, 255, 1); | |||
| --colors-text-core-standard: rgba(127, 105, 255, 1); | |||
| } | |||
| .dark { | |||
| @@ -108,6 +112,10 @@ | |||
| --colors-background-neutral-standard: rgba(11, 10, 18, 1); | |||
| --colors-background-neutral-strong: rgba(29, 26, 44, 1); | |||
| --colors-background-neutral-weak: rgba(17, 16, 23, 1); | |||
| --colors-outline-sentiment-primary: rgba(146, 118, 255, 1); | |||
| --colors-text-core-standard: rgba(137, 126, 255, 1); | |||
| } | |||
| } | |||