Feat: Rendering recall test page #3221 (#7689)

### What problem does this PR solve?

Feat: Rendering recall test page #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-05-16 18:56:48 +08:00 committed by GitHub
parent d73a08b9eb
commit bfaa469b9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 199 additions and 134 deletions

View File

@ -19,7 +19,7 @@ interface IProps {
leftPanel?: ReactNode; leftPanel?: ReactNode;
} }
const FilterButton = React.forwardRef< export const FilterButton = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
ButtonProps & { count?: number } ButtonProps & { count?: number }
>(({ count = 0, ...props }, ref) => { >(({ count = 0, ...props }, ref) => {

View File

@ -4,7 +4,8 @@ import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
import { Select as AntSelect, Form, message, Slider } from 'antd'; import { Select as AntSelect, Form, message, Slider } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { SingleFormSlider } from './ui/dual-range-slider'; import { z } from 'zod';
import { SliderInputFormField } from './slider-input-form-field';
import { import {
FormControl, FormControl,
FormField, FormField,
@ -63,6 +64,14 @@ export const RerankItem = () => {
); );
}; };
export const topKSchema = {
top_k: z.number().optional(),
};
export const initialTopKValue = {
top_k: 1024,
};
const Rerank = () => { const Rerank = () => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
@ -143,7 +152,7 @@ function RerankFormField() {
} }
export function RerankFormFields() { export function RerankFormFields() {
const { control, watch } = useFormContext(); const { watch } = useFormContext();
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
const rerankId = watch(RerankId); const rerankId = watch(RerankId);
@ -151,23 +160,13 @@ export function RerankFormFields() {
<> <>
<RerankFormField></RerankFormField> <RerankFormField></RerankFormField>
{rerankId && ( {rerankId && (
<FormField <SliderInputFormField
control={control}
name={'top_k'} name={'top_k'}
render={({ field }) => ( label={t('topK')}
<FormItem> max={2048}
<FormLabel tooltip={t('topKTip')}>{t('topK')}</FormLabel> min={1}
<FormControl> tooltip={t('topKTip')}
<SingleFormSlider ></SliderInputFormField>
{...field}
max={2048}
min={1}
></SingleFormSlider>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)} )}
</> </>
); );

View File

@ -1,15 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { Form, Slider } from 'antd'; import { Form, Slider } from 'antd';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { SingleFormSlider } from '../ui/dual-range-slider'; import { SliderInputFormField } from '../slider-input-form-field';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
type FieldType = { type FieldType = {
similarity_threshold?: number; similarity_threshold?: number;
@ -73,51 +65,24 @@ export function SimilaritySliderFormField({
vectorSimilarityWeightName = 'vector_similarity_weight', vectorSimilarityWeightName = 'vector_similarity_weight',
isTooltipShown, isTooltipShown,
}: SimilaritySliderFormFieldProps) { }: SimilaritySliderFormFieldProps) {
const form = useFormContext();
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
return ( return (
<> <>
<FormField <SliderInputFormField
control={form.control}
name={'similarity_threshold'} name={'similarity_threshold'}
render={({ field }) => ( label={t('similarityThreshold')}
<FormItem> max={1}
<FormLabel tooltip={isTooltipShown && t('similarityThresholdTip')}> step={0.01}
{t('similarityThreshold')} tooltip={isTooltipShown && t('similarityThresholdTip')}
</FormLabel> ></SliderInputFormField>
<FormControl> <SliderInputFormField
<SingleFormSlider
{...field}
max={1}
step={0.01}
></SingleFormSlider>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={vectorSimilarityWeightName} name={vectorSimilarityWeightName}
render={({ field }) => ( label={t('vectorSimilarityWeight')}
<FormItem> max={1}
<FormLabel step={0.01}
tooltip={isTooltipShown && t('vectorSimilarityWeightTip')} tooltip={isTooltipShown && t('vectorSimilarityWeightTip')}
> ></SliderInputFormField>
{t('vectorSimilarityWeight')}
</FormLabel>
<FormControl>
<SingleFormSlider
{...field}
max={1}
step={0.01}
></SingleFormSlider>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</> </>
); );
} }

View File

@ -1,3 +1,4 @@
import { cn } from '@/lib/utils';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { SingleFormSlider } from './ui/dual-range-slider'; import { SingleFormSlider } from './ui/dual-range-slider';
@ -18,6 +19,7 @@ type SliderInputFormFieldProps = {
label: string; label: string;
tooltip?: ReactNode; tooltip?: ReactNode;
defaultValue?: number; defaultValue?: number;
className?: string;
}; };
export function SliderInputFormField({ export function SliderInputFormField({
@ -28,6 +30,7 @@ export function SliderInputFormField({
name, name,
tooltip, tooltip,
defaultValue, defaultValue,
className,
}: SliderInputFormFieldProps) { }: SliderInputFormFieldProps) {
const form = useFormContext(); const form = useFormContext();
@ -39,7 +42,12 @@ export function SliderInputFormField({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel tooltip={tooltip}>{label}</FormLabel> <FormLabel tooltip={tooltip}>{label}</FormLabel>
<div className="flex items-center gap-14 justify-between"> <div
className={cn(
'flex items-center gap-14 justify-between',
className,
)}
>
<FormControl> <FormControl>
<SingleFormSlider <SingleFormSlider
{...field} {...field}

View File

@ -10,13 +10,12 @@ import kbService, { listDataset } from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks'; import { useDebounce } from 'ahooks';
import { message } from 'antd'; import { message } from 'antd';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { import {
useGetPaginationWithRouter, useGetPaginationWithRouter,
useHandleSearchChange, useHandleSearchChange,
} from './logic-hooks'; } from './logic-hooks';
import { useSetPaginationParams } from './route-hook';
export const enum KnowledgeApiAction { export const enum KnowledgeApiAction {
TestRetrieval = 'testRetrieval', TestRetrieval = 'testRetrieval',
@ -35,8 +34,17 @@ export const useKnowledgeBaseId = () => {
export const useTestRetrieval = () => { export const useTestRetrieval = () => {
const knowledgeBaseId = useKnowledgeBaseId(); const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const [values, setValues] = useState<ITestRetrievalRequestBody>(); const [values, setValues] = useState<ITestRetrievalRequestBody>();
const mountedRef = useRef(false);
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const onPaginationChange = useCallback((page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
}, []);
const queryParams = useMemo(() => { const queryParams = useMemo(() => {
return { return {
@ -44,15 +52,16 @@ export const useTestRetrieval = () => {
kb_id: values?.kb_id || knowledgeBaseId, kb_id: values?.kb_id || knowledgeBaseId,
page, page,
size: pageSize, size: pageSize,
doc_ids: filterValue.doc_ids,
}; };
}, [knowledgeBaseId, page, pageSize, values]); }, [filterValue, knowledgeBaseId, page, pageSize, values]);
const { const {
data, data,
isFetching: loading, isFetching: loading,
refetch, refetch,
} = useQuery<INextTestingResult>({ } = useQuery<INextTestingResult>({
queryKey: [KnowledgeApiAction.TestRetrieval, queryParams], queryKey: [KnowledgeApiAction.TestRetrieval, queryParams, page, pageSize],
initialData: { initialData: {
chunks: [], chunks: [],
doc_aggs: [], doc_aggs: [],
@ -62,12 +71,27 @@ export const useTestRetrieval = () => {
gcTime: 0, gcTime: 0,
queryFn: async () => { queryFn: async () => {
const { data } = await kbService.retrieval_test(queryParams); const { data } = await kbService.retrieval_test(queryParams);
console.log('🚀 ~ queryFn: ~ data:', data);
return data?.data ?? {}; return data?.data ?? {};
}, },
}); });
return { data, loading, setValues, refetch }; useEffect(() => {
if (mountedRef.current) {
refetch();
}
mountedRef.current = true;
}, [page, pageSize, refetch, filterValue]);
return {
data,
loading,
setValues,
refetch,
onPaginationChange,
page,
pageSize,
handleFilterSubmit,
};
}; };
export const useFetchNextKnowledgeListByPage = () => { export const useFetchNextKnowledgeListByPage = () => {

View File

@ -50,7 +50,8 @@ const MarkdownContent = ({
const { setDocumentIds, data: fileThumbnails } = const { setDocumentIds, data: fileThumbnails } =
useFetchDocumentThumbnailsByIds(); useFetchDocumentThumbnailsByIds();
const contentWithCursor = useMemo(() => { const contentWithCursor = useMemo(() => {
let text = DOMPurify.sanitize(content); // let text = DOMPurify.sanitize(content);
let text = content;
if (text === '') { if (text === '') {
text = t('chat.searching'); text = t('chat.searching');
} }

View File

@ -0,0 +1,15 @@
import { ReactNode } from 'react';
type TopTitleProps = {
title: ReactNode;
description: ReactNode;
};
export function TopTitle({ title, description }: TopTitleProps) {
return (
<div className="pb-5">
<div className="text-2xl font-semibold">{title}</div>
<p className="text-text-sub-title pt-2">{description}</p>
</div>
);
}

View File

@ -5,6 +5,7 @@ import { DocumentParserType } from '@/constants/knowledge';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { TopTitle } from '../dataset-title';
import CategoryPanel from './category-panel'; import CategoryPanel from './category-panel';
import { ChunkMethodForm } from './chunk-method-form'; import { ChunkMethodForm } from './chunk-method-form';
import { formSchema } from './form-schema'; import { formSchema } from './form-schema';
@ -74,13 +75,11 @@ export default function DatasetSettings() {
return ( return (
<section className="p-5 "> <section className="p-5 ">
<div className="pb-5"> <TopTitle
<div className="text-2xl font-semibold">Configuration</div> title={'Configuration'}
<p className="text-text-sub-title pt-2"> description={` Update your knowledge base configuration here, particularly the chunk
Update your knowledge base configuration here, particularly the chunk method.`}
method. ></TopTitle>
</p>
</div>
<div className="flex gap-14"> <div className="flex gap-14">
<Form {...form}> <Form {...form}>
<form <form

View File

@ -1,8 +1,16 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { FormContainer } from '@/components/form-container';
import { FilterButton } from '@/components/list-filter-bar';
import { FilterPopover } from '@/components/list-filter-bar/filter-popover';
import { FilterCollection } from '@/components/list-filter-bar/interface';
import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useTestRetrieval } from '@/hooks/use-knowledge-request'; import { useTestRetrieval } from '@/hooks/use-knowledge-request';
import { ITestingChunk } from '@/interfaces/database/knowledge'; import { ITestingChunk } from '@/interfaces/database/knowledge';
import { camelCase } from 'lodash'; import { camelCase } from 'lodash';
import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { TopTitle } from '../dataset-title';
import TestingForm from './testing-form'; import TestingForm from './testing-form';
const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [ const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [
@ -14,7 +22,7 @@ const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [
const ChunkTitle = ({ item }: { item: ITestingChunk }) => { const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
return ( return (
<div className="flex gap-3 text-xs"> <div className="flex gap-3 text-xs text-text-sub-title-invert italic">
{similarityList.map((x) => ( {similarityList.map((x) => (
<div key={x.field} className="space-x-1"> <div key={x.field} className="space-x-1">
<span>{((item[x.field] as number) * 100).toFixed(2)}</span> <span>{((item[x.field] as number) * 100).toFixed(2)}</span>
@ -26,43 +34,83 @@ const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
}; };
export default function RetrievalTesting() { export default function RetrievalTesting() {
const { loading, setValues, refetch, data } = useTestRetrieval(); const {
loading,
setValues,
refetch,
data,
onPaginationChange,
page,
pageSize,
handleFilterSubmit,
} = useTestRetrieval();
const filters: FilterCollection[] = useMemo(() => {
return [
{
field: 'doc_ids',
label: 'File',
list:
data.doc_aggs?.map((x) => ({
id: x.doc_id,
label: x.doc_name,
count: x.count,
})) ?? [],
},
];
}, [data.doc_aggs]);
return ( return (
<section className="flex divide-x h-full"> <div className="p-5">
<div className="p-4"> <section className="flex justify-between items-center">
<TestingForm <TopTitle
loading={loading} title={'Configuration'}
setValues={setValues} description={` Update your knowledge base configuration here, particularly the chunk
refetch={refetch} method.`}
></TestingForm> ></TopTitle>
</div> <Button>Save as Preset</Button>
<div className="p-4 flex-1 "> </section>
<h2 className="text-4xl font-bold mb-8 px-[10%]"> <section className="flex divide-x h-full">
15 Results from 3 files <div className="p-4 flex-1">
</h2> <div className="flex justify-between pb-2.5">
<section className="flex flex-col gap-4 overflow-auto h-[83vh] px-[10%]"> <span className="text-text-title font-semibold text-2xl">
{data.chunks.map((x) => ( Test setting
<Card </span>
key={x.chunk_id} <Button variant={'outline'}>
className="bg-colors-background-neutral-weak border-colors-outline-neutral-strong" <Plus /> Add New Test
> </Button>
<CardHeader> </div>
<CardTitle> <TestingForm
<div className="flex gap-2 flex-wrap"> loading={loading}
<ChunkTitle item={x}></ChunkTitle> setValues={setValues}
</div> refetch={refetch}
</CardTitle> ></TestingForm>
</CardHeader> </div>
<CardContent> <div className="p-4 flex-1">
<p className="text-colors-text-neutral-strong"> <div className="flex justify-between pb-2.5">
{x.content_with_weight} <span className="text-text-title font-semibold text-2xl">
</p> Test results
</CardContent> </span>
</Card> <FilterPopover filters={filters} onChange={handleFilterSubmit}>
))} <FilterButton></FilterButton>
</section> </FilterPopover>
</div> </div>
</section> <section className="flex flex-col gap-5 overflow-auto h-[76vh] mb-5">
{data.chunks?.map((x) => (
<FormContainer key={x.chunk_id} className="px-5 py-2.5">
<ChunkTitle item={x}></ChunkTitle>
<p className="!mt-2.5"> {x.content_with_weight}</p>
</FormContainer>
))}
</section>
<RAGFlowPagination
total={data.total}
onChange={onPaginationChange}
current={page}
pageSize={pageSize}
></RAGFlowPagination>
</div>
</section>
</div>
); );
} }

View File

@ -4,7 +4,12 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { RerankFormFields } from '@/components/rerank'; import { FormContainer } from '@/components/form-container';
import {
initialTopKValue,
RerankFormFields,
topKSchema,
} from '@/components/rerank';
import { import {
initialKeywordsSimilarityWeightValue, initialKeywordsSimilarityWeightValue,
initialSimilarityThresholdValue, initialSimilarityThresholdValue,
@ -12,6 +17,7 @@ import {
SimilaritySliderFormField, SimilaritySliderFormField,
similarityThresholdSchema, similarityThresholdSchema,
} from '@/components/similarity-slider'; } from '@/components/similarity-slider';
import { ButtonLoading } from '@/components/ui/button';
import { import {
Form, Form,
FormControl, FormControl,
@ -20,7 +26,6 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { LoadingButton } from '@/components/ui/loading-button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item';
import { useTestRetrieval } from '@/hooks/use-knowledge-request'; import { useTestRetrieval } from '@/hooks/use-knowledge-request';
@ -46,6 +51,7 @@ export default function TestingForm({
}), }),
...similarityThresholdSchema, ...similarityThresholdSchema,
...keywordsSimilarityWeightSchema, ...keywordsSimilarityWeightSchema,
...topKSchema,
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -53,6 +59,7 @@ export default function TestingForm({
defaultValues: { defaultValues: {
...initialSimilarityThresholdValue, ...initialSimilarityThresholdValue,
...initialKeywordsSimilarityWeightValue, ...initialKeywordsSimilarityWeightValue,
...initialTopKValue,
}, },
}); });
@ -71,12 +78,14 @@ export default function TestingForm({
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<SimilaritySliderFormField <FormContainer className="p-10">
vectorSimilarityWeightName="keywords_similarity_weight" <SimilaritySliderFormField
isTooltipShown vectorSimilarityWeightName="keywords_similarity_weight"
></SimilaritySliderFormField> isTooltipShown
<RerankFormFields></RerankFormFields> ></SimilaritySliderFormField>
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> <RerankFormFields></RerankFormFields>
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField>
</FormContainer>
<FormField <FormField
control={form.control} control={form.control}
name="question" name="question"
@ -94,16 +103,13 @@ export default function TestingForm({
</FormItem> </FormItem>
)} )}
/> />
<LoadingButton <ButtonLoading
variant={'tertiary'}
size={'sm'}
type="submit" type="submit"
className="w-full"
disabled={!!!trim(question)} disabled={!!!trim(question)}
loading={loading} loading={loading}
> >
{t('knowledgeDetails.testingLabel')} {t('knowledgeDetails.testingLabel')}
</LoadingButton> </ButtonLoading>
</form> </form>
</Form> </Form>
); );