Feature/1217 (#12087)

### What problem does this PR solve?

feature: Complete metadata functionality

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-12-22 17:35:12 +08:00
committed by GitHub
parent 993bf7c2c8
commit 51b12841d6
9 changed files with 310 additions and 68 deletions

View File

@ -0,0 +1,170 @@
import { Checkbox } from '@/components/ui/checkbox';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { memo, useState } from 'react';
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
import { FormControl, FormField, FormItem, FormLabel } from '../ui/form';
import { FilterType } from './interface';
const handleCheckChange = ({
checked,
field,
item,
isNestedField = false,
parentId = '',
}: {
checked: boolean;
field: ControllerRenderProps<
{ [x: string]: { [x: string]: any } | string[] },
string
>;
item: FilterType;
isNestedField?: boolean;
parentId?: string;
}) => {
if (isNestedField && parentId) {
const currentValue = field.value || {};
const currentParentValues =
(currentValue as Record<string, string[]>)[parentId] || [];
const newParentValues = checked
? [...currentParentValues, item.id.toString()]
: currentParentValues.filter(
(value: string) => value !== item.id.toString(),
);
const newValue = {
...currentValue,
[parentId]: newParentValues,
};
if (newValue[parentId].length === 0) {
delete newValue[parentId];
}
return field.onChange(newValue);
} else {
const list = checked
? [...(Array.isArray(field.value) ? field.value : []), item.id.toString()]
: (Array.isArray(field.value) ? field.value : []).filter(
(value) => value !== item.id.toString(),
);
return field.onChange(list);
}
};
const FilterItem = memo(
({
item,
field,
level = 0,
}: {
item: FilterType;
field: ControllerRenderProps<
{ [x: string]: { [x: string]: any } | string[] },
string
>;
level: number;
}) => {
return (
<div
className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-4' : ''}`}
>
<FormItem className="flex flex-row space-x-3 space-y-0 items-center">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id.toString())}
onCheckedChange={(checked: boolean) =>
handleCheckChange({ checked, field, item })
}
/>
</FormControl>
<FormLabel onClick={(e) => e.stopPropagation()}>
{item.label}
</FormLabel>
</FormItem>
{item.count !== undefined && (
<span className="text-sm">{item.count}</span>
)}
</div>
);
},
);
export const FilterField = memo(
({
item,
parent,
level = 0,
}: {
item: FilterType;
parent: FilterType;
level?: number;
}) => {
const form = useFormContext();
const [showAll, setShowAll] = useState(false);
const hasNestedList = item.list && item.list.length > 0;
return (
<FormField
key={item.id}
control={form.control}
name={parent.field as string}
render={({ field }) => {
if (hasNestedList) {
return (
<div className={`flex flex-col gap-2 ${level > 0 ? 'ml-4' : ''}`}>
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => {
setShowAll(!showAll);
}}
>
<FormLabel className="text-text-primary">
{item.label}
</FormLabel>
{showAll ? (
<ChevronUp size={12} />
) : (
<ChevronDown size={12} />
)}
</div>
{showAll &&
item.list?.map((child) => (
<FilterField
key={child.id}
item={child}
parent={{
...item,
field: `${parent.field}.${item.field}`,
}}
level={level + 1}
/>
// <FilterItem key={child.id} item={child} field={child.field} level={level+1} />
// <div
// className="flex flex-row space-x-3 space-y-0 items-center"
// key={child.id}
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes(child.id.toString())}
// onCheckedChange={(checked) =>
// handleCheckChange({ checked, field, item: child })
// }
// />
// </FormControl>
// <FormLabel onClick={(e) => e.stopPropagation()}>
// {child.label}
// </FormLabel>
// </div>
))}
</div>
);
}
return <FilterItem item={item} field={field} level={level} />;
}}
/>
);
},
);
FilterField.displayName = 'FilterField';

View File

