diff --git a/web/src/components/theme-toggle.tsx b/web/src/components/theme-toggle.tsx new file mode 100644 index 000000000..3d05fdcb8 --- /dev/null +++ b/web/src/components/theme-toggle.tsx @@ -0,0 +1,48 @@ +import { ThemeEnum } from '@/constants/common'; +import { Moon, Sun } from 'lucide-react'; +import { FC, useCallback } from 'react'; +import { useIsDarkTheme, useTheme } from './theme-provider'; +import { Button } from './ui/button'; + +const ThemeToggle: FC = () => { + const { setTheme } = useTheme(); + const isDarkTheme = useIsDarkTheme(); + const handleThemeChange = useCallback( + (checked: boolean) => { + setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light); + }, + [setTheme], + ); + return ( + + ); +}; + +export default ThemeToggle; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 613a3db96..1981c3e21 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1792,6 +1792,8 @@ Important structured information may include: names, dates, locations, events, k result: 'Result', parseSummary: 'Parse Summary', parseSummaryTip: 'Parser:deepdoc', + parserMethod: 'Parser Method', + outputFormat: 'Output Format', rerunFromCurrentStep: 'Rerun From Current Step', rerunFromCurrentStepTip: 'Changes detected. Click to re-run.', confirmRerun: 'Confirm Rerun Process', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e011d55e8..20469e14c 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1680,6 +1680,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, result: '结果', parseSummary: '解析摘要', parseSummaryTip: '解析器: deepdoc', + parserMethod: '解析方法', + outputFormat: '输出格式', rerunFromCurrentStep: '从当前步骤重新运行', rerunFromCurrentStepTip: '已修改,点击重新运行。', confirmRerun: '确认重新运行流程', diff --git a/web/src/pages/dataflow-result/hooks.ts b/web/src/pages/dataflow-result/hooks.ts index c41d0a451..4c4ad590a 100644 --- a/web/src/pages/dataflow-result/hooks.ts +++ b/web/src/pages/dataflow-result/hooks.ts @@ -345,10 +345,10 @@ export const useSummaryInfo = ( const { output_format, parse_method } = setups; const res = []; if (parse_method) { - res.push(`${t('dataflow.parserMethod')}: ${parse_method}`); + res.push(`${t('dataflowParser.parserMethod')}: ${parse_method}`); } if (output_format) { - res.push(`${t('dataflow.outputFormat')}: ${output_format}`); + res.push(`${t('dataflowParser.outputFormat')}: ${output_format}`); } return res.join(' '); } diff --git a/web/src/pages/profile-setting/profile/index.tsx b/web/src/pages/profile-setting/profile/index.tsx index 9b6b0b4c4..7f8cf5676 100644 --- a/web/src/pages/profile-setting/profile/index.tsx +++ b/web/src/pages/profile-setting/profile/index.tsx @@ -121,7 +121,7 @@ const ProfilePage: FC = () => { // }; return ( -
+
{/* Header */}

{t('profile')}

diff --git a/web/src/pages/profile-setting/sidebar/hooks.tsx b/web/src/pages/profile-setting/sidebar/hooks.tsx index 260df587c..e69bd6c3b 100644 --- a/web/src/pages/profile-setting/sidebar/hooks.tsx +++ b/web/src/pages/profile-setting/sidebar/hooks.tsx @@ -1,10 +1,11 @@ import { useLogout } from '@/hooks/login-hooks'; import { Routes } from '@/routes'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { useNavigate } from 'umi'; export const useHandleMenuClick = () => { const navigate = useNavigate(); + const [active, setActive] = useState(); const { logout } = useLogout(); const handleMenuClick = useCallback( @@ -12,11 +13,12 @@ export const useHandleMenuClick = () => { if (key === Routes.Logout) { logout(); } else { + setActive(key); navigate(`${Routes.ProfileSetting}${key}`); } }, [logout, navigate], ); - return { handleMenuClick }; + return { handleMenuClick, active }; }; diff --git a/web/src/pages/profile-setting/sidebar/index.tsx b/web/src/pages/profile-setting/sidebar/index.tsx index 75e63056d..4a1079874 100644 --- a/web/src/pages/profile-setting/sidebar/index.tsx +++ b/web/src/pages/profile-setting/sidebar/index.tsx @@ -1,10 +1,9 @@ -import { useIsDarkTheme, useTheme } from '@/components/theme-provider'; +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import ThemeToggle from '@/components/theme-toggle'; import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { ThemeEnum } from '@/constants/common'; import { useLogout } from '@/hooks/login-hooks'; import { useSecondPathName } from '@/hooks/route-hook'; +import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; import { cn } from '@/lib/utils'; import { Routes } from '@/routes'; import { @@ -12,11 +11,9 @@ import { Banknote, Box, FileCog, - LayoutGrid, - LogOut, User, + Users, } from 'lucide-react'; -import { useCallback } from 'react'; import { useHandleMenuClick } from './hooks'; const menuItems = [ @@ -24,7 +21,7 @@ const menuItems = [ section: 'Account & collaboration', items: [ { icon: User, label: 'Profile', key: Routes.Profile }, - { icon: LayoutGrid, label: 'Team', key: Routes.Team }, + { icon: Users, label: 'Team', key: Routes.Team }, { icon: Banknote, label: 'Plan', key: Routes.Plan }, { icon: Banknote, label: 'MCP', key: Routes.Mcp }, ], @@ -53,66 +50,62 @@ const menuItems = [ export function SideBar() { const pathName = useSecondPathName(); - const { handleMenuClick } = useHandleMenuClick(); - const { setTheme } = useTheme(); - const isDarkTheme = useIsDarkTheme(); + const { data: userInfo } = useFetchUserInfo(); + const { handleMenuClick, active } = useHandleMenuClick(); const { logout } = useLogout(); - const handleThemeChange = useCallback( - (checked: boolean) => { - setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light); - }, - [setTheme], - ); - return ( -
-
-
- - +
+
+
diff --git a/web/src/pages/user-setting/index.tsx b/web/src/pages/user-setting/index.tsx index 7fa104431..4caf9fa2f 100644 --- a/web/src/pages/user-setting/index.tsx +++ b/web/src/pages/user-setting/index.tsx @@ -1,5 +1,5 @@ import { Outlet } from 'umi'; -import SideBar from './sidebar'; +import { SideBar } from './sidebar'; import { PageHeader } from '@/components/page-header'; import { diff --git a/web/src/pages/user-setting/profile/hooks/use-profile.ts b/web/src/pages/user-setting/profile/hooks/use-profile.ts new file mode 100644 index 000000000..5b8bdf7b5 --- /dev/null +++ b/web/src/pages/user-setting/profile/hooks/use-profile.ts @@ -0,0 +1,151 @@ +// src/hooks/useProfile.ts +import { + useFetchUserInfo, + useSaveSetting, +} from '@/hooks/use-user-setting-request'; +import { rsaPsw } from '@/utils'; +import { useCallback, useEffect, useState } from 'react'; + +interface ProfileData { + userName: string; + timeZone: string; + currPasswd?: string; + newPasswd?: string; + avatar: string; + email: string; + confirmPasswd?: string; +} + +export const EditType = { + editName: 'editName', + editTimeZone: 'editTimeZone', + editPassword: 'editPassword', +} as const; + +export type IEditType = keyof typeof EditType; + +export const modalTitle = { + [EditType.editName]: 'Edit Name', + [EditType.editTimeZone]: 'Edit Time Zone', + [EditType.editPassword]: 'Edit Password', +} as const; + +export const useProfile = () => { + const { data: userInfo } = useFetchUserInfo(); + const [profile, setProfile] = useState({ + userName: '', + avatar: '', + timeZone: '', + email: '', + currPasswd: '', + }); + + const [editType, setEditType] = useState(EditType.editName); + const [isEditing, setIsEditing] = useState(false); + const [editForm, setEditForm] = useState>({}); + const { + saveSetting, + loading: submitLoading, + data: saveSettingData, + } = useSaveSetting(); + + useEffect(() => { + // form.setValue('currPasswd', ''); // current password + const profile = { + userName: userInfo.nickname, + timeZone: userInfo.timezone, + avatar: userInfo.avatar || '', + email: userInfo.email, + currPasswd: userInfo.password, + }; + setProfile(profile); + }, [userInfo, setProfile]); + + useEffect(() => { + if (saveSettingData === 0) { + setIsEditing(false); + setEditForm({}); + } + }, [saveSettingData]); + const onSubmit = (newProfile: ProfileData) => { + const payload: Partial<{ + nickname: string; + password: string; + new_password: string; + avatar: string; + timezone: string; + }> = { + nickname: newProfile.userName, + avatar: newProfile.avatar, + timezone: newProfile.timeZone, + }; + + if ( + 'currPasswd' in newProfile && + 'newPasswd' in newProfile && + newProfile.currPasswd && + newProfile.newPasswd + ) { + payload.password = rsaPsw(newProfile.currPasswd!) as string; + payload.new_password = rsaPsw(newProfile.newPasswd!) as string; + } + console.log('payload', payload); + if (editType === EditType.editName && payload.nickname) { + saveSetting({ nickname: payload.nickname }); + setProfile(newProfile); + } + if (editType === EditType.editTimeZone && payload.timezone) { + saveSetting({ timezone: payload.timezone }); + setProfile(newProfile); + } + if (editType === EditType.editPassword && payload.password) { + saveSetting({ + password: payload.password, + new_password: payload.new_password, + }); + setProfile(newProfile); + } + // saveSetting(payload); + }; + + const handleEditClick = useCallback( + (type: IEditType) => { + setEditForm(profile); + setEditType(type); + setIsEditing(true); + }, + [profile], + ); + + const handleCancel = useCallback(() => { + setIsEditing(false); + setEditForm({}); + }, []); + + const handleSave = (data: ProfileData) => { + console.log('handleSave', data); + const newProfile = { ...profile, ...data }; + + onSubmit(newProfile); + // setIsEditing(false); + // setEditForm({}); + }; + + const handleAvatarUpload = (avatar: string) => { + setProfile((prev) => ({ ...prev, avatar })); + saveSetting({ avatar }); + }; + + return { + profile, + setProfile, + submitLoading: submitLoading, + isEditing, + editType, + editForm, + handleEditClick, + handleCancel, + handleSave, + handleAvatarUpload, + }; +}; diff --git a/web/src/pages/user-setting/profile/index.tsx b/web/src/pages/user-setting/profile/index.tsx new file mode 100644 index 000000000..266569fc5 --- /dev/null +++ b/web/src/pages/user-setting/profile/index.tsx @@ -0,0 +1,413 @@ +// src/components/ProfilePage.tsx +import { AvatarUpload } from '@/components/avatar-upload'; +import PasswordInput from '@/components/originui/password-input'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Modal } from '@/components/ui/modal/modal'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useTranslate } from '@/hooks/common-hooks'; +import { TimezoneList } from '@/pages/user-setting/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from 'i18next'; +import { Loader2Icon, PenLine } from 'lucide-react'; +import { FC, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { EditType, modalTitle, useProfile } from './hooks/use-profile'; + +const baseSchema = z.object({ + userName: z + .string() + .min(1, { message: t('setting.usernameMessage') }) + .trim(), + timeZone: z + .string() + .trim() + .min(1, { message: t('setting.timezonePlaceholder') }), +}); + +const nameSchema = baseSchema.extend({ + currPasswd: z.string().optional(), + newPasswd: z.string().optional(), + confirmPasswd: z.string().optional(), +}); + +const passwordSchema = baseSchema + .extend({ + currPasswd: z + .string({ + required_error: t('setting.currentPasswordMessage'), + }) + .trim(), + newPasswd: z + .string({ + required_error: t('setting.newPasswordMessage'), + }) + .trim() + .min(8, { message: t('setting.newPasswordDescription') }), + confirmPasswd: z + .string({ + required_error: t('setting.confirmPasswordMessage'), + }) + .trim() + .min(8, { message: t('setting.newPasswordDescription') }), + }) + .superRefine((data, ctx) => { + if ( + data.newPasswd && + data.confirmPasswd && + data.newPasswd !== data.confirmPasswd + ) { + ctx.addIssue({ + path: ['confirmPasswd'], + message: t('setting.confirmPasswordNonMatchMessage'), + code: z.ZodIssueCode.custom, + }); + } + }); +const ProfilePage: FC = () => { + const { t } = useTranslate('setting'); + + const { + profile, + editType, + isEditing, + submitLoading, + editForm, + handleEditClick, + handleCancel, + handleSave, + handleAvatarUpload, + } = useProfile(); + + const form = useForm>({ + resolver: zodResolver( + editType === EditType.editPassword ? passwordSchema : nameSchema, + ), + defaultValues: { + userName: '', + timeZone: '', + }, + // shouldUnregister: true, + }); + useEffect(() => { + form.reset({ ...editForm, currPasswd: undefined }); + }, [editForm, form]); + + // const ModalContent: FC = () => { + // // let content = null; + // // if (editType === EditType.editName) { + // // content = editName(); + // // } + // return ( + // <> + + // + // ); + // }; + + return ( +
+ {/* Header */} +
+

{t('profile')}

+
+ {t('profileDescription')} +
+
+ + {/* Main Content */} +
+ {/* Name */} +
+ +
+
+ {profile.userName} +
+ +
+
+ + {/* Avatar */} +
+ +
+ +
+
+ + {/* Time Zone */} +
+ +
+
+ {profile.timeZone} +
+ +
+
+ + {/* Email Address */} +
+ +
+
+ {profile.email} +
+ + {t('emailDescription')} + +
+
+ + {/* Password */} +
+ +
+
+ {profile.currPasswd ? '********' : ''} +
+ +
+
+
+ + {editType && ( + { + if (!open) { + handleCancel(); + } + }} + className="!w-[480px]" + > + {/* */} +
+ handleSave(data as any))} + className="flex flex-col mt-6 mb-8 ml-2 space-y-6 " + > + {editType === EditType.editName && ( + ( + +
+ + {t('username')} + + + + +
+
+
+ +
+
+ )} + /> + )} + + {editType === EditType.editTimeZone && ( + ( + +
+ + {t('timezone')} + + +
+
+
+ +
+
+ )} + /> + )} + + {editType === EditType.editPassword && ( + <> + ( + +
+ + {t('currentPassword')} + + + + +
+
+ +
+
+ )} + /> + ( + +
+ + {t('newPassword')} + + + + +
+
+ +
+
+ )} + /> + ( + +
+ + {t('confirmPassword')} + + + { + form.trigger('confirmPasswd'); + }} + onChange={(ev) => { + form.setValue( + 'confirmPasswd', + ev.target.value.trim(), + ); + }} + /> + +
+
+ +
+
+ )} + /> + + )} + +
+ + +
+ + +
+ )} +
+ ); +}; + +export default ProfilePage; diff --git a/web/src/pages/user-setting/sidebar/hooks.tsx b/web/src/pages/user-setting/sidebar/hooks.tsx new file mode 100644 index 000000000..fcfedcfca --- /dev/null +++ b/web/src/pages/user-setting/sidebar/hooks.tsx @@ -0,0 +1,24 @@ +import { useLogout } from '@/hooks/login-hooks'; +import { Routes } from '@/routes'; +import { useCallback, useState } from 'react'; +import { useNavigate } from 'umi'; + +export const useHandleMenuClick = () => { + const navigate = useNavigate(); + const [active, setActive] = useState(); + const { logout } = useLogout(); + + const handleMenuClick = useCallback( + (key: Routes) => () => { + if (key === Routes.Logout) { + logout(); + } else { + setActive(key); + navigate(`${Routes.UserSetting}${key}`); + } + }, + [logout, navigate], + ); + + return { handleMenuClick, active }; +}; diff --git a/web/src/pages/user-setting/sidebar/index.less b/web/src/pages/user-setting/sidebar/index.less deleted file mode 100644 index 16777e579..000000000 --- a/web/src/pages/user-setting/sidebar/index.less +++ /dev/null @@ -1,5 +0,0 @@ -.sideBarWrapper { - .version { - color: rgb(17, 206, 17); - } -} diff --git a/web/src/pages/user-setting/sidebar/index.tsx b/web/src/pages/user-setting/sidebar/index.tsx index 1e8e62b2f..9b469e7ee 100644 --- a/web/src/pages/user-setting/sidebar/index.tsx +++ b/web/src/pages/user-setting/sidebar/index.tsx @@ -1,84 +1,109 @@ +import { IconFontFill } from '@/components/icon-font'; +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import ThemeToggle from '@/components/theme-toggle'; +import { Button } from '@/components/ui/button'; import { Domain } from '@/constants/common'; -import { useTranslate } from '@/hooks/common-hooks'; import { useLogout } from '@/hooks/login-hooks'; import { useSecondPathName } from '@/hooks/route-hook'; -import { useFetchSystemVersion } from '@/hooks/user-setting-hooks'; -import type { MenuProps } from 'antd'; -import { Flex, Menu } from 'antd'; -import React, { useEffect, useMemo } from 'react'; -import { useNavigate } from 'umi'; import { - UserSettingBaseKey, - UserSettingIconMap, - UserSettingRouteKey, -} from '../constants'; -import styles from './index.less'; + useFetchSystemVersion, + useFetchUserInfo, +} from '@/hooks/use-user-setting-request'; +import { cn } from '@/lib/utils'; +import { Routes } from '@/routes'; +import { Banknote, Box, Cog, Unplug, User, Users } from 'lucide-react'; +import { useEffect } from 'react'; +import { useHandleMenuClick } from './hooks'; -type MenuItem = Required['items'][number]; +const menuItems = [ + { icon: User, label: 'Profile', key: Routes.Profile }, + { icon: Users, label: 'Team', key: Routes.Team }, + { icon: Box, label: 'Model Providers', key: Routes.Model }, + { icon: Unplug, label: 'API', key: Routes.Api }, + // { + // icon: MessageSquareQuote, + // label: 'Prompt Templates', + // key: Routes.Profile, + // }, + // { icon: TextSearch, label: 'Retrieval Templates', key: Routes.Profile }, + { icon: Cog, label: 'System', key: Routes.System }, + // { icon: Banknote, label: 'Plan', key: Routes.Plan }, + { icon: Banknote, label: 'MCP', key: Routes.Mcp }, +]; -const SideBar = () => { - const navigate = useNavigate(); +export function SideBar() { const pathName = useSecondPathName(); - const { logout } = useLogout(); - const { t } = useTranslate('setting'); + const { data: userInfo } = useFetchUserInfo(); + const { handleMenuClick, active } = useHandleMenuClick(); const { version, fetchSystemVersion } = useFetchSystemVersion(); - useEffect(() => { if (location.host !== Domain) { fetchSystemVersion(); } }, [fetchSystemVersion]); - - function getItem( - label: string, - key: React.Key, - icon?: React.ReactNode, - children?: MenuItem[], - type?: 'group', - ): MenuItem { - return { - key, - icon, - children, - label: ( - - {t(label)} - - {label === 'system' && version} - - - ), - type, - } as MenuItem; - } - - const items: MenuItem[] = Object.values(UserSettingRouteKey).map((value) => - getItem(value, value, UserSettingIconMap[value]), - ); - - const handleMenuClick: MenuProps['onClick'] = ({ key }) => { - if (key === UserSettingRouteKey.Logout) { - logout(); - } else { - navigate(`/${UserSettingBaseKey}/${key}`); - } - }; - - const selectedKeys = useMemo(() => { - return [pathName]; - }, [pathName]); + const { logout } = useLogout(); return ( -
- -
- ); -}; + + ); +} diff --git a/web/src/routes.ts b/web/src/routes.ts index e35609559..5a35ba99b 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -18,9 +18,11 @@ export enum Routes { Files = '/files', ProfileSetting = '/profile-setting', Profile = '/profile', + Api = '/api', Mcp = '/mcp', Team = '/team', Plan = '/plan', + System = '/system', Model = '/model', Prompt = '/prompt', ProfileMcp = `${ProfileSetting}${Mcp}`, @@ -362,7 +364,7 @@ const routes = [ { path: '/user-setting/profile', // component: '@/pages/user-setting/setting-profile', - component: '@/pages/user-setting/setting-profile', + component: '@/pages/user-setting/profile', }, { path: '/user-setting/locale', @@ -381,11 +383,11 @@ const routes = [ component: '@/pages/user-setting/setting-team', }, { - path: '/user-setting/system', + path: `/user-setting${Routes.System}`, component: '@/pages/user-setting/setting-system', }, { - path: '/user-setting/api', + path: `/user-setting${Routes.Api}`, component: '@/pages/user-setting/setting-api', }, {