### What problem does this PR solve? Feat: Rendering recall test page #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| @@ -19,7 +19,7 @@ interface IProps { | |||
| leftPanel?: ReactNode; | |||
| } | |||
| const FilterButton = React.forwardRef< | |||
| export const FilterButton = React.forwardRef< | |||
| HTMLButtonElement, | |||
| ButtonProps & { count?: number } | |||
| >(({ count = 0, ...props }, ref) => { | |||
| @@ -4,7 +4,8 @@ import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks'; | |||
| import { Select as AntSelect, Form, message, Slider } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| 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 { | |||
| FormControl, | |||
| 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 { t } = useTranslate('knowledgeDetails'); | |||
| @@ -143,7 +152,7 @@ function RerankFormField() { | |||
| } | |||
| export function RerankFormFields() { | |||
| const { control, watch } = useFormContext(); | |||
| const { watch } = useFormContext(); | |||
| const { t } = useTranslate('knowledgeDetails'); | |||
| const rerankId = watch(RerankId); | |||
| @@ -151,23 +160,13 @@ export function RerankFormFields() { | |||
| <> | |||
| <RerankFormField></RerankFormField> | |||
| {rerankId && ( | |||
| <FormField | |||
| control={control} | |||
| <SliderInputFormField | |||
| name={'top_k'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('topKTip')}>{t('topK')}</FormLabel> | |||
| <FormControl> | |||
| <SingleFormSlider | |||
| {...field} | |||
| max={2048} | |||
| min={1} | |||
| ></SingleFormSlider> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| label={t('topK')} | |||
| max={2048} | |||
| min={1} | |||
| tooltip={t('topKTip')} | |||
| ></SliderInputFormField> | |||
| )} | |||
| </> | |||
| ); | |||
| @@ -1,15 +1,7 @@ | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { Form, Slider } from 'antd'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { SingleFormSlider } from '../ui/dual-range-slider'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '../ui/form'; | |||
| import { SliderInputFormField } from '../slider-input-form-field'; | |||
| type FieldType = { | |||
| similarity_threshold?: number; | |||
| @@ -73,51 +65,24 @@ export function SimilaritySliderFormField({ | |||
| vectorSimilarityWeightName = 'vector_similarity_weight', | |||
| isTooltipShown, | |||
| }: SimilaritySliderFormFieldProps) { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslate('knowledgeDetails'); | |||
| return ( | |||
| <> | |||
| <FormField | |||
| control={form.control} | |||
| <SliderInputFormField | |||
| name={'similarity_threshold'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={isTooltipShown && t('similarityThresholdTip')}> | |||
| {t('similarityThreshold')} | |||
| </FormLabel> | |||
| <FormControl> | |||
| <SingleFormSlider | |||
| {...field} | |||
| max={1} | |||
| step={0.01} | |||
| ></SingleFormSlider> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| label={t('similarityThreshold')} | |||
| max={1} | |||
| step={0.01} | |||
| tooltip={isTooltipShown && t('similarityThresholdTip')} | |||
| ></SliderInputFormField> | |||
| <SliderInputFormField | |||
| name={vectorSimilarityWeightName} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel | |||
| tooltip={isTooltipShown && t('vectorSimilarityWeightTip')} | |||
| > | |||
| {t('vectorSimilarityWeight')} | |||
| </FormLabel> | |||
| <FormControl> | |||
| <SingleFormSlider | |||
| {...field} | |||
| max={1} | |||
| step={0.01} | |||
| ></SingleFormSlider> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| label={t('vectorSimilarityWeight')} | |||
| max={1} | |||
| step={0.01} | |||
| tooltip={isTooltipShown && t('vectorSimilarityWeightTip')} | |||
| ></SliderInputFormField> | |||
| </> | |||
| ); | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import { ReactNode } from 'react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { SingleFormSlider } from './ui/dual-range-slider'; | |||
| @@ -18,6 +19,7 @@ type SliderInputFormFieldProps = { | |||
| label: string; | |||
| tooltip?: ReactNode; | |||
| defaultValue?: number; | |||
| className?: string; | |||
| }; | |||
| export function SliderInputFormField({ | |||
| @@ -28,6 +30,7 @@ export function SliderInputFormField({ | |||
| name, | |||
| tooltip, | |||
| defaultValue, | |||
| className, | |||
| }: SliderInputFormFieldProps) { | |||
| const form = useFormContext(); | |||
| @@ -39,7 +42,12 @@ export function SliderInputFormField({ | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <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> | |||
| <SingleFormSlider | |||
| {...field} | |||
| @@ -10,13 +10,12 @@ import kbService, { listDataset } from '@/services/knowledge-service'; | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { message } from 'antd'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
| import { useParams } from 'umi'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| } from './logic-hooks'; | |||
| import { useSetPaginationParams } from './route-hook'; | |||
| export const enum KnowledgeApiAction { | |||
| TestRetrieval = 'testRetrieval', | |||
| @@ -35,8 +34,17 @@ export const useKnowledgeBaseId = () => { | |||
| export const useTestRetrieval = () => { | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const { page, size: pageSize } = useSetPaginationParams(); | |||
| 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(() => { | |||
| return { | |||
| @@ -44,15 +52,16 @@ export const useTestRetrieval = () => { | |||
| kb_id: values?.kb_id || knowledgeBaseId, | |||
| page, | |||
| size: pageSize, | |||
| doc_ids: filterValue.doc_ids, | |||
| }; | |||
| }, [knowledgeBaseId, page, pageSize, values]); | |||
| }, [filterValue, knowledgeBaseId, page, pageSize, values]); | |||
| const { | |||
| data, | |||
| isFetching: loading, | |||
| refetch, | |||
| } = useQuery<INextTestingResult>({ | |||
| queryKey: [KnowledgeApiAction.TestRetrieval, queryParams], | |||
| queryKey: [KnowledgeApiAction.TestRetrieval, queryParams, page, pageSize], | |||
| initialData: { | |||
| chunks: [], | |||
| doc_aggs: [], | |||
| @@ -62,12 +71,27 @@ export const useTestRetrieval = () => { | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await kbService.retrieval_test(queryParams); | |||
| console.log('🚀 ~ queryFn: ~ 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 = () => { | |||
| @@ -50,7 +50,8 @@ const MarkdownContent = ({ | |||
| const { setDocumentIds, data: fileThumbnails } = | |||
| useFetchDocumentThumbnailsByIds(); | |||
| const contentWithCursor = useMemo(() => { | |||
| let text = DOMPurify.sanitize(content); | |||
| // let text = DOMPurify.sanitize(content); | |||
| let text = content; | |||
| if (text === '') { | |||
| text = t('chat.searching'); | |||
| } | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -5,6 +5,7 @@ import { DocumentParserType } from '@/constants/knowledge'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { TopTitle } from '../dataset-title'; | |||
| import CategoryPanel from './category-panel'; | |||
| import { ChunkMethodForm } from './chunk-method-form'; | |||
| import { formSchema } from './form-schema'; | |||
| @@ -74,13 +75,11 @@ export default function DatasetSettings() { | |||
| return ( | |||
| <section className="p-5 "> | |||
| <div className="pb-5"> | |||
| <div className="text-2xl font-semibold">Configuration</div> | |||
| <p className="text-text-sub-title pt-2"> | |||
| Update your knowledge base configuration here, particularly the chunk | |||
| method. | |||
| </p> | |||
| </div> | |||
| <TopTitle | |||
| title={'Configuration'} | |||
| description={` Update your knowledge base configuration here, particularly the chunk | |||
| method.`} | |||
| ></TopTitle> | |||
| <div className="flex gap-14"> | |||
| <Form {...form}> | |||
| <form | |||
| @@ -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 { useTestRetrieval } from '@/hooks/use-knowledge-request'; | |||
| import { ITestingChunk } from '@/interfaces/database/knowledge'; | |||
| import { camelCase } from 'lodash'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { useMemo } from 'react'; | |||
| import { TopTitle } from '../dataset-title'; | |||
| import TestingForm from './testing-form'; | |||
| 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 { t } = useTranslate('knowledgeDetails'); | |||
| return ( | |||
| <div className="flex gap-3 text-xs"> | |||
| <div className="flex gap-3 text-xs text-text-sub-title-invert italic"> | |||
| {similarityList.map((x) => ( | |||
| <div key={x.field} className="space-x-1"> | |||
| <span>{((item[x.field] as number) * 100).toFixed(2)}</span> | |||
| @@ -26,43 +34,83 @@ const ChunkTitle = ({ item }: { item: ITestingChunk }) => { | |||
| }; | |||
| 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 ( | |||
| <section className="flex divide-x h-full"> | |||
| <div className="p-4"> | |||
| <TestingForm | |||
| loading={loading} | |||
| setValues={setValues} | |||
| refetch={refetch} | |||
| ></TestingForm> | |||
| </div> | |||
| <div className="p-4 flex-1 "> | |||
| <h2 className="text-4xl font-bold mb-8 px-[10%]"> | |||
| 15 Results from 3 files | |||
| </h2> | |||
| <section className="flex flex-col gap-4 overflow-auto h-[83vh] px-[10%]"> | |||
| {data.chunks.map((x) => ( | |||
| <Card | |||
| key={x.chunk_id} | |||
| className="bg-colors-background-neutral-weak border-colors-outline-neutral-strong" | |||
| > | |||
| <CardHeader> | |||
| <CardTitle> | |||
| <div className="flex gap-2 flex-wrap"> | |||
| <ChunkTitle item={x}></ChunkTitle> | |||
| </div> | |||
| </CardTitle> | |||
| </CardHeader> | |||
| <CardContent> | |||
| <p className="text-colors-text-neutral-strong"> | |||
| {x.content_with_weight} | |||
| </p> | |||
| </CardContent> | |||
| </Card> | |||
| ))} | |||
| </section> | |||
| </div> | |||
| </section> | |||
| <div className="p-5"> | |||
| <section className="flex justify-between items-center"> | |||
| <TopTitle | |||
| title={'Configuration'} | |||
| description={` Update your knowledge base configuration here, particularly the chunk | |||
| method.`} | |||
| ></TopTitle> | |||
| <Button>Save as Preset</Button> | |||
| </section> | |||
| <section className="flex divide-x h-full"> | |||
| <div className="p-4 flex-1"> | |||
| <div className="flex justify-between pb-2.5"> | |||
| <span className="text-text-title font-semibold text-2xl"> | |||
| Test setting | |||
| </span> | |||
| <Button variant={'outline'}> | |||
| <Plus /> Add New Test | |||
| </Button> | |||
| </div> | |||
| <TestingForm | |||
| loading={loading} | |||
| setValues={setValues} | |||
| refetch={refetch} | |||
| ></TestingForm> | |||
| </div> | |||
| <div className="p-4 flex-1"> | |||
| <div className="flex justify-between pb-2.5"> | |||
| <span className="text-text-title font-semibold text-2xl"> | |||
| Test results | |||
| </span> | |||
| <FilterPopover filters={filters} onChange={handleFilterSubmit}> | |||
| <FilterButton></FilterButton> | |||
| </FilterPopover> | |||
| </div> | |||
| <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> | |||
| ); | |||
| } | |||
| @@ -4,7 +4,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { RerankFormFields } from '@/components/rerank'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { | |||
| initialTopKValue, | |||
| RerankFormFields, | |||
| topKSchema, | |||
| } from '@/components/rerank'; | |||
| import { | |||
| initialKeywordsSimilarityWeightValue, | |||
| initialSimilarityThresholdValue, | |||
| @@ -12,6 +17,7 @@ import { | |||
| SimilaritySliderFormField, | |||
| similarityThresholdSchema, | |||
| } from '@/components/similarity-slider'; | |||
| import { ButtonLoading } from '@/components/ui/button'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| @@ -20,7 +26,6 @@ import { | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { LoadingButton } from '@/components/ui/loading-button'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; | |||
| import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | |||
| @@ -46,6 +51,7 @@ export default function TestingForm({ | |||
| }), | |||
| ...similarityThresholdSchema, | |||
| ...keywordsSimilarityWeightSchema, | |||
| ...topKSchema, | |||
| }); | |||
| const form = useForm<z.infer<typeof formSchema>>({ | |||
| @@ -53,6 +59,7 @@ export default function TestingForm({ | |||
| defaultValues: { | |||
| ...initialSimilarityThresholdValue, | |||
| ...initialKeywordsSimilarityWeightValue, | |||
| ...initialTopKValue, | |||
| }, | |||
| }); | |||
| @@ -71,12 +78,14 @@ export default function TestingForm({ | |||
| return ( | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> | |||
| <SimilaritySliderFormField | |||
| vectorSimilarityWeightName="keywords_similarity_weight" | |||
| isTooltipShown | |||
| ></SimilaritySliderFormField> | |||
| <RerankFormFields></RerankFormFields> | |||
| <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> | |||
| <FormContainer className="p-10"> | |||
| <SimilaritySliderFormField | |||
| vectorSimilarityWeightName="keywords_similarity_weight" | |||
| isTooltipShown | |||
| ></SimilaritySliderFormField> | |||
| <RerankFormFields></RerankFormFields> | |||
| <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> | |||
| </FormContainer> | |||
| <FormField | |||
| control={form.control} | |||
| name="question" | |||
| @@ -94,16 +103,13 @@ export default function TestingForm({ | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <LoadingButton | |||
| variant={'tertiary'} | |||
| size={'sm'} | |||
| <ButtonLoading | |||
| type="submit" | |||
| className="w-full" | |||
| disabled={!!!trim(question)} | |||
| loading={loading} | |||
| > | |||
| {t('knowledgeDetails.testingLabel')} | |||
| </LoadingButton> | |||
| </ButtonLoading> | |||
| </form> | |||
| </Form> | |||
| ); | |||