From 5248fcca561da176cca4135975c39a0fb739e21c Mon Sep 17 00:00:00 2001 From: twwu Date: Fri, 27 Jun 2025 15:56:38 +0800 Subject: [PATCH] feat: implement support for single and multiple choice in crawled result items --- web/app/components/base/radio/ui.tsx | 19 +- .../online-documents/page-selector/index.tsx | 332 ++++++++++++++++++ .../base/crawled-result-item.tsx | 23 +- .../website-crawl/base/crawled-result.tsx | 22 +- .../website-crawl/base/crawler.tsx | 1 + 5 files changed, 381 insertions(+), 16 deletions(-) create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source/online-documents/page-selector/index.tsx diff --git a/web/app/components/base/radio/ui.tsx b/web/app/components/base/radio/ui.tsx index 178262d0b9..19778c7cc3 100644 --- a/web/app/components/base/radio/ui.tsx +++ b/web/app/components/base/radio/ui.tsx @@ -5,14 +5,29 @@ import cn from '@/utils/classnames' type Props = { isChecked: boolean + disabled?: boolean + onCheck?: () => void } const RadioUI: FC = ({ isChecked, + disabled = false, + onCheck, }) => { return ( -
-
+
{ + onCheck?.() + }} + /> ) } export default React.memo(RadioUI) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/online-documents/page-selector/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/online-documents/page-selector/index.tsx new file mode 100644 index 0000000000..37710201c7 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/online-documents/page-selector/index.tsx @@ -0,0 +1,332 @@ +import { memo, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FixedSizeList as List, areEqual } from 'react-window' +import type { ListChildComponentProps } from 'react-window' +import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' +import Checkbox from '@/app/components/base/checkbox' +import NotionIcon from '@/app/components/base/notion-icon' +import cn from '@/utils/classnames' +import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' + +type PageSelectorProps = { + value: Set + disabledValue: Set + searchValue: string + pagesMap: DataSourceNotionPageMap + list: DataSourceNotionPage[] + onSelect: (selectedPagesId: Set) => void + canPreview?: boolean + previewPageId?: string + onPreview?: (selectedPageId: string) => void +} +type NotionPageTreeItem = { + children: Set + descendants: Set + depth: number + ancestors: string[] +} & DataSourceNotionPage +type NotionPageTreeMap = Record +type NotionPageItem = { + expand: boolean + depth: number +} & DataSourceNotionPage + +const recursivePushInParentDescendants = ( + pagesMap: DataSourceNotionPageMap, + listTreeMap: NotionPageTreeMap, + current: NotionPageTreeItem, + leafItem: NotionPageTreeItem, +) => { + const parentId = current.parent_id + const pageId = current.page_id + + if (!parentId || !pageId) + return + + if (parentId !== 'root' && pagesMap[parentId]) { + if (!listTreeMap[parentId]) { + const children = new Set([pageId]) + const descendants = new Set([pageId, leafItem.page_id]) + listTreeMap[parentId] = { + ...pagesMap[parentId], + children, + descendants, + depth: 0, + ancestors: [], + } + } + else { + listTreeMap[parentId].children.add(pageId) + listTreeMap[parentId].descendants.add(pageId) + listTreeMap[parentId].descendants.add(leafItem.page_id) + } + leafItem.depth++ + leafItem.ancestors.unshift(listTreeMap[parentId].page_name) + + if (listTreeMap[parentId].parent_id !== 'root') + recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem) + } +} + +const ItemComponent = ({ index, style, data }: ListChildComponentProps<{ + dataList: NotionPageItem[] + handleToggle: (index: number) => void + checkedIds: Set + disabledCheckedIds: Set + handleCheck: (index: number) => void + canPreview?: boolean + handlePreview: (index: number) => void + listMapWithChildrenAndDescendants: NotionPageTreeMap + searchValue: string + previewPageId: string + pagesMap: DataSourceNotionPageMap +}>) => { + const { t } = useTranslation() + const { + dataList, + handleToggle, + checkedIds, + disabledCheckedIds, + handleCheck, + canPreview, + handlePreview, + listMapWithChildrenAndDescendants, + searchValue, + previewPageId, + pagesMap, + } = data + const current = dataList[index] + const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id] + const hasChild = currentWithChildrenAndDescendants.descendants.size > 0 + const ancestors = currentWithChildrenAndDescendants.ancestors + const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name] + const disabled = disabledCheckedIds.has(current.page_id) + + const renderArrow = () => { + if (hasChild) { + return ( +
handleToggle(index)} + > + { + current.expand + ? + : + } +
+ ) + } + if (current.parent_id === 'root' || !pagesMap[current.parent_id]) { + return ( +
+ ) + } + return ( +
+ ) + } + + return ( +
+ { + if (disabled) + return + handleCheck(index) + }} + /> + {!searchValue && renderArrow()} + +
+ {current.page_name} +
+ { + canPreview && ( +
handlePreview(index)}> + {t('common.dataSource.notion.selector.preview')} +
+ ) + } + { + searchValue && ( +
+ {breadCrumbs.join(' / ')} +
+ ) + } +
+ ) +} +const Item = memo(ItemComponent, areEqual) + +const PageSelector = ({ + value, + disabledValue, + searchValue, + pagesMap, + list, + onSelect, + canPreview = true, + previewPageId, + onPreview, +}: PageSelectorProps) => { + const { t } = useTranslation() + const [prevDataList, setPrevDataList] = useState(list) + const [dataList, setDataList] = useState([]) + const [localPreviewPageId, setLocalPreviewPageId] = useState('') + if (prevDataList !== list) { + setPrevDataList(list) + setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => { + return { + ...item, + expand: false, + depth: 0, + } + })) + } + const searchDataList = list.filter((item) => { + return item.page_name.includes(searchValue) + }).map((item) => { + return { + ...item, + expand: false, + depth: 0, + } + }) + const currentDataList = searchValue ? searchDataList : dataList + const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId + + const listMapWithChildrenAndDescendants = useMemo(() => { + return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => { + const pageId = next.page_id + if (!prev[pageId]) + prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] } + + recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId]) + return prev + }, {}) + }, [list, pagesMap]) + + const handleToggle = (index: number) => { + const current = dataList[index] + const pageId = current.page_id + const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId] + const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants) + const childrenIds = Array.from(currentWithChildrenAndDescendants.children) + let newDataList = [] + + if (current.expand) { + current.expand = false + + newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))] + } + else { + current.expand = true + + newDataList = [ + ...dataList.slice(0, index + 1), + ...childrenIds.map(item => ({ + ...pagesMap[item], + expand: false, + depth: listMapWithChildrenAndDescendants[item].depth, + })), + ...dataList.slice(index + 1)] + } + setDataList(newDataList) + } + + const copyValue = new Set([...value]) + const handleCheck = (index: number) => { + const current = currentDataList[index] + const pageId = current.page_id + const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId] + + if (copyValue.has(pageId)) { + if (!searchValue) { + for (const item of currentWithChildrenAndDescendants.descendants) + copyValue.delete(item) + } + + copyValue.delete(pageId) + } + else { + if (!searchValue) { + for (const item of currentWithChildrenAndDescendants.descendants) + copyValue.add(item) + } + + copyValue.add(pageId) + } + + onSelect(new Set([...copyValue])) + } + + const handlePreview = (index: number) => { + const current = currentDataList[index] + const pageId = current.page_id + + setLocalPreviewPageId(pageId) + + if (onPreview) + onPreview(pageId) + } + + if (!currentDataList.length) { + return ( +
+ {t('common.dataSource.notion.selector.noSearchResult')} +
+ ) + } + + return ( + data.dataList[index].page_id} + itemData={{ + dataList: currentDataList, + handleToggle, + checkedIds: value, + disabledCheckedIds: disabledValue, + handleCheck, + canPreview, + handlePreview, + listMapWithChildrenAndDescendants, + searchValue, + previewPageId: currentPreviewPageId, + pagesMap, + }} + > + {Item} + + ) +} + +export default PageSelector diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawled-result-item.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawled-result-item.tsx index 577521b6d3..2e80085cfe 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawled-result-item.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawled-result-item.tsx @@ -5,6 +5,7 @@ import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets' import Checkbox from '@/app/components/base/checkbox' import Button from '@/app/components/base/button' import { useTranslation } from 'react-i18next' +import Radio from '@/app/components/base/radio/ui' type CrawledResultItemProps = { payload: CrawlResultItemType @@ -13,6 +14,7 @@ type CrawledResultItemProps = { isPreview: boolean showPreview: boolean onPreview: () => void + supportMultipleChoice?: boolean } const CrawledResultItem = ({ @@ -22,22 +24,33 @@ const CrawledResultItem = ({ isPreview, onPreview, showPreview, + supportMultipleChoice = true, }: CrawledResultItemProps) => { const { t } = useTranslation() const handleCheckChange = useCallback(() => { onCheckChange(!isChecked) }, [isChecked, onCheckChange]) + return (
- + { + supportMultipleChoice ? ( + + ) : ( + + ) + }
void onPreview?: (payload: CrawlResultItem, index: number) => void usedTime: number + supportMultipleChoice?: boolean } const CrawledResult = ({ @@ -26,6 +27,7 @@ const CrawledResult = ({ onSelectedChange, usedTime, onPreview, + supportMultipleChoice = true, }: CrawledResultProps) => { const { t } = useTranslation() @@ -42,12 +44,11 @@ const CrawledResult = ({ const handleItemCheckChange = useCallback((item: CrawlResultItem) => { return (checked: boolean) => { if (checked) - onSelectedChange([...checkedList, item]) - + supportMultipleChoice ? onSelectedChange([...checkedList, item]) : onSelectedChange([item]) else onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url)) } - }, [checkedList, onSelectedChange]) + }, [checkedList, onSelectedChange, supportMultipleChoice]) const handlePreview = useCallback((index: number) => { if (!onPreview) return @@ -63,12 +64,14 @@ const CrawledResult = ({ })}
-
- -
+ {supportMultipleChoice && ( +
+ +
+ )}
{list.map((item, index) => ( ))}
diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawler.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawler.tsx index 278c22918a..2b72742bb0 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawler.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/website-crawl/base/crawler.tsx @@ -169,6 +169,7 @@ const Crawler = ({ usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0} previewIndex={previewIndex} onPreview={onPreview} + supportMultipleChoice={false} // only support single choice in test run /> )}