Fix: Optimized the login page and fixed some known issues. #9869 (#10514)

### What problem does this PR solve?

Fix: Optimized the login page and fixed some known issues. #9869

- Added the FlipCard3D component to implement a 3D flip effect on the
login/registration forms.
- Adjusted the Spotlight component to support custom positioning and
color configurations.
- Updated the route to point to the new login page /login-next.
- Added a cancel interface to the auto-generate function.
- Fixed scroll bar issues in PDF preview.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-10-13 15:31:36 +08:00
committed by GitHub
parent 9c53b3336a
commit 77481ab3ab
17 changed files with 347 additions and 265 deletions

View File

@ -1,7 +1,7 @@
.documentContainer {
width: 100%;
// height: calc(100vh - 284px);
height: calc(100vh - 170px);
height: calc(100vh - 180px);
position: relative;
:global(.PdfHighlighter) {
overflow-x: hidden;

View File

@ -1,7 +1,7 @@
.documentContainer {
width: 100%;
// height: calc(100vh - 284px);
height: calc(100vh - 170px);
height: calc(100vh - 180px);
position: relative;
:global(.PdfHighlighter) {
overflow-x: hidden;

View File

@ -4,7 +4,6 @@ import CSVFileViewer from './csv-preview';
import { DocPreviewer } from './doc-preview';
import { ExcelCsvPreviewer } from './excel-preview';
import { ImagePreviewer } from './image-preview';
import styles from './index.less';
import PdfPreviewer, { IProps } from './pdf-preview';
import { PptPreviewer } from './ppt-preview';
import { TxtPreviewer } from './txt-preview';
@ -24,7 +23,7 @@ const Preview = ({
return (
<>
{fileType === 'pdf' && highlights && setWidthAndHeight && (
<section className={styles.documentPreview}>
<section>
<PdfPreviewer
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}

View File

@ -24,6 +24,8 @@ interface StatCardProps {
interface CardFooterProcessProps {
success: number;
failed: number;
successTip?: string;
failedTip?: string;
}
const StatCard: FC<StatCardProps> = ({
@ -56,7 +58,9 @@ const StatCard: FC<StatCardProps> = ({
const CardFooterProcess: FC<CardFooterProcessProps> = ({
success = 0,
successTip,
failed = 0,
failedTip,
}) => {
const { t } = useTranslation();
return (
@ -65,8 +69,13 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
<div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5">
<div className="flex items-center rounded-lg gap-1">
<div className="w-2 h-2 rounded-full bg-state-success "></div>
<div className="font-normal text-text-secondary text-xs">
<div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.success')}
{successTip && (
<AntToolTip title={successTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div>
</div>
<div>{success || 0}</div>
@ -74,8 +83,13 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
<div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2">
<div className="flex items-center rounded-lg gap-1">
<div className="w-2 h-2 rounded-full bg-state-error"></div>
<div className="font-normal text-text-secondary text-xs">
<div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.failed')}
{failedTip && (
<AntToolTip title={failedTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div>
</div>
<div>{failed || 0}</div>
@ -259,7 +273,9 @@ const FileLogsPage: FC = () => {
>
<CardFooterProcess
success={topAllData.downloads.success}
successTip={t('datasetOverview.downloadSuccessTip')}
failed={topAllData.downloads.failed}
failedTip={t('datasetOverview.downloadFailedTip')}
/>
</StatCard>
<StatCard
@ -276,7 +292,9 @@ const FileLogsPage: FC = () => {
>
<CardFooterProcess
success={topAllData.processing.success}
successTip={t('datasetOverview.processingSuccessTip')}
failed={topAllData.processing.failed}
failedTip={t('datasetOverview.processingFailedTip')}
/>
</StatCard>
</div>

View File

@ -144,7 +144,12 @@ export function ParseTypeItem({ line = 2 }: { line?: number }) {
>
<FormControl>
<Radio.Group {...field}>
<div className="w-1/2 flex gap-2 justify-between text-muted-foreground">
<div
className={cn(
'flex gap-2 justify-between text-muted-foreground',
line === 1 ? 'w-1/2' : 'w-3/4',
)}
>
<Radio value={1}>{t('builtIn')}</Radio>
<Radio value={2}>{t('manualSetup')}</Radio>
</div>

View File

@ -6,7 +6,7 @@ import { t } from 'i18next';
import { useEffect, useState } from 'react';
import { useParams } from 'umi';
import { ProcessingType } from '../../dataset-overview/dataset-common';
import { GenerateType } from './generate';
import { GenerateType, GenerateTypeMap } from './generate';
export const generateStatus = {
running: 'running',
completed: 'completed',
@ -103,9 +103,28 @@ export const useTraceGenerate = ({ open }: { open: boolean }) => {
raptorRunloading,
};
};
export const useUnBindTask = () => {
const { id } = useParams();
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({ type }: { type: ProcessingType }) => {
const { data } = await deletePipelineTask({ kb_id: id as string, type });
if (data.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return data;
},
});
return { handleUnbindTask };
};
export const useDatasetGenerate = () => {
const queryClient = useQueryClient();
const { id } = useParams();
const { handleUnbindTask } = useUnBindTask();
const {
data,
isPending: loading,
@ -143,8 +162,12 @@ export const useDatasetGenerate = () => {
type: GenerateType;
}) => {
const { data } = await agentService.cancelDataflow(task_id);
if (data.code === 0) {
message.success(t('message.operated'));
const unbindData = await handleUnbindTask({
type: GenerateTypeMap[type as GenerateType],
});
if (data.code === 0 && unbindData.code === 0) {
// message.success(t('message.operated'));
queryClient.invalidateQueries({
queryKey: [type],
});
@ -154,21 +177,3 @@ export const useDatasetGenerate = () => {
});
return { runGenerate: mutateAsync, pauseGenerate, data, loading };
};
export const useUnBindTask = () => {
const { id } = useParams();
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({ type }: { type: ProcessingType }) => {
const { data } = await deletePipelineTask({ kb_id: id as string, type });
if (data.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return data;
},
});
return { handleUnbindTask };
};

View File

@ -10,7 +10,7 @@ import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date';
import { isEmpty } from 'lodash';
import { Banknote, DatabaseZap, FileSearch2, FolderOpen } from 'lucide-react';
import { Banknote, FileSearch2, FolderOpen, Logs } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks';
@ -40,7 +40,7 @@ export function SideBar({ refreshCount }: PropType) {
key: Routes.DatasetTesting,
},
{
icon: <DatabaseZap className="size-4" />,
icon: <Logs className="size-4" />,
label: t(`knowledgeDetails.overview`),
key: Routes.DataSetOverview,
},

View File

@ -17,7 +17,6 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { FormLayout } from '@/constants/form';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
@ -103,7 +102,6 @@ export function InputForm({ onOk }: IModalProps<any>) {
form.setValue('pipeline_id', '');
}
}, [parseType, form]);
const { navigateToAgents } = useNavigatePage();
return (
<Form {...form}>
@ -157,7 +155,7 @@ export function DatasetCreatingDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px] focus-visible:!outline-none">
<DialogContent className="sm:max-w-[425px] focus-visible:!outline-none flex flex-col">
<DialogHeader>
<DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle>
</DialogHeader>

View File

@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import './index.less';
type IProps = {
children: React.ReactNode;
isLoginPage: boolean;
};
const FlipCard3D = (props: IProps) => {
const { children, isLoginPage } = props;
const [isFlipped, setIsFlipped] = useState(false);
useEffect(() => {
console.log('title', isLoginPage);
if (isLoginPage) {
setIsFlipped(false);
} else {
setIsFlipped(true);
}
}, [isLoginPage]);
return (
<div className="relative w-full h-full perspective-1000">
<div
className={`relative w-full h-full transition-transform transform-style-3d ${isFlipped ? 'rotate-y-180' : ''}`}
>
{/* Front Face */}
<div className="absolute inset-0 flex items-center justify-center bg-blue-500 text-white rounded-xl backface-hidden">
{children}
</div>
{/* Back Face */}
<div className="absolute inset-0 flex items-center justify-center bg-green-500 text-white rounded-xl backface-hidden rotate-y-180">
{children}
</div>
</div>
</div>
);
};
export default FlipCard3D;

View File

@ -40,3 +40,17 @@
}
//////////////////////////////////////////////////////////////////////////
.perspective-1000 {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
transition-duration: 0.4s;
}
.backface-hidden {
backface-visibility: hidden;
}
.rotate-y-180 {
transform: rotateY(180deg);
}

View File

@ -25,11 +25,12 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { Eye, EyeOff } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { BgSvg } from './bg';
import FlipCard3D from './card';
import './index.less';
import { SpotlightTopLeft, SpotlightTopRight } from './spotlight-top';
const Login = () => {
const [title, setTitle] = useState('login');
@ -40,6 +41,8 @@ const Login = () => {
const { login: loginWithChannel, loading: loginWithChannelLoading } =
useLoginWithChannel();
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const [isLoginPage, setIsLoginPage] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const loading =
signLoading ||
registerLoading ||
@ -60,10 +63,15 @@ const Login = () => {
};
const changeTitle = () => {
setIsLoginPage(title !== 'login');
if (title === 'login' && !registerEnabled) {
return;
}
setTitle((title) => (title === 'login' ? 'register' : 'login'));
setTimeout(() => {
setTitle(title === 'login' ? 'register' : 'login');
}, 200);
// setTitle((title) => (title === 'login' ? 'register' : 'login'));
};
const FormSchema = z
@ -129,177 +137,214 @@ const Login = () => {
return (
<div className="min-h-screen relative overflow-hidden">
<BgSvg />
<Spotlight opcity={0.6} coverage={60} />
<SpotlightTopLeft opcity={0.2} coverage={20} />
<SpotlightTopRight opcity={0.2} coverage={20} />
<Spotlight opcity={0.4} coverage={60} color={'rgb(128, 255, 248)'} />
<Spotlight
opcity={0.3}
coverage={12}
X={'10%'}
Y={'-10%'}
color={'rgb(128, 255, 248)'}
/>
<Spotlight
opcity={0.3}
coverage={12}
X={'90%'}
Y={'-10%'}
color={'rgb(128, 255, 248)'}
/>
{/* <SpotlightTopRight opcity={0.7} coverage={10} /> */}
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
<div className="flex items-center mb-4 w-full pl-10 pt-10 ">
<div className="w-10 h-10 rounded-lg border flex items-center justify-center mr-3">
<div className="w-12 h-12 p-2 rounded-lg border-2 border-border flex items-center justify-center mr-3">
<img
src={'/logo.svg'}
alt="logo"
className="size-10 mr-[12] cursor-pointer"
className="size-8 mr-[12] cursor-pointer"
/>
</div>
<span className="text-xl font-bold self-end">RAGFlow</span>
<div className="text-xl font-bold self-center">RAGFlow</div>
</div>
<h1 className="text-2xl font-bold text-center mb-2">
A Leading RAG engine with Agent for superior LLM context.
</h1>
<h1 className="text-2xl font-bold text-center mb-2">{t('title')}</h1>
<div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 border border-accent-primary rounded-full hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
Let's get started
{t('start')}
</div>
</div>
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8">
{/* Logo and Header */}
{/* Login Form */}
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login'
? 'Sign in to Your Account'
: 'Create an Account'}
</h2>
</div>
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-border-button">
<Form {...form}>
<form
className="space-y-6"
onSubmit={form.handleSubmit((data) => onCheck(data))}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('nicknamePlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'login' && (
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
<FlipCard3D isLoginPage={isLoginPage}>
<div className="flex flex-col items-center justify-center w-full">
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
</h2>
</div>
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-8 pr-8 pb-2 border border-border-button ">
<Form {...form}>
<form
className="flex flex-col gap-6 text-text-primary"
onSubmit={form.handleSubmit((data) => onCheck(data))}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
<FormLabel>{t('rememberMe')}</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('nicknamePlaceholder')}
autoComplete="username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
/>
)}
<ButtonLoading
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
placeholder={t('passwordPlaceholder')}
autoComplete={
title === 'login'
? 'current-password'
: 'new-password'
}
{...field}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-500" />
) : (
<Eye className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'login' && (
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
<FormLabel>{t('rememberMe')}</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ButtonLoading
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
</form>
</Form>
{title === 'login' && registerEnabled && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
{t('signUp')}
</Button>
))}
</p>
</div>
)}
{title === 'register' && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</form>
</Form>
{title === 'login' && registerEnabled && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('signUp')}
</Button>
</p>
</div>
)}
{title === 'register' && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</div>
</div>
</FlipCard3D>
</div>
</div>
);

View File

@ -1,70 +0,0 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import React from 'react';
interface SpotlightProps {
className?: string;
opcity?: number;
coverage?: number;
}
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopLeft: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 10% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopRight: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 90% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};