@ -4,21 +4,27 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; import {
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ZodArray, ZodString, z } from 'zod'; import { ZodArray, ZodString, z } from 'zod';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { import {
Form, Form,
FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { t } from 'i18next'; import { t } from 'i18next';
import { FilterField } from './filter-field';
import { FilterChange, FilterCollection, FilterValue } from './interface'; import { FilterChange, FilterCollection, FilterValue } from './interface';
export type CheckboxFormMultipleProps = { export type CheckboxFormMultipleProps = {
@ -35,29 +41,71 @@ function CheckboxFormMultiple({
onChange, onChange,
setOpen, setOpen,
}: CheckboxFormMultipleProps) { }: CheckboxFormMultipleProps) {
const fieldsDict = filters?.reduce<Record<string, Array<any>>>((pre, cur) => { const [resolvedFilters, setResolvedFilters] =
pre[cur.field] = []; useState<FilterCollection[]>(filters);
return pre;
}, {});
const FormSchema = z.object( useEffect(() => {
filters.reduce<Record<string, ZodArray<ZodString, 'many'>>>((pre, cur) => { if (filters && filters.length > 0) {
pre[cur.field] = z.array(z.string()); setResolvedFilters(filters);
}
}, [filters]);
// .refine((value) => value.some((item) => item), { const fieldsDict = useMemo(() => {
// message: 'You have to select at least one item.', if (resolvedFilters.length === 0) {
// }); return {};
}
return resolvedFilters.reduce<Record<string, any>>((pre, cur) => {
const hasNested = cur.list?.some(
(item) => item.list && item.list.length > 0,
);
if (hasNested) {
pre[cur.field] = {};
} else {
pre[cur.field] = [];
}
return pre; return pre;
}, {}), }, {});
); }, [resolvedFilters]);
const FormSchema = useMemo(() => {
if (resolvedFilters.length === 0) {
return z.object({});
}
return z.object(
resolvedFilters.reduce<
Record<
string,
ZodArray<ZodString, 'many'> | z.ZodObject<any> | z.ZodOptional<any>
>
>((pre, cur) => {
const hasNested = cur.list?.some(
(item) => item.list && item.list.length > 0,
);
if (hasNested) {
pre[cur.field] = z
.record(z.string(), z.array(z.string().optional()).optional())
.optional();
} else {
pre[cur.field] = z.array(z.string().optional()).optional();
}
return pre;
}, {}),
);
}, [resolvedFilters]);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined,
defaultValues: fieldsDict, defaultValues: fieldsDict,
}); });
function onSubmit(data: z.infer<typeof FormSchema>) { function onSubmit() {
onChange?.(data); const formValues = form.getValues();
onChange?.({ ...formValues });
setOpen(false); setOpen(false);
} }
@ -67,8 +115,10 @@ function CheckboxFormMultiple({
}, [fieldsDict, onChange, setOpen]); }, [fieldsDict, onChange, setOpen]);
useEffect(() => { useEffect(() => {
form.reset(value); if (resolvedFilters.length > 0) {
}, [form, value]); form.reset(value || fieldsDict);
}
}, [form, value, resolvedFilters, fieldsDict]);
return ( return (
<Form {...form}> <Form {...form}>
@ -85,44 +135,21 @@ function CheckboxFormMultiple({
render={() => ( render={() => (
<FormItem className="space-y-4"> <FormItem className="space-y-4">
<div> <div>
<FormLabel className="text-base text-text-sub-title-invert"> <FormLabel className="text-text-primary">{x.label}</FormLabel>
{x.label}
</FormLabel>
</div> </div>
{x.list.map((item) => ( {x.list.map((item) => {
<FormField return (
key={item.id} <FilterField
control={form.control} key={item.id}
name={x.field} item={{ ...item }}
render={({ field }) => { parent={{
return ( ...x,
<div className="flex items-center justify-between text-text-primary text-xs"> id: x.field,
<FormItem // field: `${x.field}${item.field ? '.' + item.field : ''}`,
key={item.id} }}
className="flex flex-row space-x-3 space-y-0 items-center " />
> );
<FormControl> })}
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id,
),
);
}}
/>
</FormControl>
<FormLabel>{item.label}</FormLabel>
</FormItem>
<span className=" text-sm">{item.count}</span>
</div>
);
}}
/>
))}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -137,7 +164,13 @@ function CheckboxFormMultiple({
> >
{t('common.clear')} {t('common.clear')}
</Button> </Button>
<Button type="submit" size={'sm'}> <Button
type="submit"
onClick={() => {
console.log(form.formState.errors, form.getValues());
}}
size={'sm'}
>
{t('common.submit')} {t('common.submit')}
</Button> </Button>
</div> </div>

View File

@ -17,6 +17,7 @@ interface IProps {
onSearchChange?: ChangeEventHandler<HTMLInputElement>; onSearchChange?: ChangeEventHandler<HTMLInputElement>;
showFilter?: boolean; showFilter?: boolean;
leftPanel?: ReactNode; leftPanel?: ReactNode;
preChildren?: ReactNode;
} }
export const FilterButton = React.forwardRef< export const FilterButton = React.forwardRef<
@ -46,6 +47,7 @@ export const FilterButton = React.forwardRef<
export default function ListFilterBar({ export default function ListFilterBar({
title, title,
children, children,
preChildren,
searchString, searchString,
onSearchChange, onSearchChange,
showFilter = true, showFilter = true,
@ -63,7 +65,18 @@ export default function ListFilterBar({
const filterCount = useMemo(() => { const filterCount = useMemo(() => {
return typeof value === 'object' && value !== null return typeof value === 'object' && value !== null
? Object.values(value).reduce((pre, cur) => { ? Object.values(value).reduce((pre, cur) => {
return pre + cur.length; if (Array.isArray(cur)) {
return pre + cur.length;
}
if (typeof cur === 'object') {
return (
pre +
Object.values(cur).reduce((pre, cur) => {
return pre + cur.length;
}, 0)
);
}
return pre;
}, 0) }, 0)
: 0; : 0;
}, [value]); }, [value]);
@ -80,6 +93,7 @@ export default function ListFilterBar({
{leftPanel || title} {leftPanel || title}
</div> </div>
<div className="flex gap-5 items-center"> <div className="flex gap-5 items-center">
{preChildren}
{showFilter && ( {showFilter && (
<FilterPopover <FilterPopover
value={value} value={value}

View File

@ -1,6 +1,9 @@
export type FilterType = { export type FilterType = {
id: string; id: string;
field?: string;
label: string | JSX.Element; label: string | JSX.Element;
list?: FilterType[];
value?: string | string[];
count?: number; count?: number;
}; };
@ -10,6 +13,9 @@ export type FilterCollection = {
list: FilterType[]; list: FilterType[];
}; };
export type FilterValue = Record<string, Array<string>>; export type FilterValue = Record<
string,
Array<string> | Record<string, Array<string>>
>;
export type FilterChange = (value: FilterValue) => void; export type FilterChange = (value: FilterValue) => void;

View File

@ -124,6 +124,7 @@ export const useFetchDocumentList = () => {
{ {
suffix: filterValue.type, suffix: filterValue.type,
run_status: filterValue.run, run_status: filterValue.run,
metadata: filterValue.metadata,
}, },
); );
if (ret.data.code === 0) { if (ret.data.code === 0) {
@ -196,6 +197,7 @@ export const useGetDocumentFilter = (): {
filter: data?.filter || { filter: data?.filter || {
run_status: {}, run_status: {},
suffix: {}, suffix: {},
metadata: {},
}, },
onOpenChange: handleOnpenChange, onOpenChange: handleOnpenChange,
}; };

View File

@ -60,4 +60,5 @@ interface GraphRag {
export type IDocumentInfoFilter = { export type IDocumentInfoFilter = {
run_status: Record<number, number>; run_status: Record<number, number>;
suffix: Record<string, number>; suffix: Record<string, number>;
metadata: Record<string, Record<string, number>>;
}; };

View File

@ -204,7 +204,8 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
</div> </div>
</div> </div>
)} )}
{metaData.restrictDefinedValues && ( {((metaData.restrictDefinedValues && isShowValueSwitch) ||
!isShowValueSwitch) && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div>{t('knowledgeDetails.metadata.values')}</div> <div>{t('knowledgeDetails.metadata.values')}</div>

View File

@ -216,12 +216,6 @@ const Generate: React.FC<GenerateProps> = (props) => {
? graphRunData ? graphRunData
: raptorRunData : raptorRunData
) as ITraceInfo; ) as ITraceInfo;
console.log(
name,
'data',
data,
!data || (!data.progress && data.progress !== 0),
);
return ( return (
<div key={name}> <div key={name}>
<MenuItem <MenuItem

View File

@ -25,12 +25,33 @@ export function useSelectDatasetFilters() {
})); }));
} }
}, [filter.run_status, t]); }, [filter.run_status, t]);
const metaDataList = useMemo(() => {
if (filter.metadata) {
return Object.keys(filter.metadata).map((x) => ({
id: x.toString(),
field: x.toString(),
label: x.toString(),
list: Object.keys(filter.metadata[x]).map((y) => ({
id: y.toString(),
field: y.toString(),
label: y.toString(),
value: [y],
count: filter.metadata[x][y],
})),
count: Object.keys(filter.metadata[x]).reduce(
(acc, cur) => acc + filter.metadata[x][cur],
0,
),
}));
}
}, [filter.metadata]);
const filters: FilterCollection[] = useMemo(() => { const filters: FilterCollection[] = useMemo(() => {
return [ return [
{ field: 'type', label: 'File Type', list: fileTypes }, { field: 'type', label: 'File Type', list: fileTypes },
{ field: 'run', label: 'Status', list: fileStatus }, { field: 'run', label: 'Status', list: fileStatus },
{ field: 'metadata', label: 'metadata', list: metaDataList },
] as FilterCollection[]; ] as FilterCollection[];
}, [fileStatus, fileTypes]); }, [fileStatus, fileTypes, metaDataList]);
return { filters, onOpenChange }; return { filters, onOpenChange };
} }