fix: fix dataset-page's bugs (#8786)

### What problem does this PR solve?

fix dataset-page's bugs,Input component supports icon, added Radio
component, and removed antd from chunk-result-bar page [#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:
chanx
2025-07-11 11:34:36 +08:00
committed by GitHub
parent 07208e519b
commit 52dce4329d
23 changed files with 399 additions and 153 deletions

View File

@ -12,7 +12,13 @@ import {
type EntityTypesFormFieldProps = {
name?: string;
};
const initialEntityTypes = [
'organization',
'person',
'geo',
'event',
'category',
];
export function EntityTypesFormField({
name = 'parser_config.entity_types',
}: EntityTypesFormFieldProps) {
@ -23,7 +29,9 @@ export function EntityTypesFormField({
<FormField
control={form.control}
name={name}
render={({ field }) => (
defaultValue={initialEntityTypes}
render={({ field }) => {
return (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
@ -40,7 +48,8 @@ export function EntityTypesFormField({
<FormMessage />
</div>
</FormItem>
)}
);
}}
/>
);
}

View File

@ -25,7 +25,7 @@ const EntityTypesItem = ({
rules={[{ required: true }]}
initialValue={initialEntityTypes}
>
<EditTag></EditTag>
<EditTag value={field}></EditTag>
</Form.Item>
);
};

View File

@ -1,12 +1,17 @@
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { useCallback, useState } from 'react';
import { FilterChange, FilterValue } from './interface';
export function useHandleFilterSubmit() {
const [filterValue, setFilterValue] = useState<FilterValue>({});
const handleFilterSubmit: FilterChange = useCallback((value) => {
const { setPagination } = useGetPaginationWithRouter();
const handleFilterSubmit: FilterChange = useCallback(
(value) => {
setFilterValue(value);
}, []);
setPagination({ page: 1 });
},
[setPagination],
);
return { filterValue, setFilterValue, handleFilterSubmit };
}

View File

@ -7,7 +7,7 @@ interface IProps {
max?: number;
}
export function MaxTokenNumberFormField({ max = 2048 }: IProps) {
export function MaxTokenNumberFormField({ max = 2048, initialValue }: IProps) {
const { t } = useTranslate('knowledgeConfiguration');
return (
@ -15,6 +15,7 @@ export function MaxTokenNumberFormField({ max = 2048 }: IProps) {
name={'parser_config.chunk_token_num'}
label={t('chunkTokenNumber')}
max={max}
defaultValue={initialValue ?? 0}
layout={FormLayout.Horizontal}
></SliderInputFormField>
);

View File

@ -1,9 +1,31 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
type InputProps = React.ComponentProps<'input'> & {
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
};
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
function Input({
className,
type,
icon,
iconPosition = 'left',
...props
}: InputProps) {
return (
<div className="relative">
{icon && (
<div
className={cn(
'absolute w-1 top-0 flex h-full items-center justify-center pointer-events-none',
iconPosition === 'left' ? 'left-5' : 'right-5',
iconPosition === 'left' ? 'pr-2' : 'pl-2',
)}
>
{icon}
</div>
)}
<input
type={type}
data-slot="input"
@ -15,10 +37,13 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none',
type === 'file' &&
'text-muted-foreground/70 file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic',
icon && iconPosition === 'left' && 'pl-7',
icon && iconPosition === 'right' && 'pr-7',
className,
)}
{...props}
/>
</div>
);
}

View File

@ -64,10 +64,10 @@ const RaptorFormFields = () => {
control={form.control}
name={UseRaptorField}
render={({ field }) => {
if (typeof field.value === 'undefined') {
// default value set
form.setValue('parser_config.raptor.use_raptor', false);
}
// if (typeof field.value === 'undefined') {
// // default value set
// form.setValue('parser_config.raptor.use_raptor', false);
// }
return (
<FormItem
defaultChecked={false}
@ -102,7 +102,8 @@ const RaptorFormFields = () => {
<FormField
control={form.control}
name={'parser_config.raptor.prompt'}
render={({ field }) => (
render={({ field }) => {
return (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-start">
<FormLabel
@ -113,7 +114,11 @@ const RaptorFormFields = () => {
</FormLabel>
<div className="w-3/4">
<FormControl>
<Textarea {...field} rows={8} />
<Textarea
{...field}
rows={8}
defaultValue={t('promptText')}
/>
</FormControl>
</div>
</div>
@ -122,7 +127,8 @@ const RaptorFormFields = () => {
<FormMessage />
</div>
</FormItem>
)}
);
}}
/>
<SliderInputFormField
name={'parser_config.raptor.max_token'}
@ -164,7 +170,7 @@ const RaptorFormFields = () => {
<div className="w-3/4">
<FormControl defaultValue={0}>
<div className="flex gap-4">
<Input {...field} />
<Input {...field} defaultValue={0} />
<Button
size={'sm'}
onClick={handleGenerate}

View File

@ -4,10 +4,12 @@ import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {
value?: string | number | readonly string[] | undefined;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, type, value, ...props }, ref) => {
return (
<input
type={type}
@ -16,6 +18,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
className,
)}
ref={ref}
value={value ?? ''}
{...props}
/>
);

View File

@ -5,7 +5,27 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const Popover = PopoverPrimitive.Root;
const Popover = (props: PopoverPrimitive.PopoverProps) => {
const { children, open: openState, onOpenChange } = props;
const [open, setOpen] = React.useState(true);
React.useEffect(() => {
setOpen(!!openState);
}, [openState]);
const handleOnOpenChange = React.useCallback(
(e: boolean) => {
if (onOpenChange) {
onOpenChange?.(e);
}
setOpen(e);
},
[onOpenChange],
);
return (
<PopoverPrimitive.Root open={open} onOpenChange={handleOnOpenChange}>
{children}
</PopoverPrimitive.Root>
);
};
const PopoverTrigger = PopoverPrimitive.Trigger;

View File

@ -0,0 +1,133 @@
import { cn } from '@/lib/utils';
import { Radio as LucideRadio } from 'lucide-react';
import React, { useContext, useState } from 'react';
const RadioGroupContext = React.createContext<{
value: string | number;
onChange: (value: string | number) => void;
disabled?: boolean;
} | null>(null);
type RadioProps = {
value: string | number;
checked?: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
children?: React.ReactNode;
};
function Radio({ value, checked, disabled, onChange, children }: RadioProps) {
const groupContext = useContext(RadioGroupContext);
const isControlled = checked !== undefined;
// const [internalChecked, setInternalChecked] = useState(false);
const isChecked = isControlled ? checked : groupContext?.value === value;
const mergedDisabled = disabled || groupContext?.disabled;
const handleClick = () => {
if (mergedDisabled) return;
// if (!isControlled) {
// setInternalChecked(!isChecked);
// }
if (onChange) {
onChange(!isChecked);
}
if (groupContext && !groupContext.disabled) {
groupContext.onChange(value);
}
};
return (
<label
className={cn(
'flex items-center cursor-pointer gap-2 text-sm',
mergedDisabled && 'cursor-not-allowed opacity-50',
)}
>
<span
className={cn(
'flex h-4 w-4 items-center justify-center rounded-full border border-input transition-colors',
'peer ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isChecked && 'border-primary bg-primary/10',
mergedDisabled && 'border-muted',
)}
onClick={handleClick}
>
{isChecked && (
<LucideRadio className="h-3 w-3 fill-primary text-primary" />
)}
</span>
{children && <span className="text-foreground">{children}</span>}
</label>
);
}
type RadioGroupProps = {
value?: string | number;
defaultValue?: string | number;
onChange?: (value: string | number) => void;
disabled?: boolean;
children: React.ReactNode;
className?: string;
direction?: 'horizontal' | 'vertical';
};
function Group({
value,
defaultValue,
onChange,
disabled,
children,
className,
direction = 'horizontal',
}: RadioGroupProps) {
const [internalValue, setInternalValue] = useState(defaultValue || '');
const isControlled = value !== undefined;
const mergedValue = isControlled ? value : internalValue;
const handleChange = (val: string | number) => {
if (disabled) return;
if (!isControlled) {
setInternalValue(val);
}
if (onChange) {
onChange(val);
}
};
return (
<RadioGroupContext.Provider
value={{
value: mergedValue,
onChange: handleChange,
disabled,
}}
>
<div
className={cn(
'flex gap-4',
direction === 'vertical' ? 'flex-col' : 'flex-row',
className,
)}
>
{React.Children.map(children, (child) =>
React.cloneElement(child as React.ReactElement, {
disabled: disabled || child?.props?.disabled,
}),
)}
</div>
</RadioGroupContext.Provider>
);
}
const RadioComponent = Object.assign(Radio, {
Group,
});
export { RadioComponent as Radio };

View File

@ -31,6 +31,15 @@ export function Segmented({
onChange,
className,
}: SegmentedProps) {
const [selectedValue, setSelectedValue] = React.useState<
SegmentedValue | undefined
>(value);
const handleOnChange = (e: SegmentedValue) => {
if (onChange) {
onChange(e);
}
setSelectedValue(e);
};
return (
<div
className={cn(
@ -48,11 +57,11 @@ export function Segmented({
className={cn(
'inline-flex items-center px-6 py-2 text-base font-normal rounded-3xl cursor-pointer text-text-badge',
{
'bg-text-title': value === actualValue,
'text-text-title-invert': value === actualValue,
'bg-text-title': selectedValue === actualValue,
'text-text-title-invert': selectedValue === actualValue,
},
)}
onClick={() => onChange?.(actualValue)}
onClick={() => handleOnChange(actualValue)}
>
{isObject ? option.label : option}
</div>

View File

@ -180,7 +180,9 @@ export const useCreateChunk = () => {
const { data } = await service(payload);
if (data.code === 0) {
message.success(t('message.created'));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}, 1000); // Delay to ensure the list is updated
}
return data?.code;
},

View File

@ -122,10 +122,11 @@ export const useSetNextDocumentStatus = () => {
documentId,
}: {
status: boolean;
documentId: string;
documentId: string | string[];
}) => {
const ids = Array.isArray(documentId) ? documentId : [documentId];
const { data } = await kbService.document_change_status({
doc_id: documentId,
doc_ids: ids,
status: Number(status),
});
if (data.code === 0) {

View File

@ -25,13 +25,14 @@ import {
import { useDebounce } from 'ahooks';
import { message } from 'antd';
import { useState } from 'react';
import { useSearchParams } from 'umi';
import { useParams, useSearchParams } from 'umi';
import { useHandleSearchChange } from './logic-hooks';
import { useSetPaginationParams } from './route-hook';
export const useKnowledgeBaseId = (): string => {
const [searchParams] = useSearchParams();
const knowledgeBaseId = searchParams.get('id');
const { id } = useParams();
const knowledgeBaseId = searchParams.get('id') || id;
return knowledgeBaseId || '';
};

View File

@ -37,20 +37,6 @@ export const useSetSelectedRecord = <T = IKnowledgeFile>() => {
return { currentRecord, setRecord };
};
export const useHandleSearchChange = () => {
const [searchString, setSearchString] = useState('');
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value;
setSearchString(value);
},
[],
);
return { handleInputChange, searchString };
};
export const useChangeLanguage = () => {
const { i18n } = useTranslation();
const { saveSetting } = useSaveSetting();
@ -82,9 +68,12 @@ export const useGetPaginationWithRouter = () => {
const setCurrentPagination = useCallback(
(pagination: { page: number; pageSize?: number }) => {
if (pagination.pageSize !== pageSize) {
pagination.page = 1; // Reset to first page if pageSize changes
}
setPaginationParams(pagination.page, pagination.pageSize);
},
[setPaginationParams],
[setPaginationParams, pageSize],
);
const pagination: PaginationProps = useMemo(() => {
@ -106,6 +95,21 @@ export const useGetPaginationWithRouter = () => {
};
};
export const useHandleSearchChange = () => {
const [searchString, setSearchString] = useState('');
const { setPagination } = useGetPaginationWithRouter();
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value;
setSearchString(value);
setPagination({ page: 1 });
},
[setPagination],
);
return { handleInputChange, searchString };
};
export const useGetPagination = () => {
const [pagination, setPagination] = useState({ page: 1, pageSize: 10 });
const { t } = useTranslate('common');

View File

@ -4,6 +4,7 @@ import { useNavigate, useParams, useSearchParams } from 'umi';
export enum QueryStringMap {
KnowledgeId = 'knowledgeId',
id = 'id',
}
export const useNavigatePage = () => {
@ -77,6 +78,7 @@ export const useNavigatePage = () => {
[QueryStringMap.KnowledgeId]: searchParams.get(
QueryStringMap.KnowledgeId,
),
[QueryStringMap.id]: searchParams.get(QueryStringMap.id),
};
if (queryStringKey) {
return allQueryString[queryStringKey];

View File

@ -204,10 +204,11 @@ export const useSetDocumentStatus = () => {
documentId,
}: {
status: boolean;
documentId: string;
documentId: string | string[];
}) => {
const ids = Array.isArray(documentId) ? documentId : [documentId];
const { data } = await kbService.document_change_status({
doc_id: documentId,
doc_ids: ids,
status: Number(status),
});
if (data.code === 0) {

View File

@ -1,18 +1,26 @@
import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
import { useTranslate } from '@/hooks/common-hooks';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import {
Button,
Input,
Popover,
Radio,
RadioChangeEvent,
Segmented,
SegmentedProps,
Space,
} from 'antd';
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Radio } from '@/components/ui/radio';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { SearchOutlined } from '@ant-design/icons';
import { ListFilter, Plus } from 'lucide-react';
import { useState } from 'react';
import { ChunkTextMode } from '../../constant';
interface ChunkResultBarProps {
changeChunkTextMode: React.Dispatch<React.SetStateAction<string | number>>;
available: number | undefined;
selectAllChunk: (value: boolean) => void;
handleSetAvailable: (value: number | undefined) => void;
createChunk: () => void;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
searchString: string;
}
export default ({
changeChunkTextMode,
available,
@ -21,52 +29,75 @@ export default ({
createChunk,
handleInputChange,
searchString,
}) => {
}: ChunkResultBarProps) => {
const { t } = useTranslate('chunk');
const handleFilterChange = (e: RadioChangeEvent) => {
const [textSelectValue, setTextSelectValue] = useState<string | number>(
ChunkTextMode.Full,
);
const handleFilterChange = (e: string | number) => {
const value = e === -1 ? undefined : (e as number);
selectAllChunk(false);
handleSetAvailable(e.target.value);
handleSetAvailable(value);
};
const filterContent = (
<div className="w-[200px]">
<Radio.Group onChange={handleFilterChange} value={available}>
<Space direction="vertical">
<Radio value={undefined}>{t('all')}</Radio>
<div className="flex flex-col gap-2 p-4">
<Radio value={-1}>{t('all')}</Radio>
<Radio value={1}>{t('enabled')}</Radio>
<Radio value={0}>{t('disabled')}</Radio>
</Space>
</div>
</Radio.Group>
</div>
);
return (
<div className="flex pr-[25px]">
<Segmented
options={[
const textSelectOptions = [
{ label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
{ label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
]}
onChange={changeChunkTextMode as SegmentedProps['onChange']}
/>
];
const changeTextSelectValue = (value: string | number) => {
setTextSelectValue(value);
changeChunkTextMode(value);
};
return (
<div className="flex pr-[25px]">
<div className="flex items-center gap-4 bg-card text-muted-foreground w-fit h-[35px] rounded-md px-4 py-2 text-base">
{textSelectOptions.map((option) => (
<div
key={option.value}
className={cn('flex items-center cursor-pointer', {
'text-white': option.value === textSelectValue,
})}
onClick={() => changeTextSelectValue(option.value)}
>
{option.label}
</div>
))}
</div>
<div className="ml-auto"></div>
<Input
className="bg-card text-muted-foreground"
style={{ width: 200 }}
size="middle"
placeholder={t('search')}
prefix={<SearchOutlined />}
allowClear
icon={<SearchOutlined />}
onChange={handleInputChange}
value={searchString}
/>
<div className="w-[20px]"></div>
<Popover content={filterContent} placement="bottom" arrow={false}>
<Button icon={<FilterIcon />} />
<Popover>
<PopoverTrigger asChild>
<Button className="bg-card text-muted-foreground hover:bg-card">
<ListFilter />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[200px]">
{filterContent}
</PopoverContent>
</Popover>
<div className="w-[20px]"></div>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => createChunk()}
/>
<Button onClick={() => createChunk()}>
<Plus size={44} />
</Button>
</div>
);
};

View File

@ -20,12 +20,17 @@ import ChunkResultBar from './components/chunk-result-bar';
import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
import DocumentHeader from './components/document-preview/document-header';
import { PageHeader } from '@/components/page-header';
import message from '@/components/ui/message';
import {
RAGFlowPagination,
RAGFlowPaginationType,
} from '@/components/ui/ragflow-pagination';
import { Spin } from '@/components/ui/spin';
import {
QueryStringMap,
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import styles from './index.less';
const Chunk = () => {
@ -56,7 +61,7 @@ const Chunk = () => {
chunkUpdatingVisible,
documentId,
} = useUpdateChunk();
const { navigateToDataset, getQueryString } = useNavigatePage();
useEffect(() => {
setChunkList(data);
}, [data]);
@ -159,6 +164,10 @@ const Chunk = () => {
return (
<>
<PageHeader
title="Back"
back={navigateToDataset(getQueryString(QueryStringMap.id) as string)}
></PageHeader>
<div className={styles.chunkPage}>
<div className="flex flex-1 gap-8">
<div className="w-2/5">

View File

@ -61,9 +61,7 @@ export function useBulkOperateDataset({
const onChangeStatus = useCallback(
(enabled: boolean) => {
selectedRowKeys.forEach((id) => {
setDocumentStatus({ status: enabled, documentId: id });
});
setDocumentStatus({ status: enabled, documentId: selectedRowKeys });
},
[selectedRowKeys, setDocumentStatus],
);

View File

@ -10,7 +10,7 @@ export default ({
tab: 'generalForm' | 'chunkMethodForm';
parserId: string;
}) => {
const [visible, setVisible] = useState(true);
const [visible, setVisible] = useState(false);
return (
<div

View File

@ -23,7 +23,7 @@ export function NaiveConfiguration() {
<ChunkMethodItem></ChunkMethodItem>
<LayoutRecognizeFormField></LayoutRecognizeFormField>
<EmbeddingModelItem></EmbeddingModelItem>
<MaxTokenNumberFormField></MaxTokenNumberFormField>
<MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
</ConfigurationFormContainer>
<ConfigurationFormContainer>

View File

@ -22,7 +22,6 @@ export function GeneralForm() {
const { t } = useTranslation();
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
// const [submitLoading, setSubmitLoading] = useState(false); // submit button loading
const { saveKnowledgeConfiguration, loading: submitLoading } =
useUpdateKnowledge();
@ -94,7 +93,7 @@ export function GeneralForm() {
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
<div className="flex flex-col items-center">
<Upload />
<p>Upload</p>
<p>{t('common.upload')}</p>
</div>
</div>
) : (

View File

@ -131,22 +131,9 @@ export default function DatasetSettings() {
<ChunkMethodForm></ChunkMethodForm>
</TabsContent>
</Tabs>
{/* <div className="text-right">
<ButtonLoading type="submit">Submit</ButtonLoading>
</div> */}
</form>
</Form>
<ChunkMethodLearnMore tab={currentTab} parserId={parserId} />
{/* <div
style={{
display: currentTab === 'chunkMethodForm' ? 'block' : 'none',
}}
>
<Button variant="outline">Learn More</Button>
<div className="bg-[#FFF]/10 p-[20px] rounded-[12px] mt-[10px]">
<CategoryPanel chunkMethod={parserId}></CategoryPanel>
</div>
</div> */}
</div>
</section>
);