### 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
| destructive: | destructive: | ||||
| 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | ||||
| outline: | 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: | secondary: | ||||
| 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | ||||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | ghost: 'hover:bg-accent hover:text-accent-foreground', |
| Logout = 'logout', | 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 | // Please lowercase the file name | ||||
| export const IconMap = { | export const IconMap = { |
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { Card, CardContent } from '@/components/ui/card'; | import { Card, CardContent } from '@/components/ui/card'; | ||||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented '; | |||||
| import { ChevronRight, Cpu, MessageSquare, Search } from 'lucide-react'; | import { ChevronRight, Cpu, MessageSquare, Search } from 'lucide-react'; | ||||
| import { useMemo, useState } from 'react'; | |||||
| const applications = [ | const applications = [ | ||||
| { | { | ||||
| ]; | ]; | ||||
| export function 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 ( | return ( | ||||
| <section className="mt-12"> | <section className="mt-12"> | ||||
| <div className="flex justify-between items-center mb-6"> | <div className="flex justify-between items-center mb-6"> | ||||
| <h2 className="text-2xl font-bold">Applications</h2> | <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> | ||||
| <div className="grid grid-cols-4 gap-6"> | <div className="grid grid-cols-4 gap-6"> | ||||
| {[...Array(12)].map((_, i) => { | {[...Array(12)].map((_, i) => { |
| files: '1,242 files', | files: '1,242 files', | ||||
| size: '152 MB', | size: '152 MB', | ||||
| created: '12.02.2024', | created: '12.02.2024', | ||||
| image: '/image-3.png', | |||||
| image: 'https://github.com/shadcn.png', | |||||
| }, | }, | ||||
| { | { | ||||
| id: 2, | id: 2, | ||||
| files: '1,242 files', | files: '1,242 files', | ||||
| size: '152 MB', | size: '152 MB', | ||||
| created: '12.02.2024', | created: '12.02.2024', | ||||
| image: '/image.png', | |||||
| image: 'https://github.com/shadcn.png', | |||||
| }, | }, | ||||
| { | { | ||||
| id: 3, | id: 3, | ||||
| files: '1,242 files', | files: '1,242 files', | ||||
| size: '152 MB', | size: '152 MB', | ||||
| created: '12.02.2024', | created: '12.02.2024', | ||||
| image: '/rectangle-86.png', | |||||
| image: 'https://github.com/shadcn.png', | |||||
| }, | }, | ||||
| { | { | ||||
| id: 4, | id: 4, | ||||
| files: '1,242 files', | files: '1,242 files', | ||||
| size: '152 MB', | size: '152 MB', | ||||
| created: '12.02.2024', | created: '12.02.2024', | ||||
| image: '/image-2.png', | |||||
| image: 'https://github.com/shadcn.png', | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| options={options} | options={options} | ||||
| value={currentPath} | value={currentPath} | ||||
| onChange={handleChange} | onChange={handleChange} | ||||
| className="bg-backgroundInverseStandard text-backgroundInverseStandard-foreground" | |||||
| className="bg-colors-background-inverse-standard text-backgroundInverseStandard-foreground" | |||||
| ></Segmented> | ></Segmented> | ||||
| </div> | </div> | ||||
| <div className="flex items-center gap-4"> | <div className="flex items-center gap-4"> |
| 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]; | |||||
| }; |
| 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> | |||||
| ); | |||||
| } |
| export default function Plan() { | |||||
| return <div>plan</div>; | |||||
| } |
| 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> | |||||
| ); | |||||
| } |
| 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 }; | |||||
| }; |
| 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> | |||||
| ); | |||||
| } |
| export default function Team() { | |||||
| return <div>team</div>; | |||||
| } |
| layout: false, | layout: false, | ||||
| component: '@/pages/home', | 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; | export default routes; |
| background: 'var(--background)', | background: 'var(--background)', | ||||
| foreground: 'hsl(var(--foreground))', | foreground: 'hsl(var(--foreground))', | ||||
| buttonBlueText: 'var(--button-blue-text)', | buttonBlueText: 'var(--button-blue-text)', | ||||
| 'colors-outline-sentiment-primary': | |||||
| 'var(--colors-outline-sentiment-primary)', | |||||
| primary: { | primary: { | ||||
| DEFAULT: 'hsl(var(--primary))', | DEFAULT: 'hsl(var(--primary))', | ||||
| foreground: 'hsl(var(--primary-foreground))', | foreground: 'hsl(var(--primary-foreground))', |
| --background-inverse-standard-foreground: rgb(92, 81, 81); | --background-inverse-standard-foreground: rgb(92, 81, 81); | ||||
| --button-blue-text: rgb(22, 119, 255); | --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 { | .dark { | ||||
| --colors-background-neutral-standard: rgba(11, 10, 18, 1); | --colors-background-neutral-standard: rgba(11, 10, 18, 1); | ||||
| --colors-background-neutral-strong: rgba(29, 26, 44, 1); | --colors-background-neutral-strong: rgba(29, 26, 44, 1); | ||||
| --colors-background-neutral-weak: rgba(17, 16, 23, 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); | |||||
| } | } | ||||
| } | } | ||||