refactor(ui): adjust dataset page styles (#13452)

### What problem does this PR solve?

- Adjust UI styles in **Dataset** pages.
- Adjust several shared components styles
- Modify files and directory structure in `src/layouts`

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-06 21:13:14 +08:00
committed by GitHub
parent 7166a7e50e
commit 094eae3cf5
44 changed files with 589 additions and 543 deletions

View File

@ -1,8 +1,7 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { BrushCleaning } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { ReactNode, useId } from 'react';
import { useTranslation } from 'react-i18next';
import {
ConfirmDeleteDialog,
@ -30,58 +29,69 @@ export function BulkOperateBar({
className,
unit,
}: BulkOperateBarProps) {
const isDeleteItem = useCallback((id: string) => {
return id === 'delete';
}, []);
const { t } = useTranslation();
const ariaDescriptionId = useId();
return (
<Card className={cn('mb-4', className)}>
<CardContent className="p-1 pl-5 flex items-center gap-6">
<section className="text-text-sub-title-invert flex items-center gap-2">
<span>
{t('common.selected')}: {count}{' '}
{unit ?? t('knowledgeDetails.files')}
</span>
<BrushCleaning className="size-3" />
</section>
<Separator orientation={'vertical'} className="h-3"></Separator>
<Card
className={className}
role="menu"
aria-label={t('common.bulkOperate')}
aria-describedby={ariaDescriptionId}
>
<CardContent className="ps-5 pe-1 py-1 flex items-center gap-6">
<p
id={ariaDescriptionId}
className="text-sm text-text-secondary flex items-center gap-2"
>
{t('common.selected')}: {count} {unit ?? t('knowledgeDetails.files')}
<BrushCleaning className="size-[1em]" />
</p>
<Separator orientation={'vertical'} className="h-[1em]"></Separator>
<ul className="flex gap-2">
{list.map((x) => (
<li
key={x.id}
className={cn({ ['text-state-error']: isDeleteItem(x.id) })}
>
<ConfirmDeleteDialog
hidden={!isDeleteItem(x.id)}
onOk={x.onClick}
title={
unit
? t('common.delete') + ' ' + unit
: t('deleteModal.delFiles')
}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
></ConfirmDeleteDialogNode>
),
}}
{list.map((x) => {
const isDeleteItem = x.id === 'delete';
const buttonEl = (
<Button
variant={isDeleteItem ? 'danger' : 'outline'}
onClick={isDeleteItem ? () => {} : x.onClick}
role="menuitem"
>
<Button
variant={!isDeleteItem(x.id) ? 'ghost' : 'delete'}
onClick={isDeleteItem(x.id) ? () => {} : x.onClick}
className={cn({
['text-state-error border border-state-error bg-state-error/5']:
isDeleteItem(x.id),
})}
>
{x.icon} {x.label}
</Button>
</ConfirmDeleteDialog>
</li>
))}
{x.icon} {x.label}
</Button>
);
return (
<li key={x.id}>
{isDeleteItem ? (
<ConfirmDeleteDialog
key="deleteModal"
onOk={x.onClick}
title={
unit
? t('common.delete') + ' ' + unit
: t('deleteModal.delFiles')
}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
/>
),
}}
>
{buttonEl}
</ConfirmDeleteDialog>
) : (
buttonEl
)}
</li>
);
})}
</ul>
</CardContent>
</Card>

View File

@ -10,7 +10,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Plus, X } from 'lucide-react';
import { LucidePlus, LucideTrash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Separator } from '../ui/separator';
@ -51,7 +51,9 @@ export function DynamicPageRange() {
</FormItem>
)}
/>
<Separator className="w-3 "></Separator>
<FormField
control={form.control}
name={`parser_config.pages.${index}.to`}
@ -70,19 +72,27 @@ export function DynamicPageRange() {
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
<Button
className="ml-4"
size="icon"
variant="outline"
onClick={() => remove(index)}
>
<LucideTrash2 />
</Button>
</div>
);
})}
<Button
onClick={() => append({ from: 1, to: 100 })}
className="mt-4 border-dashed w-full"
variant={'outline'}
block
className="mt-4"
variant="dashed"
type="button"
>
<Plus />
<LucidePlus />
{t('knowledgeDetails.addPage')}
</Button>
</div>

View File

