Optimize the style and logic of the profile (#8639)

### What problem does this PR solve?

Optimize the style and logic of the profile [#3221
](https://github.com/infiniflow/ragflow/issues/3221)

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
dcc123456
2025-07-03 13:31:22 +08:00
committed by GitHub
parent 747da87a1e
commit 1dd18f95e9
4 changed files with 227 additions and 173 deletions

View File

@ -552,6 +552,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
setting: { setting: {
profile: 'Profile', profile: 'Profile',
avatar: 'Avatar', avatar: 'Avatar',
avatarTip: 'This will be displayed on your profile.',
profileDescription: 'Update your photo and personal details here.', profileDescription: 'Update your photo and personal details here.',
maxTokens: 'Max Tokens', maxTokens: 'Max Tokens',
maxTokensMessage: 'Max Tokens is required', maxTokensMessage: 'Max Tokens is required',
@ -584,6 +585,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
currentPassword: 'Current password', currentPassword: 'Current password',
currentPasswordMessage: 'Please input your password!', currentPasswordMessage: 'Please input your password!',
newPassword: 'New password', newPassword: 'New password',
changePassword: 'Change Password',
newPasswordMessage: 'Please input your password!', newPasswordMessage: 'Please input your password!',
newPasswordDescription: newPasswordDescription:
'Your new password must be more than 8 characters.', 'Your new password must be more than 8 characters.',

View File

@ -535,6 +535,7 @@ export default {
setting: { setting: {
profile: '概述', profile: '概述',
avatar: '头像', avatar: '头像',
avatarTip: '這會在你的個人主頁展示',
profileDescription: '在此更新您的照片和個人詳細信息。', profileDescription: '在此更新您的照片和個人詳細信息。',
maxTokens: '最大token數', maxTokens: '最大token數',
maxTokensMessage: '最大token數是必填項', maxTokensMessage: '最大token數是必填項',
@ -567,6 +568,7 @@ export default {
currentPassword: '當前密碼', currentPassword: '當前密碼',
currentPasswordMessage: '請輸入當前密碼', currentPasswordMessage: '請輸入當前密碼',
newPassword: '新密碼', newPassword: '新密碼',
changePassword: '修改密碼',
newPasswordMessage: '請輸入新密碼', newPasswordMessage: '請輸入新密碼',
newPasswordDescription: '您的新密碼必須超過 8 個字符。', newPasswordDescription: '您的新密碼必須超過 8 個字符。',
confirmPassword: '確認新密碼', confirmPassword: '確認新密碼',

View File

@ -556,6 +556,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
setting: { setting: {
profile: '概要', profile: '概要',
avatar: '头像', avatar: '头像',
avatarTip: '这会在你的个人主页展示',
profileDescription: '在此更新您的照片和个人详细信息。', profileDescription: '在此更新您的照片和个人详细信息。',
maxTokens: '最大token数', maxTokens: '最大token数',
maxTokensMessage: '最大token数是必填项', maxTokensMessage: '最大token数是必填项',
@ -588,6 +589,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
currentPassword: '当前密码', currentPassword: '当前密码',
currentPasswordMessage: '请输入当前密码', currentPasswordMessage: '请输入当前密码',
newPassword: '新密码', newPassword: '新密码',
changePassword: '修改密码',
newPasswordMessage: '请输入新密码', newPasswordMessage: '请输入新密码',
newPasswordDescription: '您的新密码必须超过 8 个字符。', newPasswordDescription: '您的新密码必须超过 8 个字符。',
confirmPassword: '确认新密码', confirmPassword: '确认新密码',

View File

@ -28,52 +28,49 @@ import { Loader2Icon, Pencil, Upload } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
function defineSchema(
function defineSchema(t: TFunction<'translation', string>) { t: TFunction<'translation', string>,
return z showPasswordForm = false,
.object({ ) {
const baseSchema = z.object({
userName: z userName: z
.string() .string()
.min(1, { .min(1, { message: t('usernameMessage') })
message: t('usernameMessage'),
})
.trim(), .trim(),
avatarUrl: z.string().trim(), avatarUrl: z.string().trim(),
timeZone: z timeZone: z
.string() .string()
.trim() .trim()
.min(1, { .min(1, { message: t('timezonePlaceholder') }),
message: t('timezonePlaceholder'),
}),
email: z email: z
.string({ required_error: 'Please select an email to display.' })
.trim()
.regex(/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, {
message: 'Enter a valid email address.',
}),
});
if (showPasswordForm) {
return baseSchema
.extend({
currPasswd: z
.string({ .string({
required_error: 'Please select an email to display.', required_error: t('currentPasswordMessage'),
}) })
.trim() .trim()
.regex( .min(1, { message: t('currentPasswordMessage') }),
/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
{
message: 'Enter a valid email address.',
},
),
currPasswd: z
.string()
.trim()
.min(1, {
message: t('currentPasswordMessage'),
}),
newPasswd: z newPasswd: z
.string() .string({
required_error: t('confirmPasswordMessage'),
})
.trim() .trim()
.min(8, { .min(8, { message: t('confirmPasswordMessage') }),
message: t('confirmPasswordMessage'),
}),
confirmPasswd: z confirmPasswd: z
.string() .string({
required_error: t('newPasswordDescription'),
})
.trim() .trim()
.min(8, { .min(8, { message: t('newPasswordDescription') }),
message: t('newPasswordDescription'),
}),
}) })
.refine((data) => data.newPasswd === data.confirmPasswd, { .refine((data) => data.newPasswd === data.confirmPasswd, {
message: t('confirmPasswordNonMatchMessage'), message: t('confirmPasswordNonMatchMessage'),
@ -81,15 +78,21 @@ function defineSchema(t: TFunction<'translation', string>) {
}); });
} }
return baseSchema;
}
export default function Profile() { export default function Profile() {
const [avatarFile, setAvatarFile] = useState<File | null>(null); const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
const { data: userInfo } = useFetchUserInfo(); const { data: userInfo } = useFetchUserInfo();
const { saveSetting, loading: submitLoading } = useSaveSetting(); const {
saveSetting,
loading: submitLoading,
data: saveUserData,
} = useSaveSetting();
const { t } = useTranslate('setting'); const { t } = useTranslate('setting');
const FormSchema = defineSchema(t); const [showPasswordForm, setShowPasswordForm] = useState(false);
const FormSchema = defineSchema(t, showPasswordForm);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { defaultValues: {
@ -97,10 +100,11 @@ export default function Profile() {
avatarUrl: '', avatarUrl: '',
timeZone: '', timeZone: '',
email: '', email: '',
currPasswd: '', // currPasswd: '',
newPasswd: '', // newPasswd: '',
confirmPasswd: '', // confirmPasswd: '',
}, },
shouldUnregister: true,
}); });
useEffect(() => { useEffect(() => {
@ -108,10 +112,20 @@ export default function Profile() {
form.setValue('email', userInfo?.email); // email form.setValue('email', userInfo?.email); // email
form.setValue('userName', userInfo?.nickname); // nickname form.setValue('userName', userInfo?.nickname); // nickname
form.setValue('timeZone', userInfo?.timezone); // time zone form.setValue('timeZone', userInfo?.timezone); // time zone
form.setValue('currPasswd', ''); // current password // form.setValue('currPasswd', ''); // current password
setAvatarBase64Str(userInfo?.avatar ?? ''); setAvatarBase64Str(userInfo?.avatar ?? '');
}, [userInfo]); }, [userInfo]);
useEffect(() => {
if (saveUserData === 0) {
setShowPasswordForm(false);
form.resetField('currPasswd');
form.resetField('newPasswd');
form.resetField('confirmPasswd');
}
console.log('saveUserData', saveUserData);
}, [saveUserData]);
useEffect(() => { useEffect(() => {
if (avatarFile) { if (avatarFile) {
// make use of img compression transformFile2Base64 // make use of img compression transformFile2Base64
@ -122,24 +136,34 @@ export default function Profile() {
}, [avatarFile]); }, [avatarFile]);
function onSubmit(data: z.infer<typeof FormSchema>) { function onSubmit(data: z.infer<typeof FormSchema>) {
// toast('You submitted the following values', { const payload: Partial<{
// description: ( nickname: string;
// <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4"> password: string;
// <code className="text-white">{JSON.stringify(data, null, 2)}</code> new_password: string;
// </pre> avatar: string;
// ), timezone: string;
// }); }> = {
// console.log('data=', data);
// final submit form
saveSetting({
nickname: data.userName, nickname: data.userName,
password: rsaPsw(data.currPasswd) as string,
new_password: rsaPsw(data.newPasswd) as string,
avatar: avatarBase64Str, avatar: avatarBase64Str,
timezone: data.timeZone, timezone: data.timeZone,
}); };
if (showPasswordForm && 'currPasswd' in data && 'newPasswd' in data) {
payload.password = rsaPsw(data.currPasswd!) as string;
payload.new_password = rsaPsw(data.newPasswd!) as string;
}
saveSetting(payload);
} }
useEffect(() => {
if (showPasswordForm) {
form.register('currPasswd');
form.register('newPasswd');
form.register('confirmPasswd');
} else {
form.unregister(['currPasswd', 'newPasswd', 'confirmPasswd']);
}
}, [showPasswordForm]);
return ( return (
<section className="p-8"> <section className="p-8">
<h1 className="text-3xl font-bold">{t('profile')}</h1> <h1 className="text-3xl font-bold">{t('profile')}</h1>
@ -152,12 +176,13 @@ export default function Profile() {
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="block space-y-6" className="block space-y-6"
> >
{/* Username Field */}
<FormField <FormField
control={form.control} control={form.control}
name="userName" name="userName"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" items-center space-y-0 "> <FormItem className=" items-center space-y-0 ">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<span className="text-red-600">*</span> <span className="text-red-600">*</span>
{t('username')} {t('username')}
@ -170,24 +195,26 @@ export default function Profile() {
/> />
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="w-1/4"></div> <div className="w-1/4"></div>
<FormMessage /> <FormMessage />
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
{/* Avatar Field */}
<FormField <FormField
control={form.control} control={form.control}
name="avatarUrl" name="avatarUrl"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-y-0"> <FormItem className="flex items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
Avatar Avatar
</FormLabel> </FormLabel>
<FormControl className="w-3/4"> <FormControl className="w-3/4">
<> <div className="flex justify-start items-end space-x-2">
<div className="relative group"> <div className="relative group">
{!avatarBase64Str ? ( {!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center"> <div className="w-[64px] h-[64px] grid place-content-center">
@ -198,18 +225,18 @@ export default function Profile() {
</div> </div>
) : ( ) : (
<div className="w-[64px] h-[64px] relative grid place-content-center"> <div className="w-[64px] h-[64px] relative grid place-content-center">
<Avatar className="w-[64px] h-[64px]"> <Avatar className="w-[64px] h-[64px] rounded-md">
<AvatarImage <AvatarImage
className="block" className="block"
src={avatarBase64Str} src={avatarBase64Str}
alt="" alt=""
/> />
<AvatarFallback></AvatarFallback> <AvatarFallback className="rounded-md"></AvatarFallback>
</Avatar> </Avatar>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60"> <div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil <Pencil
size={20} size={16}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block" className="absolute right-1 bottom-1 opacity-50 hidden group-hover:block"
/> />
</div> </div>
</div> </div>
@ -234,22 +261,27 @@ export default function Profile() {
}} }}
/> />
</div> </div>
</> <div className="margin-1 text-muted-foreground">
{t('avatarTip')}
</div>
</div>
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="w-1/4"></div> <div className="w-1/4"></div>
<FormMessage /> <FormMessage />
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
{/* Time Zone Field */}
<FormField <FormField
control={form.control} control={form.control}
name="timeZone" name="timeZone"
render={({ field }) => ( render={({ field }) => (
<FormItem className="items-center space-y-0"> <FormItem className="items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<span className="text-red-600">*</span> <span className="text-red-600">*</span>
{t('timezone')} {t('timezone')}
@ -269,37 +301,35 @@ export default function Profile() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="w-1/4"></div> <div className="w-1/4"></div>
<FormMessage /> <FormMessage />
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
{/* Email Address Field */}
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<div> <div>
<FormItem className="items-center space-y-0"> <FormItem className="items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
{t('email')} {t('email')}
</FormLabel> </FormLabel>
<FormControl className="w-3/4"> <FormControl className="w-3/4">
<Input <>{field.value}</>
placeholder="Alex@gmail.com"
disabled
{...field}
/>
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="w-1/4"></div> <div className="w-1/4"></div>
<FormMessage /> <FormMessage />
</div> </div>
</FormItem> </FormItem>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<p className="w-1/4">&nbsp;</p> <p className="w-1/4">&nbsp;</p>
<p className="text-sm text-muted-foreground whitespace-nowrap w-3/4"> <p className="text-sm text-muted-foreground whitespace-nowrap w-3/4">
{t('emailDescription')} {t('emailDescription')}
@ -308,22 +338,34 @@ export default function Profile() {
</div> </div>
)} )}
/> />
<div className="h-[10px]"></div>
{/* Password Section */}
<div className="pb-6"> <div className="pb-6">
<div className="flex items-center justify-start">
<h1 className="text-3xl font-bold">{t('password')}</h1> <h1 className="text-3xl font-bold">{t('password')}</h1>
<Button
type="button"
className="bg-transparent hover:bg-transparent border text-muted-foreground hover:text-white ml-10"
onClick={() => {
setShowPasswordForm(!showPasswordForm);
}}
>
{t('changePassword')}
</Button>
</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{t('passwordDescription')} {t('passwordDescription')}
</div> </div>
</div> </div>
<div className="h-0 overflow-hidden absolute"> {/* Password Form */}
<input type="password" className=" w-0 height-0 opacity-0" /> {showPasswordForm && (
</div> <>
<FormField <FormField
control={form.control} control={form.control}
name="currPasswd" name="currPasswd"
render={({ field }) => ( render={({ field }) => (
<FormItem className="items-center space-y-0"> <FormItem className="items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
<span className="text-red-600">*</span> <span className="text-red-600">*</span>
{t('currentPassword')} {t('currentPassword')}
@ -332,7 +374,7 @@ export default function Profile() {
<PasswordInput {...field} /> <PasswordInput {...field} />
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="min-w-[170px] max-w-[170px]"></div> <div className="min-w-[170px] max-w-[170px]"></div>
<FormMessage /> <FormMessage />
</div> </div>
@ -344,7 +386,7 @@ export default function Profile() {
name="newPasswd" name="newPasswd"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" items-center space-y-0"> <FormItem className=" items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
<span className="text-red-600">*</span> <span className="text-red-600">*</span>
{t('newPassword')} {t('newPassword')}
@ -353,7 +395,7 @@ export default function Profile() {
<PasswordInput {...field} /> <PasswordInput {...field} />
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="min-w-[170px] max-w-[170px]"></div> <div className="min-w-[170px] max-w-[170px]"></div>
<FormMessage /> <FormMessage />
</div> </div>
@ -365,7 +407,7 @@ export default function Profile() {
name="confirmPasswd" name="confirmPasswd"
render={({ field }) => ( render={({ field }) => (
<FormItem className=" items-center space-y-0"> <FormItem className=" items-center space-y-0">
<div className="flex w-[600px]"> <div className="flex w-[640px]">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
<span className="text-red-600">*</span> <span className="text-red-600">*</span>
{t('confirmPassword')} {t('confirmPassword')}
@ -385,15 +427,21 @@ export default function Profile() {
/> />
</FormControl> </FormControl>
</div> </div>
<div className="flex w-[600px] pt-1"> <div className="flex w-[640px] pt-1">
<div className="min-w-[170px] max-w-[170px]">&nbsp;</div> <div className="min-w-[170px] max-w-[170px]">
&nbsp;
</div>
<FormMessage /> <FormMessage />
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
<div className="w-[600px] text-right space-x-4"> </>
<Button variant="secondary">{t('cancel')}</Button> )}
<div className="w-[640px] text-right space-x-4">
<Button type="reset" variant="secondary">
{t('cancel')}
</Button>
<Button type="submit" disabled={submitLoading}> <Button type="submit" disabled={submitLoading}>
{submitLoading && <Loader2Icon className="animate-spin" />} {submitLoading && <Loader2Icon className="animate-spin" />}
{t('save', { keyPrefix: 'common' })} {t('save', { keyPrefix: 'common' })}