@ -42,7 +42,6 @@ import { DataFlowSelect } from '../data-pipeline-select';
import { DelimiterFormField } from '../delimiter-form-field';
import { EntityTypesFormField } from '../entity-types-form-field';
import { ExcelToHtmlFormField } from '../excel-to-html-form-field';
import { FormContainer } from '../form-container';
import { LayoutRecognizeFormField } from '../layout-recognize-form-field';
import { MaxTokenNumberFormField } from '../max-token-number-from-field';
import { MinerUOptionsFormField } from '../mineru-options-form-field';
@ -293,22 +292,16 @@ export function ChunkMethodDialog({
<DialogHeader>
<DialogTitle>{t('knowledgeDetails.chunkMethod')}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 max-h-[70vh] overflow-auto"
className="space-y-6 max-h-[70vh] overflow-auto -mx-6 px-10 py-5"
id={FormId}
>
<FormContainer>
<div className="space-y-6">
<ParseTypeItem />
{parseType === 1 && <ChunkMethodItem></ChunkMethodItem>}
{parseType === 2 && (
<DataFlowSelect
isMult={false}
// toDataPipeline={navigateToAgents}
formFieldName="pipeline_id"
/>
)}
{parseType === 1 && <ChunkMethodItem />}
{/* <FormField
control={form.control}
@ -326,9 +319,9 @@ export function ChunkMethodDialog({
</FormItem>
)}
/> */}
{showPages && parseType === 1 && (
<DynamicPageRange></DynamicPageRange>
)}
{showPages && parseType === 1 && <DynamicPageRange />}
{showPages && parseType === 1 && layoutRecognize && (
<FormField
control={form.control}
@ -341,31 +334,25 @@ export function ChunkMethodDialog({
{t('knowledgeDetails.taskPageSize')}
</FormLabel>
<FormControl>
<Input
{...field}
type={'number'}
min={1}
max={128}
></Input>
<Input {...field} type={'number'} min={1} max={128} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</FormContainer>
</div>
{parseType === 1 && (
<>
<FormContainer
show={showOne || showMaxTokenNumber}
className="space-y-3"
>
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{showOne && (
<>
<LayoutRecognizeFormField showMineruOptions={false} />
{isMineruSelected && <MinerUOptionsFormField />}
</>
)}
{showMaxTokenNumber && (
<>
<MaxTokenNumberFormField
@ -374,26 +361,21 @@ export function ChunkMethodDialog({
? 8192 * 2
: 2048
}
></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
/>
<DelimiterFormField />
<ChildrenDelimiterForm />
</>
)}
</FormContainer>
<FormContainer
show={
isMineruSelected ||
showAutoKeywords(selectedTag) ||
showExcelToHtml
}
className="space-y-3"
>
</div>
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{selectedTag === DocumentParserType.Naive && (
<>
<EnableTocToggle />
<ImageContextWindow />
</>
)}
{showAutoKeywords(selectedTag) && (
<>
<AutoMetadata
@ -404,28 +386,39 @@ export function ChunkMethodDialog({
<AutoQuestionsFormField></AutoQuestionsFormField>
</>
)}
{showExcelToHtml && (
<ExcelToHtmlFormField></ExcelToHtmlFormField>
)}
</FormContainer>
</div>
{/* {showRaptorParseConfiguration(
selectedTag as DocumentParserType,
) && (
<FormContainer>
<RaptorFormFields></RaptorFormFields>
</FormContainer>
)} */}
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
useGraphRag && (
selectedTag as DocumentParserType,
) && (
<FormContainer>
<UseGraphRagFormField></UseGraphRagFormField>
<RaptorFormFields></RaptorFormFields>
</FormContainer>
)} */}
{showEntityTypes && (
<EntityTypesFormField></EntityTypesFormField>
)}
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
useGraphRag && (
<FormContainer>
<UseGraphRagFormField></UseGraphRagFormField>
</FormContainer>
)} */}
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{showEntityTypes && <EntityTypesFormField />}
</div>
</>
)}
<div className="space-y-6 empty:hidden">
{parseType === 2 && (
<DataFlowSelect
isMult={false}
// toDataPipeline={navigateToAgents}
formFieldName="pipeline_id"
/>
)}
</div>
</form>
</Form>
<DialogFooter>

View File

@ -53,22 +53,24 @@ const Empty = (props: EmptyProps) => {
export default Empty;
export const EmptyCard = (props: EmptyCardProps) => {
const { icon, className, children, title, description, style } = props;
const { icon, className, children, title, description, style, ...restProps } =
props;
return (
<div
<article
className={cn(
'flex flex-col gap-3 items-start justify-start border border-dashed border-border-button rounded-md p-5 w-fit',
className,
)}
style={style}
{...restProps}
>
{icon}
{title && <div className="text-text-primary text-base">{title}</div>}
{title && <div className="text-text-primary text-sm">{title}</div>}
{description && (
<div className="text-text-secondary text-sm">{description}</div>
<p className="text-text-secondary text-sm">{description}</p>
)}
{children}
</div>
</article>
);
};
@ -104,11 +106,14 @@ export const EmptyAppCard = (props: {
break;
}
return (
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
<div>
<EmptyCard
onClick={isSearch ? undefined : props.onClick}
data-testid={testId}
tabIndex={isSearch ? undefined : 0}
icon={showIcon ? cardData.icon : undefined}
title={isSearch ? notFound : title}
className={cn('cursor-pointer ', className)}
className={cn('cursor-pointer', className)}
style={style}
// description={EmptyCardData[type].description}
>

View File

@ -15,4 +15,4 @@ export type EmptyCardProps = {
title?: string;
description?: string;
style?: React.CSSProperties;
};
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>;

View File

@ -1,5 +1,5 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Card, CardContent } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { formatDate } from '@/utils/date';
import { ReactNode } from 'react';
@ -26,48 +26,61 @@ export function HomeCard({
}: IProps) {
return (
<Card
as="article"
data-testid={testId}
data-agent-name={data.name}
onClick={() => {
// navigateToSearch(data?.id);
onClick?.();
}}
tabIndex={0}
className="px-2.5 py-4 flex gap-2 items-start group h-full w-full hover:shadow-md"
>
<CardContent className="p-4 flex gap-2 items-start group h-full w-full hover:shadow-md">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"
avatar={data.avatar}
name={data.name}
/>
</div>
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between">
<section className="flex flex-1 min-w-0 gap-1 items-center">
<div
className="text-base font-bold leading-snug truncate"
data-testid="agent-name"
>
{data.name}
</div>
{icon}
</section>
{moreDropdown}
</section>
<div>
<RAGFlowAvatar
className="w-[32px] h-[32px]"
avatar={data.avatar}
name={data.name}
/>
</div>
<section className="flex flex-col gap-1 mt-1">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
<div className="flex justify-between items-center">
<p className="text-sm opacity-80 whitespace-nowrap">
{formatDate(data.update_time)}
</p>
{sharedBadge}
</div>
</section>
</div>
</CardContent>
<div className="flex-1 w-0">
<CardHeader
as="header"
className="p-0 flex-1 flex flex-row items-center gap-2 space-y-0"
>
<CardTitle className="flex-1 inline-flex w-0 me-auto">
<h3
className="flex-1 truncate text-base font-bold leading-snug"
data-testid="agent-name"
>
{data.name}
</h3>
{icon}
</CardTitle>
<div>{moreDropdown}</div>
</CardHeader>
<CardContent className="p-0">
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between"></section>
<section className="flex flex-col gap-1 mt-1">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
<div className="flex justify-between items-center">
<p className="text-sm opacity-80 whitespace-nowrap">
{formatDate(data.update_time)}
</p>
{sharedBadge}
</div>
</section>
</div>
</CardContent>
</div>
</Card>
);
}

View File

@ -91,7 +91,7 @@ export default function ListFilterBar({
}, [value]);
return (
<div className={cn('flex justify-between mb-5 items-center', className)}>
<div className={cn('flex justify-between items-center', className)}>
<div className="text-2xl font-semibold flex items-center gap-2.5">
{typeof icon === 'string' ? (
// <IconFont name={icon} className="size-6"></IconFont>
@ -101,7 +101,8 @@ export default function ListFilterBar({
)}
{leftPanel || title}
</div>
<div className="flex gap-5 items-center">
<div className="flex gap-4 items-center" role="toolbar">
{preChildren}
{showFilter && (
<FilterPopover
@ -119,6 +120,7 @@ export default function ListFilterBar({
value={searchString}
onChange={onSearchChange}
className="w-32"
role="searchbox"
></SearchInput>
{children}
</div>

View File

@ -9,10 +9,10 @@ export const MoreButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
<Button
ref={ref}
variant="ghost"
size={size || 'icon'}
size={size || 'icon-xs'}
className={cn(
'invisible size-3.5 bg-transparent group-hover:bg-transparent',
'group-focus-within:visible group-hover:visible aria-expanded:visible',
'opacity-0 size-3.5 transition-all bg-transparent group-hover:bg-transparent',
'group-focus-within:opacity-100 group-hover:opacity-100 aria-expanded:opacity-100',
className,
)}
{...props}

View File

@ -1,6 +1,6 @@
import { cn } from '@/lib/utils';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { forwardRef, memo, useEffect, useRef, useState } from 'react';
import { forwardRef, memo, useMemo } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
const PREDEFINED_COLORS = [
@ -26,6 +26,15 @@ const getStringHash = (str: string): number => {
return Math.abs(hash);
};
const getInitials = (name?: string) => {
if (typeof name !== 'string' || !name) return '';
const parts = name?.trim().split(/\s+/);
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
return parts[0][0].toUpperCase();
};
const getColorForName = (name: string): { from: string; to: string } => {
const hash = getStringHash(name);
const index = hash % PREDEFINED_COLORS.length;
@ -42,50 +51,15 @@ export const RAGFlowAvatar = memo(
}
>(({ name, avatar, isPerson = false, className, ...props }, ref) => {
// Generate initial letter logic
const getInitials = (name?: string) => {
if (typeof name !== 'string' || !name) return '';
const parts = name?.trim().split(/\s+/);
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
return parts[0][0].toUpperCase();
};
const initials = getInitials(name);
const { from, to } = name
? getColorForName(name)
: { from: 'hsl(0, 0%, 30%)', to: 'hsl(0, 0%, 80%)' };
const fallbackRef = useRef<HTMLElement>(null);
const [fontSize, setFontSize] = useState('0.875rem');
// Calculate font size
const calculateFontSize = () => {
if (fallbackRef.current) {
const containerWidth = fallbackRef.current.offsetWidth;
const newSize = containerWidth * 0.6;
setFontSize(`${newSize}px`);
}
};
useEffect(() => {
calculateFontSize();
if (fallbackRef.current) {
const resizeObserver = new ResizeObserver(() => {
calculateFontSize();
});
resizeObserver.observe(fallbackRef.current);
return () => {
if (fallbackRef.current) {
resizeObserver.unobserve(fallbackRef.current);
}
resizeObserver.disconnect();
};
}
}, []);
const { initials, from, to } = useMemo(
() => ({
initials: getInitials(name),
from: 'hsl(0, 0%, 30%)',
to: 'hsl(0, 0%, 80%)',
...(name ? getColorForName(name) : {}),
}),
[name],
);
return (
<Avatar
@ -95,22 +69,27 @@ export const RAGFlowAvatar = memo(
>
<AvatarImage src={avatar} />
<AvatarFallback
ref={(node) => {
fallbackRef.current = node;
calculateFontSize();
}}
className={cn(
'bg-gradient-to-b',
'flex items-center justify-center',
'text-white ',
{ 'rounded-md': !isPerson },
)}
className="flex items-center justify-center bg-gradient-to-b text-white"
style={{
backgroundImage: `linear-gradient(to bottom, ${from}, ${to})`,
fontSize: fontSize,
}}
role="presentation"
aria-hidden="true"
>
{initials}
<svg
className="size-full block text-current select-none"
viewBox={`${-(50 + 22.5 * (initials.length - 1))} -50 ${100 + 45 * (initials.length - 1)} 100`}
preserveAspectRatio="xMinYMid meet"
>
<text
fontSize={55}
fill="currentColor"
textAnchor="middle"
dominantBaseline="central"
>
{initials}
</text>
</svg>
</AvatarFallback>
</Avatar>
);

View File

@ -143,8 +143,8 @@ export const HomeIcon = ({
imgClass,
}: {
name: string;
height?: string;
width?: string;
height?: string | number;
width?: string | number;
imgClass?: string;
}) => {
const isDark = useIsDarkTheme();

View File

@ -1,48 +0,0 @@
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 (
<Button
type="button"
onClick={() => handleThemeChange(!isDarkTheme)}
className="relative inline-flex h-6 w-14 items-center rounded-full transition-colors p-0.5 border-none focus:border-none bg-bg-card hover:bg-bg-card"
// aria-label={isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
>
<div className="inline-flex h-full w-full items-center">
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-disabled h-4 w-5'
: ' text-text-primary bg-bg-base h-full w-8 flex-1'
}`}
>
<Sun />
</div>
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-primary bg-bg-base h-full w-8 flex-1'
: 'text-text-disabled h-4 w-5'
}`}
>
<Moon />
</div>
</div>
</Button>
);
};
export default ThemeToggle;

View File

@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-bg-member',
'flex h-full w-full items-center justify-center bg-bg-member',
className,
)}
{...props}

View File

@ -8,7 +8,7 @@ import { Link, LinkProps } from 'react-router';
const buttonVariants = cva(
cn(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm transition-colors outline-0',
'disabled:pointer-events-none disabled:opacity-50 rounded border-0.5 border-transparent',
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0',
),
@ -83,9 +83,13 @@ const buttonVariants = cva(
`,
link: 'text-primary underline-offset-4 hover:underline',
// Static
// Button has no interaction transitions
static: '',
},
size: {
auto: 'h-full px-1',
auto: '',
xl: 'h-12 rounded-xl px-5',
lg: 'h-10 rounded-lg px-4',
@ -107,6 +111,8 @@ const buttonVariants = cva(
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;
export type ButtonProps<IsAnchor extends boolean = false> = {
asChild?: boolean;
asLink?: boolean;
@ -114,7 +120,7 @@ export type ButtonProps<IsAnchor extends boolean = false> = {
block?: boolean;
disabled?: boolean;
dot?: boolean;
} & VariantProps<typeof buttonVariants> &
} & ButtonVariants &
(IsAnchor extends true
? LinkProps
: React.ButtonHTMLAttributes<HTMLButtonElement>);
@ -144,7 +150,7 @@ const Button = React.forwardRef(
<Comp
className={cn(
buttonVariants({ variant, size, className }),
{ 'block w-full': block },
{ 'w-full': block },
{ relative: dot },
)}
// @ts-ignore

View File

@ -3,10 +3,10 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
HTMLElement,
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn(
'rounded-lg border-border-button border-0.5 shadow-sm bg-bg-input transition-shadow',
@ -19,9 +19,9 @@ Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
@ -32,7 +32,7 @@ CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLElement,
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
>(({ className, as: As = 'div', ...props }, ref) => (
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('text-2xl leading-normal font-medium', className)}
@ -43,9 +43,9 @@ CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('text-sm text-text-secondary', className)}
{...props}
@ -55,9 +55,9 @@ CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('p-6 pt-0 transition-shadow', className)}
{...props}
@ -67,9 +67,9 @@ CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}

View File

@ -38,7 +38,10 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4',
'outline-none outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%]',
// TODO: to keep scrollbar perfectly aligned to header bottom and/or footer top,
// 'gap-4' should be removed, then bring your own body container with padding-y instead.
'gap-4',
'border-0.5 border-border-button bg-bg-base p-6 shadow-lg duration-200 sm:rounded-lg',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
@ -88,7 +91,7 @@ const DialogFooter = ({
<div
className={cn(
// '-mx-6 -mb-6 px-12 pt-4 pb-8',
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
'-mx-6 -mb-6 p-6 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
className,
)}
{...props}

View File

@ -18,7 +18,13 @@ const HoverCardContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]',
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap',
'rounded-md border bg-bg-base p-4 text-text-primary shadow-md outline-none',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'origin-[--radix-hover-card-content-transform-origin]',
className,
)}
{...props}

View File

@ -2,6 +2,7 @@ import { cn } from '@/lib/utils';
import React, { useContext, useState } from 'react';
const RadioGroupContext = React.createContext<{
name?: string;
value: string | number;
onChange: (value: string | number) => void;
disabled?: boolean;
@ -50,30 +51,46 @@ function Radio({
return (
<label
className={cn(
'flex items-center cursor-pointer gap-2 text-sm',
'group/radio relative flex items-center cursor-pointer gap-2 text-sm',
mergedDisabled && 'cursor-not-allowed opacity-50',
)}
>
<span
<input
type="radio"
name={groupContext?.name}
value={value}
checked={isChecked}
onClick={handleClick}
disabled={mergedDisabled}
data-testid={testId}
className="peer absolute size-[1px] opacity-0"
/>
<div
className={cn(
'flex h-4 w-4 items-center justify-center rounded-full border border-border transition-colors',
'peer outline-none focus-visible:border-border-button',
'flex h-4 w-4 items-center justify-center rounded-full border border-border-button transition-colors',
'group-hover/radio:border-border-default hover:border-border-default',
'peer-focus:border-primary',
isChecked && 'border-primary bg-primary/10',
mergedDisabled && 'border-muted',
)}
onClick={handleClick}
data-testid={testId}
>
{isChecked && (
<div className="h-3 w-3 fill-primary text-primary bg-text-primary rounded-full" />
)}
</span>
<div
className={cn(
'h-2 w-2 fill-primary text-primary bg-text-primary rounded-full opacity-0 scale-0 transition-all',
isChecked && 'opacity-100 scale-100',
)}
/>
</div>
{children && <span className="text-foreground">{children}</span>}
</label>
);
}
type RadioGroupProps = {
name?: string;
value?: string | number;
defaultValue?: string | number;
onChange?: (value: string | number) => void;
@ -86,6 +103,7 @@ type RadioGroupProps = {
const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
(
{
name,
value,
defaultValue,
onChange,
@ -116,6 +134,7 @@ const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
return (
<RadioGroupContext.Provider
value={{
name,
value: mergedValue,
onChange: handleChange,
disabled,

View File

@ -1,5 +1,6 @@
import { cn } from '@/lib/utils';
import * as React from 'react';
import { Button, ButtonVariants } from './button';
export declare type SegmentedValue = string | number;
export declare type SegmentedRawOption = SegmentedValue;
export interface SegmentedLabeledOption {
@ -56,7 +57,7 @@ export interface SegmentedProps extends Omit<
itemClassName?: string;
rounded?: keyof typeof segmentedVariants.round;
sizeType?: keyof typeof segmentedVariants.size;
buttonSize?: keyof typeof segmentedVariants.buttonSize;
buttonSize?: ButtonVariants['size'];
}
export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
@ -101,12 +102,12 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
const actualValue = isObject ? option.value : option;
return (
<div
<Button
key={actualValue}
type="button"
size={buttonSize}
variant="static"
className={cn(
'inline-flex items-center text-base font-normal cursor-pointer',
segmentedVariants.round[rounded],
segmentedVariants.buttonSize[buttonSize],
{
'text-text-primary bg-bg-base': selectedValue === actualValue,
},
@ -118,7 +119,7 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
onClick={() => handleOnChange(actualValue)}
>
{isObject ? option.label : option}
</div>
</Button>
);
})}
</div>

View File

@ -12,7 +12,7 @@ export function PageContainer({
}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) {
return (
<div
className={cn('size-full px-10 py-3 overflow-auto', className)}
className={cn('size-full px-5 py-3 overflow-auto', className)}
{...props}
/>
);

View File

@ -1,7 +1,7 @@
import { Outlet } from 'react-router';
import { Header } from './next-header';
import { Header } from './components/header';
export function NextLayoutContainer({ children }: React.PropsWithChildren) {
export function RootLayoutContainer({ children }: React.PropsWithChildren) {
return (
<div className="size-full grid grid-rows-[auto_1fr] grid-cols-1 grid-flow-col">
<Header className="px-5 py-4" />
@ -11,10 +11,10 @@ export function NextLayoutContainer({ children }: React.PropsWithChildren) {
);
}
export default function NextLayout() {
export default function RootLayout() {
return (
<NextLayoutContainer>
<RootLayoutContainer>
<Outlet />
</NextLayoutContainer>
</RootLayoutContainer>
);
}

View File

@ -73,6 +73,7 @@ export default {
},
selected: 'Selected',
seeAll: 'See all',
bulkOperate: 'Bulk operate',
},
login: {
loginTitle: 'Sign in to your account',
@ -514,7 +515,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
buildItFromScratch: 'Build it from scratch',
dataFlow: 'Pipeline',
parseType: 'Parse type',
manualSetup: 'Choose pipeline',
manualSetup: 'Pipeline',
builtIn: 'Built-in',
titleDescription:
'Update your memory configuration here, particularly the LLM and prompts.',

View File

@ -20,7 +20,7 @@ import { getSystemVersion, logout } from '@/services/admin-service';
import authorizationUtil from '@/utils/authorization-util';
import ThemeSwitch from '../components/theme-switch';
import ThemeSwitch from '../../../components/theme-switch';
import { IS_ENTERPRISE } from '../utils';
import { CurrentUserInfoContext } from './root-layout';

View File

@ -33,8 +33,8 @@ import authorizationUtil from '@/utils/authorization-util';
import { login } from '@/services/admin-service';
import ThemeSwitch from '../../components/theme-switch';
import { BgSvg } from '../login-next/bg';
import ThemeSwitch from './components/theme-switch';
import { CurrentUserInfoContext } from './layouts/root-layout';

View File

@ -50,25 +50,23 @@ export function DatasetActionCell({
}, [record, showRenameModal]);
return (
<section className="flex gap-4 items-center text-text-sub-title-invert opacity-0 group-hover:opacity-100 transition-opacity">
<div
className="
flex gap-2 items-center opacity-0
transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"
>
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-xs"
variant="ghost"
disabled={isRunning}
onClick={handleRename}
>
<PenLine />
<PenLine className="size-[1em]" />
</Button>
<HoverCard>
<HoverCardTrigger>
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
disabled={isRunning}
size={'sm'}
>
<Eye />
<Button size="icon-xs" variant="ghost" disabled={isRunning}>
<Eye className="size-[1em]" />
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto">
@ -94,26 +92,24 @@ export function DatasetActionCell({
{isVirtualDocument || (
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size="icon-xs"
variant="ghost"
onClick={onDownloadDocument}
disabled={isRunning}
size={'sm'}
>
<Download />
<Download className="size-[1em]" />
</Button>
)}
<ConfirmDeleteDialog onOk={handleRemove}>
<Button
data-testid="document-delete"
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-xs"
variant="ghost"
disabled={isRunning}
>
<Trash2 />
<Trash2 className="size-[1em]" />
</Button>
</ConfirmDeleteDialog>
</section>
</div>
);
}

View File

@ -199,25 +199,27 @@ const Generate: React.FC<GenerateProps> = (props) => {
return (
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild disabled={disabled}>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={disabled}
className={cn(disabled && '!cursor-not-allowed')}
variant="transparent"
size="icon"
onClick={() => {
if (!disabled) {
handleOpenChange(!open);
}
}}
>
<WandSparkles />
</Button>
</TooltipTrigger>
<div>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={disabled}
className={cn(disabled && '!cursor-not-allowed')}
variant="transparent"
size="icon"
onClick={() => {
if (!disabled) {
handleOpenChange(!open);
}
}}
>
<WandSparkles />
</Button>
</TooltipTrigger>
<TooltipContent>{t('knowledgeDetails.generate')}</TooltipContent>
</Tooltip>
<TooltipContent>{t('knowledgeDetails.generate')}</TooltipContent>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[380px] p-5 flex flex-col gap-2 ">
{Object.values(GenerateType).map((name) => {

View File

@ -6,6 +6,7 @@ import { FileUploadDialog } from '@/components/file-upload-dialog';
import ListFilterBar from '@/components/list-filter-bar';
import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
@ -16,7 +17,7 @@ import {
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
import { useFetchDocumentList } from '@/hooks/use-document-request';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { Upload } from 'lucide-react';
import { LucidePlus } from 'lucide-react';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MetadataType } from '../components/metedata/constant';
@ -136,10 +137,12 @@ export default function Dataset() {
});
return (
<>
<section className="p-5 min-w-[880px]">
<Card
as="article"
className="mb-5 mr-5 min-w-[880px] bg-transparent shadow-none"
>
<CardHeader as="header" className="p-5 space-y-0">
<ListFilterBar
title={t('header.dataset')}
onSearchChange={handleInputChange}
searchString={searchString}
value={filterValue}
@ -147,12 +150,15 @@ export default function Dataset() {
onChange={handleFilterSubmit}
onOpenChange={onOpenChange}
filters={filters}
className="items-end"
leftPanel={
<div className="items-start">
<div className="pb-1">{t('knowledgeDetails.subbarFiles')}</div>
<div className="text-text-secondary text-sm">
<div>
<h1 className="leading-normal font-medium">
{t('knowledgeDetails.subbarFiles')}
</h1>
<p className="text-text-secondary text-sm font-normal">
{t('knowledgeDetails.datasetDescription')}
</div>
</p>
</div>
}
preChildren={<Generate disabled={!(dataSetData.chunk_num > 0)} />}
@ -190,12 +196,12 @@ export default function Dataset() {
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size={'sm'}>
<Upload />
<Button size="default">
<LucidePlus />
{t('knowledgeDetails.addFile')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuContent className="w-auto min-w-40" align="end">
<DropdownMenuItem onClick={showDocumentUploadModal}>
{t('fileManager.uploadFile')}
</DropdownMenuItem>
@ -206,12 +212,17 @@ export default function Dataset() {
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
{rowSelectionIsEmpty || (
<BulkOperateBar
className="!mt-2.5 !-mb-2.5"
list={updatedList as BulkOperateItemType[]}
count={selectedCount}
></BulkOperateBar>
/>
)}
</CardHeader>
<CardContent className="px-5 py-0">
<DatasetTable
documents={documents}
pagination={pagination}
@ -220,7 +231,8 @@ export default function Dataset() {
setRowSelection={setRowSelection}
showManageMetadataModal={showManageMetadataModal}
loading={loading}
></DatasetTable>
/>
{documentUploadVisible && (
<FileUploadDialog
hideModal={hideDocumentUploadModal}
@ -282,7 +294,7 @@ export default function Dataset() {
hideModal={hideReparseDialogModal}
></ReparseDialog>
)}
</section>
</>
</CardContent>
</Card>
);
}

View File

@ -84,9 +84,8 @@ export const PopoverContent = ({ record }: IProps) => {
export function ParsingCard({ record, handleShowLog }: IProps) {
return (
<Button
variant={'transparent'}
className="border-none"
size={'sm'}
variant="ghost"
size="icon-xs"
onClick={() => handleShowLog?.(record)}
>
<Dot run={record.run}></Dot>

View File

@ -1,4 +1,5 @@
import { IconFontFill } from '@/components/icon-font';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@ -24,10 +25,10 @@ import { useHandleRunDocumentByIds } from './use-run-document';
import { isParserRunning } from './utils';
const IconMap = {
[RunningStatus.UNSTART]: (
<IconFontFill name="play" className="text-accent-primary" />
<IconFontFill name="play" className="text-accent-primary size-[1em]" />
),
[RunningStatus.RUNNING]: (
<CircleX size={14} color="rgba(var(--state-error))" />
<CircleX color="rgba(var(--state-error))" className="size-[1em]" />
),
[RunningStatus.CANCEL]: (
<IconFontFill name="reparse" className="text-accent-primary" />
@ -49,25 +50,63 @@ const ParseStatusStateMap = {
[RunningStatus.SCHEDULE]: 'running',
} as const;
export function ParsingStatusCell({
export function ParseDropdownButton({
record,
showChangeParserModal,
// showSetMetaModal,
}: {
record: IDocumentInfo;
} & UseChangeDocumentParserShowType) {
const { t } = useTranslation();
const { pipeline_id, pipeline_name, parser_id } = record;
const handleShowChangeParserModal = useCallback(() => {
showChangeParserModal(record);
}, [record, showChangeParserModal]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="static" size="auto" className="capitalize">
{pipeline_id
? pipeline_name || pipeline_id
: parser_id === 'naive'
? 'general'
: parser_id}
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="capitalize">
{pipeline_id
? pipeline_name || pipeline_id
: parser_id === 'naive'
? 'general'
: parser_id}
</p>
</TooltipContent>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.dataPipeline')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function ParsingStatusCell({
record,
showLog,
}: {
record: IDocumentInfo;
showLog: (record: IDocumentInfo) => void;
} & UseChangeDocumentParserShowType) {
const { t } = useTranslation();
const {
run,
parser_id,
pipeline_id,
pipeline_name,
progress,
chunk_num,
id,
} = record;
const { run, progress, chunk_num, id } = record;
const operationIcon = IconMap[run];
const p = Number((progress * 100).toFixed(2));
const {
@ -86,10 +125,6 @@ export function ParsingStatusCell({
handleRunDocumentByIds(record.id, isRunning, option);
};
const handleShowChangeParserModal = useCallback(() => {
showChangeParserModal(record);
}, [record, showChangeParserModal]);
const showParse = useMemo(() => {
return record.type !== DocumentType.Virtual;
}, [record]);
@ -103,71 +138,25 @@ export function ParsingStatusCell({
data-testid="document-parse-status"
data-state={ParseStatusStateMap[run] ?? 'unknown'}
>
<div className="text-ellipsis w-[100px] flex items-center justify-between">
<DropdownMenu>
<DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="border-none truncate max-w-32 cursor-pointer px-2 py-1 rounded-sm hover:bg-bg-card">
{pipeline_id
? pipeline_name || pipeline_id
: parser_id === 'naive'
? 'general'
: parser_id}
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{pipeline_id
? pipeline_name || pipeline_id
: parser_id === 'naive'
? 'general'
: parser_id}
</p>
</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.dataPipeline')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{showParse && (
<div className="flex items-center gap-3">
<Separator orientation="vertical" className="h-2.5" />
{!isParserRunning(run) && (
// <ReparseDialog
// hidden={isZeroChunk || isRunning}
// handleOperationIconClick={handleOperationIconClick}
// chunk_num={chunk_num}
// >
<div
className="cursor-pointer flex items-center gap-3"
onClick={() => {
showReparseDialogModal();
}}
>
{!isParserRunning(run) && operationIcon}
</div>
// {/* </ReparseDialog> */}
)}
<div className="flex items-center gap-2">
<Separator orientation="vertical" className="h-[1em]" />
{isParserRunning(run) ? (
<>
<div
className="flex items-center gap-1 cursor-pointer"
<Button
size="auto"
variant="static"
onClick={() => handleShowLog(record)}
>
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
<div
className="cursor-pointer flex items-center gap-3"
onClick={() => {
showReparseDialogModal();
}}
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() => showReparseDialogModal()}
// onClick={
// isZeroChunk || isRunning
// ? handleOperationIconClick(false)
@ -175,13 +164,22 @@ export function ParsingStatusCell({
// }
>
{operationIcon}
</div>
</Button>
</>
) : (
<ParsingCard
record={record}
handleShowLog={handleShowLog}
></ParsingCard>
<>
<Button
variant="ghost"
size="icon-xs"
onClick={() => {
showReparseDialogModal();
}}
>
{operationIcon}
</Button>
<ParsingCard record={record} handleShowLog={handleShowLog} />
</>
)}
</div>
)}

View File

@ -10,12 +10,12 @@ import {
} from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import {
Ban,
CircleCheck,
CircleX,
Cylinder,
Play,
Trash2,
LucideCircleX,
LucideCylinder,
LucidePlayCircle,
LucideToggleLeft,
LucideToggleRight,
LucideTrash2,
} from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -117,36 +117,36 @@ export function useBulkOperateDataset({
{
id: 'enabled',
label: t('knowledgeDetails.enabled'),
icon: <CircleCheck />,
icon: <LucideToggleRight />,
onClick: handleEnableClick,
},
{
id: 'disabled',
label: t('knowledgeDetails.disabled'),
icon: <Ban />,
icon: <LucideToggleLeft />,
onClick: handleDisableClick,
},
{
id: 'run',
label: t('knowledgeDetails.run'),
icon: <Play />,
icon: <LucidePlayCircle />,
onClick: () => showModal(),
},
{
id: 'cancel',
label: t('knowledgeDetails.cancel'),
icon: <CircleX />,
icon: <LucideCircleX />,
onClick: handleCancelClick,
},
{
id: 'batch-metadata',
label: t('knowledgeDetails.metadata.metadata'),
icon: <Cylinder />,
icon: <LucideCylinder />,
},
{
id: 'delete',
label: t('common.delete'),
icon: <Trash2 />,
icon: <LucideTrash2 />,
onClick: async () => {
const code = await handleDelete();
if (code === 0) {

View File

@ -11,15 +11,14 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useSetDocumentStatus } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { cn } from '@/lib/utils';
import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
import { formatDate } from '@/utils/date';
import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown, MonitorUp } from 'lucide-react';
import { ArrowUpDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { MetadataType } from '../components/metedata/constant';
import { ShowManageMetadataModalProps } from '../components/metedata/interface';
import { DatasetActionCell } from './dataset-action-cell';
import { ParsingStatusCell } from './parsing-status-cell';
import { ParseDropdownButton, ParsingStatusCell } from './parsing-status-cell';
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { UseRenameDocumentShowType } from './use-rename-document';
@ -38,7 +37,7 @@ export function useDatasetTableColumns({
const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails',
});
const { dataSourceInfo } = useDataSourceInfo();
// const { dataSourceInfo } = useDataSourceInfo();
const { navigateToChunkParsedResult } = useNavigatePage();
const { setDocumentStatus } = useSetDocumentStatus();
@ -47,6 +46,7 @@ export function useDatasetTableColumns({
id: 'select',
header: ({ table }) => (
<Checkbox
className="size-3"
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
@ -57,6 +57,7 @@ export function useDatasetTableColumns({
),
cell: ({ row }) => (
<Checkbox
className="size-3"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
@ -69,14 +70,19 @@ export function useDatasetTableColumns({
accessorKey: 'name',
header: ({ column }) => {
return (
<Button
variant="transparent"
className="border-none"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('name')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown />
</Button>
</div>
);
},
meta: { cellClassName: 'max-w-[20vw]' },
@ -87,7 +93,7 @@ export function useDatasetTableColumns({
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex gap-2 cursor-pointer"
className="flex items-center gap-2 cursor-pointer"
onClick={navigateToChunkParsedResult(
row.original.id,
row.original.kb_id,
@ -108,22 +114,31 @@ export function useDatasetTableColumns({
accessorKey: 'create_time',
header: ({ column }) => {
return (
<Button
variant="transparent"
className="border-none"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('uploadDate')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown />
</Button>
</div>
);
},
cell: ({ row }) => (
<div className="lowercase">
<time
className="lowercase"
dateTime={new Date(row.getValue('create_time')).toISOString()}
>
{formatDate(row.getValue('create_time'))}
</div>
</time>
),
},
/*
{
accessorKey: 'source_from',
header: t('source'),
@ -146,6 +161,7 @@ export function useDatasetTableColumns({
</div>
),
},
*/
{
accessorKey: 'status',
header: t('enabled'),
@ -174,8 +190,9 @@ export function useDatasetTableColumns({
cell: ({ row }) => {
const length = Object.keys(row.getValue('meta_fields') || {}).length;
return (
<div
className="capitalize cursor-pointer"
<Button
variant="static"
size="auto"
onClick={() => {
showManageMetadataModal({
// metadata: util.JSONToMetaDataTableData(
@ -209,7 +226,7 @@ export function useDatasetTableColumns({
}}
>
{length + ' fields'}
</div>
</Button>
);
},
},
@ -217,13 +234,25 @@ export function useDatasetTableColumns({
accessorKey: 'run',
header: t('Parse'),
// meta: { cellClassName: 'min-w-[20vw]' },
cell: ({ row }) => {
return (
<ParseDropdownButton
record={row.original}
showChangeParserModal={showChangeParserModal}
/>
);
},
},
{
id: 'run-status',
header: '',
cell: ({ row }) => {
return (
<ParsingStatusCell
record={row.original}
showChangeParserModal={showChangeParserModal}
showLog={showLog}
></ParsingStatusCell>
/>
);
},
},
@ -238,7 +267,7 @@ export function useDatasetTableColumns({
<DatasetActionCell
record={record}
showRenameModal={showRenameModal}
></DatasetActionCell>
/>
);
},
},

View File

@ -45,7 +45,7 @@ export function SeeAllCard() {
return (
<Card
className="w-full flex-none h-full cursor-pointer"
onClick={navigateToDatasetList}
onClick={() => navigateToDatasetList({ isCreate: false })}
>
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
{t('common.seeAll')} <ChevronRight className="size-4" />

View File

@ -20,14 +20,16 @@ export function ApplicationCard({
moreDropdown,
}: ApplicationCardProps) {
return (
<Card className="w-[264px]" onClick={onClick}>
<CardContent className="p-2.5 group flex justify-between w-full">
<Card className="w-[264px]" onClick={onClick} as="article">
<CardContent className="p-2.5 group flex justify-between w-full">
<div className="flex items-center gap-2.5 w-full">
<RAGFlowAvatar
className="size-14 rounded-lg"
avatar={app.avatar}
name={app.title || 'CN'}
></RAGFlowAvatar>
aria-hidden="true"
/>
<div className="flex-1">
<h3 className="text-sm font-normal line-clamp-1 mb-1 text-ellipsis w-[160px] overflow-hidden">
{app.title}
@ -37,6 +39,7 @@ export function ApplicationCard({
</p>
</div>
</div>
{moreDropdown}
</CardContent>
</Card>
@ -49,7 +52,11 @@ export type SeeAllAppCardProps = {
export function SeeAllAppCard({ click }: SeeAllAppCardProps) {
return (
<Card className="w-full min-h-[76px] cursor-pointer" onClick={click}>
<Card
className="w-full min-h-[76px] cursor-pointer"
onClick={click}
tabIndex={0}
>
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
{t('common.seeAll')} <ChevronRight className="size-4" />
</CardContent>

View File

@ -47,10 +47,10 @@ export function Applications() {
const options = useMemo(
() => [
{ value: Routes.Chats, label: t('chat.chatApps') },
{ value: Routes.Searches, label: t('search.searchApps') },
{ value: Routes.Chats, label: t('header.chat') },
{ value: Routes.Searches, label: t('header.search') },
{ value: Routes.Agents, label: t('header.flow') },
{ value: Routes.Memories, label: t('memories.memory') },
{ value: Routes.Memories, label: t('header.memories') },
],
[t],
);
@ -63,55 +63,57 @@ export function Applications() {
return (
<section className="mt-12">
<div className="flex justify-between items-center mb-5">
<h2 className="text-2xl font-semibold flex gap-2.5">
<header className="flex justify-between items-center mb-2.5">
<h2 className="text-2xl font-semibold">
<HomeIcon
imgClass="me-2.5"
name={`${IconMap[val as keyof typeof IconMap]}`}
width={'32'}
width={24}
/>
{options.find((x) => x.value === val)?.label}
</h2>
<Segmented
buttonSize="sm"
options={options}
value={val}
onChange={handleChange}
buttonSize="xl"
// className="bg-bg-card border border-border-button rounded-lg"
// activeClassName="bg-text-primary border-none rounded-lg"
></Segmented>
</div>
/>
</header>
{/* <div className="flex flex-wrap gap-4"> */}
<CardSineLineContainer>
{val === Routes.Agents && (
<Agents
setListLength={(length: number) => setListLength(length)}
setLoading={(loading: boolean) => setLoading(loading)}
></Agents>
/>
)}
{val === Routes.Chats && (
<ChatList
setListLength={(length: number) => setListLength(length)}
setLoading={(loading: boolean) => setLoading(loading)}
></ChatList>
/>
)}
{val === Routes.Searches && (
<SearchList
setListLength={(length: number) => setListLength(length)}
setLoading={(loading: boolean) => setLoading(loading)}
></SearchList>
/>
)}
{val === Routes.Memories && (
<MemoryList
setListLength={(length: number) => setListLength(length)}
setLoading={(loading: boolean) => setLoading(loading)}
></MemoryList>
/>
)}
{listLength > 0 && (
<SeeAllAppCard
click={() => handleNavigate({ isCreate: false })}
></SeeAllAppCard>
<SeeAllAppCard click={() => handleNavigate({ isCreate: false })} />
)}
</CardSineLineContainer>
{listLength <= 0 && !loading && (
<EmptyAppCard
type={EmptyTypeMap[val as keyof typeof EmptyTypeMap]}

View File

@ -42,9 +42,11 @@ export function Banner() {
export function NextBanner() {
const { t } = useTranslation();
return (
<h1 className="text-5xl font-bold leading-normal">
<span className="text-text-primary">{t('header.welcome')} </span>
<span className="text-transparent bg-clip-text bg-gradient-to-l from-[#40EBE3] to-[#4A51FF]">
<h1 className="text-5xl leading-normal">
<span className="font-semibold text-text-primary">
{t('header.welcome')}{' '}
</span>
<span className="font-bold text-transparent bg-clip-text bg-gradient-to-l from-[#40EBE3] to-[#4A51FF]">
RAGFlow
</span>
</h1>

View File

@ -26,12 +26,15 @@ export function Datasets() {
return (
<section>
<h2 className="text-2xl font-semibold mb-6 flex gap-2.5 items-center">
{/* <IconFont name="data" className="size-8"></IconFont> */}
<HomeIcon name="datasets" width={'32'} />
{t('header.dataset')}
</h2>
<div className="">
<header>
<h2 className="leading-8 text-2xl font-semibold mb-2.5">
{/* <IconFont name="data" className="size-8"></IconFont> */}
<HomeIcon imgClass="me-2.5" name="datasets" width={24} />
{t('header.dataset')}
</h2>
</header>
<div>
{loading ? (
<div className="flex-1">
<CardSkeleton />
@ -40,15 +43,13 @@ export function Datasets() {
<>
{kbs?.length > 0 && (
<CardSineLineContainer>
{kbs
?.slice(0, 6)
.map((dataset) => (
<DatasetCard
key={dataset.id}
dataset={dataset}
showDatasetRenameModal={showDatasetRenameModal}
></DatasetCard>
))}
{kbs?.slice(0, 6).map((dataset) => (
<DatasetCard
key={dataset.id}
dataset={dataset}
showDatasetRenameModal={showDatasetRenameModal}
></DatasetCard>
))}
{
<SeeAllAppCard
click={() => navigateToDatasetList({ isCreate: false })}
@ -66,13 +67,14 @@ export function Datasets() {
// </div>
)}
</div>
{datasetRenameVisible && (
<RenameDialog
hideModal={hideDatasetRenameModal}
onOk={onDatasetRenameOk}
initialName={initialDatasetName}
loading={datasetRenameLoading}
></RenameDialog>
/>
)}
</section>
);

View File

@ -1,4 +1,4 @@
import { PageContainer } from '@/layouts/page-container';
import { PageContainer } from '@/layouts/components/page-container';
import { Applications } from './applications';
import { NextBanner } from './banner';
import { Datasets } from './datasets';
@ -6,12 +6,14 @@ import { Datasets } from './datasets';
const Home = () => {
return (
<PageContainer>
<header className="mb-8">
<NextBanner />
</header>
<article>
<header className="mb-8">
<NextBanner />
</header>
<Datasets />
<Applications />
<Datasets />
<Applications />
</article>
</PageContainer>
);
};

View File

@ -6,7 +6,7 @@ import {
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { NextLayoutContainer } from '@/layouts/next';
import { RootLayoutContainer } from '@/layouts/root-layout';
import { cn } from '@/lib/utils';
import { useMount } from 'ahooks';
import { isEmpty } from 'lodash';
@ -106,7 +106,7 @@ export default function Chat() {
}
return (
<NextLayoutContainer>
<RootLayoutContainer>
<section className="h-full flex flex-col" data-testid="chat-detail">
<article className="flex flex-1 min-h-0 pb-9">
<Sessions handleConversationCardClick={handleSessionClick}></Sessions>
@ -146,6 +146,6 @@ export default function Chat() {
</Card>
</article>
</section>
</NextLayoutContainer>
</RootLayoutContainer>
);
}

View File

@ -1,6 +1,6 @@
import { IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import ThemeToggle from '@/components/theme-toggle';
import ThemeSwitch from '@/components/theme-switch';
import { Button } from '@/components/ui/button';
import { Domain } from '@/constants/common';
import { useSecondPathName } from '@/hooks/route-hook';
@ -103,15 +103,10 @@ export function SideBar() {
<div className="mr-2 px-2 text-accent-primary rounded-md">
{version}
</div>
<ThemeToggle />
<ThemeSwitch />
</div>
<Button
variant="ghost"
className="w-full gap-3 bg-bg-base border border-border-button"
onClick={() => {
logout();
}}
>
<Button block size="lg" variant="transparent" onClick={() => logout()}>
{t('setting.logout')}
</Button>
</div>

View File

@ -146,7 +146,7 @@ const routeConfigOptions = [
{
path: Routes.Root,
layout: false,
Component: () => import('@/layouts/next'),
Component: () => import('@/layouts/root-layout'),
loader: ({ request }) => {
const url = new URL(request.url);
const auth = url.searchParams.get('auth');
@ -170,7 +170,7 @@ const routeConfigOptions = [
},
{
path: Routes.Root,
Component: () => import('@/layouts/next'),
Component: () => import('@/layouts/root-layout'),
children: [
{
path: Routes.Datasets,